agent-dealer 0.2.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/bundle/server/dist/adapters/agent-deck-bind.js +443 -0
  2. package/bundle/server/dist/adapters/agent-deck-bind.test.js +454 -0
  3. package/bundle/server/dist/adapters/agent-deck.js +38 -50
  4. package/bundle/server/dist/adapters/agent-deck.test.js +63 -0
  5. package/bundle/server/dist/adapters/agent-health.js +230 -33
  6. package/bundle/server/dist/adapters/agent-health.test.js +237 -0
  7. package/bundle/server/dist/adapters/authority-lifecycle.js +289 -0
  8. package/bundle/server/dist/adapters/codex-scoped-config.js +103 -0
  9. package/bundle/server/dist/adapters/execution-authority.js +103 -0
  10. package/bundle/server/dist/adapters/execution-authority.test.js +166 -0
  11. package/bundle/server/dist/adapters/external.js +0 -1
  12. package/bundle/server/dist/adapters/git-worktree.js +514 -0
  13. package/bundle/server/dist/adapters/git-worktree.test.js +502 -0
  14. package/bundle/server/dist/adapters/github.js +170 -0
  15. package/bundle/server/dist/adapters/github.test.js +205 -0
  16. package/bundle/server/dist/adapters/linear-inbox.js +199 -30
  17. package/bundle/server/dist/adapters/linear-inbox.test.js +35 -0
  18. package/bundle/server/dist/adapters/linear-sync.js +3 -101
  19. package/bundle/server/dist/adapters/managed-repo.js +168 -0
  20. package/bundle/server/dist/adapters/managed-repo.test.js +63 -0
  21. package/bundle/server/dist/adapters/outbound-delivery.js +83 -0
  22. package/bundle/server/dist/adapters/outbound-delivery.test.js +53 -0
  23. package/bundle/server/dist/adapters/reflect-authority.js +43 -0
  24. package/bundle/server/dist/cli-env.js +26 -0
  25. package/bundle/server/dist/cli-env.test.js +14 -1
  26. package/bundle/server/dist/coordinator/abort.integration.test.js +74 -0
  27. package/bundle/server/dist/coordinator/admission.js +330 -0
  28. package/bundle/server/dist/coordinator/admission.test.js +338 -0
  29. package/bundle/server/dist/coordinator/args.js +128 -0
  30. package/bundle/server/dist/coordinator/args.test.js +132 -0
  31. package/bundle/server/dist/coordinator/authority-recovery.integration.test.js +661 -0
  32. package/bundle/server/dist/coordinator/auto-merge.integration.test.js +363 -0
  33. package/bundle/server/dist/coordinator/auto-merge.js +286 -0
  34. package/bundle/server/dist/coordinator/auto-merge.timeout.test.js +14 -0
  35. package/bundle/server/dist/coordinator/branch-progress.js +81 -0
  36. package/bundle/server/dist/coordinator/clock-jump.js +98 -0
  37. package/bundle/server/dist/coordinator/commands.js +1088 -0
  38. package/bundle/server/dist/coordinator/commands.test.js +554 -0
  39. package/bundle/server/dist/coordinator/deck-outage-config.js +32 -0
  40. package/bundle/server/dist/coordinator/deck-outage.integration.test.js +200 -0
  41. package/bundle/server/dist/coordinator/dependencies.js +242 -0
  42. package/bundle/server/dist/coordinator/dependency-readiness.integration.test.js +535 -0
  43. package/bundle/server/dist/coordinator/developer-effect.js +959 -0
  44. package/bundle/server/dist/coordinator/developer-effect.test.js +1143 -0
  45. package/bundle/server/dist/coordinator/dispatcher.js +58 -0
  46. package/bundle/server/dist/coordinator/dispatcher.test.js +81 -0
  47. package/bundle/server/dist/coordinator/effect-registry.js +17 -0
  48. package/bundle/server/dist/coordinator/execution-env.integration.test.js +122 -0
  49. package/bundle/server/dist/coordinator/failure-reason.js +273 -0
  50. package/bundle/server/dist/coordinator/failure-reason.test.js +234 -0
  51. package/bundle/server/dist/coordinator/guidance.js +33 -0
  52. package/bundle/server/dist/coordinator/guidance.test.js +110 -0
  53. package/bundle/server/dist/coordinator/human-resolution.js +88 -0
  54. package/bundle/server/dist/coordinator/human-resolution.test.js +72 -0
  55. package/bundle/server/dist/coordinator/intent-forecast.js +13 -0
  56. package/bundle/server/dist/coordinator/intent-forecast.test.js +34 -0
  57. package/bundle/server/dist/coordinator/latest-failure.js +100 -0
  58. package/bundle/server/dist/coordinator/latest-failure.test.js +125 -0
  59. package/bundle/server/dist/coordinator/metrics.js +35 -0
  60. package/bundle/server/dist/coordinator/metrics.test.js +52 -0
  61. package/bundle/server/dist/coordinator/permissions.js +127 -0
  62. package/bundle/server/dist/coordinator/permissions.test.js +184 -0
  63. package/bundle/server/dist/coordinator/presumed-dead-budget.integration.test.js +146 -0
  64. package/bundle/server/dist/coordinator/process-liveness.js +191 -0
  65. package/bundle/server/dist/coordinator/profile-snapshot.js +20 -0
  66. package/bundle/server/dist/coordinator/profile-snapshot.test.js +118 -0
  67. package/bundle/server/dist/coordinator/projection.js +147 -0
  68. package/bundle/server/dist/coordinator/projection.test.js +121 -0
  69. package/bundle/server/dist/coordinator/prompts.js +164 -0
  70. package/bundle/server/dist/coordinator/prompts.test.js +177 -0
  71. package/bundle/server/dist/coordinator/queue-by-default.integration.test.js +271 -0
  72. package/bundle/server/dist/coordinator/reclaim-republish.integration.test.js +372 -0
  73. package/bundle/server/dist/coordinator/recovery.js +466 -0
  74. package/bundle/server/dist/coordinator/recovery.test.js +164 -0
  75. package/bundle/server/dist/coordinator/reflect-trigger.js +218 -0
  76. package/bundle/server/dist/coordinator/reflect-trigger.test.js +324 -0
  77. package/bundle/server/dist/coordinator/repair-cycle.integration.test.js +234 -0
  78. package/bundle/server/dist/coordinator/restart-recovery.integration.test.js +363 -0
  79. package/bundle/server/dist/coordinator/reviewer-effect.js +507 -0
  80. package/bundle/server/dist/coordinator/reviewer-effect.test.js +612 -0
  81. package/bundle/server/dist/coordinator/reviewer-result.js +38 -0
  82. package/bundle/server/dist/coordinator/reviewer-result.test.js +45 -0
  83. package/bundle/server/dist/coordinator/routing.js +160 -0
  84. package/bundle/server/dist/coordinator/routing.test.js +336 -0
  85. package/bundle/server/dist/coordinator/session-lifecycle.integration.test.js +127 -0
  86. package/bundle/server/dist/coordinator/session-lifecycle.js +280 -0
  87. package/bundle/server/dist/coordinator/session-lifecycle.test.js +121 -0
  88. package/bundle/server/dist/coordinator/session-progress.js +351 -0
  89. package/bundle/server/dist/coordinator/session-progress.test.js +190 -0
  90. package/bundle/server/dist/coordinator/session-timeouts.js +33 -0
  91. package/bundle/server/dist/coordinator/sleep-recovery.integration.test.js +233 -0
  92. package/bundle/server/dist/coordinator/spawn.js +49 -0
  93. package/bundle/server/dist/coordinator/sync-issue-base-branch.js +18 -0
  94. package/bundle/server/dist/coordinator/usage-cap-config.js +10 -0
  95. package/bundle/server/dist/coordinator/usage-cap-defer.js +189 -0
  96. package/bundle/server/dist/coordinator/usage-cap.integration.test.js +193 -0
  97. package/bundle/server/dist/coordinator/usage.js +40 -0
  98. package/bundle/server/dist/coordinator/usage.test.js +69 -0
  99. package/bundle/server/dist/coordinator/verification-receipt.js +333 -0
  100. package/bundle/server/dist/coordinator/verification-receipt.test.js +382 -0
  101. package/bundle/server/dist/coordinator/worker-loop.js +363 -0
  102. package/bundle/server/dist/coordinator/worker-loop.test.js +327 -0
  103. package/bundle/server/dist/coordinator/workflow-instances.test.js +45 -0
  104. package/bundle/server/dist/coordinator/worktree-owner-liveness.js +82 -0
  105. package/bundle/server/dist/coordinator/worktree-owner-liveness.test.js +199 -0
  106. package/bundle/server/dist/db/artifacts-issue-columns.test.js +18 -0
  107. package/bundle/server/dist/db/authority-attempts-stale-at-migration.test.js +65 -0
  108. package/bundle/server/dist/db/idempotency-index-migration.test.js +57 -0
  109. package/bundle/server/dist/db/index.js +359 -7
  110. package/bundle/server/dist/db/linear-inbox-unlock.test.js +67 -0
  111. package/bundle/server/dist/db/migrate-to-issues.js +824 -0
  112. package/bundle/server/dist/db/migrate-to-issues.test.js +842 -0
  113. package/bundle/server/dist/db/schema.sql +311 -1
  114. package/bundle/server/dist/db/schema.test.js +66 -0
  115. package/bundle/server/dist/db/worker-session-pid-migration.test.js +112 -0
  116. package/bundle/server/dist/dev-review-cli-happy-path.integration.test.js +338 -0
  117. package/bundle/server/dist/direct-start-liveness.integration.test.js +350 -0
  118. package/bundle/server/dist/index.js +58 -5
  119. package/bundle/server/dist/paths.js +40 -4
  120. package/bundle/server/dist/paths.test.js +42 -0
  121. package/bundle/server/dist/pid-marker-claim-child.js +18 -0
  122. package/bundle/server/dist/pid-marker-hold-arbiter-child.js +24 -0
  123. package/bundle/server/dist/pid-marker.js +141 -0
  124. package/bundle/server/dist/pid-marker.test.js +208 -0
  125. package/bundle/server/dist/queue/approve-deliver.js +128 -28
  126. package/bundle/server/dist/queue/reflect-schedule.js +25 -0
  127. package/bundle/server/dist/queue/result-qa.js +1 -1
  128. package/bundle/server/dist/queue/send-gate.test.js +172 -36
  129. package/bundle/server/dist/repository/agents.js +55 -18
  130. package/bundle/server/dist/repository/artifacts-for-issue.js +42 -0
  131. package/bundle/server/dist/repository/artifacts-for-issue.test.js +48 -0
  132. package/bundle/server/dist/repository/artifacts.js +47 -0
  133. package/bundle/server/dist/repository/artifacts.test.js +48 -0
  134. package/bundle/server/dist/repository/authority-attempts.js +189 -0
  135. package/bundle/server/dist/repository/findings.js +70 -0
  136. package/bundle/server/dist/repository/findings.test.js +68 -0
  137. package/bundle/server/dist/repository/human-actions.js +142 -0
  138. package/bundle/server/dist/repository/human-actions.test.js +123 -0
  139. package/bundle/server/dist/repository/intake-settings.js +5 -59
  140. package/bundle/server/dist/repository/issues.js +305 -0
  141. package/bundle/server/dist/repository/issues.test.js +69 -0
  142. package/bundle/server/dist/repository/outbound-drafts.js +28 -0
  143. package/bundle/server/dist/repository/queue-entries.js +162 -0
  144. package/bundle/server/dist/repository/review-publications.js +153 -0
  145. package/bundle/server/dist/repository/review-publications.test.js +78 -0
  146. package/bundle/server/dist/repository/runs-external-id.test.js +5 -3
  147. package/bundle/server/dist/repository/runs.js +4 -211
  148. package/bundle/server/dist/repository/runtime-availability.js +54 -0
  149. package/bundle/server/dist/repository/usage-events.js +60 -0
  150. package/bundle/server/dist/repository/usage-events.test.js +56 -0
  151. package/bundle/server/dist/repository/work-items.js +305 -0
  152. package/bundle/server/dist/repository/work-items.test.js +142 -0
  153. package/bundle/server/dist/repository/worker-sessions.js +219 -0
  154. package/bundle/server/dist/repository/worker-sessions.test.js +60 -0
  155. package/bundle/server/dist/repository/workflow-events.js +193 -0
  156. package/bundle/server/dist/repository/workflow-events.test.js +89 -0
  157. package/bundle/server/dist/resolve-tsx-bin.js +19 -0
  158. package/bundle/server/dist/routes/human-actions.js +68 -0
  159. package/bundle/server/dist/routes/human-actions.test.js +284 -0
  160. package/bundle/server/dist/routes/index.js +29 -496
  161. package/bundle/server/dist/routes/issues.js +240 -0
  162. package/bundle/server/dist/routes/issues.test.js +451 -0
  163. package/bundle/server/dist/routes/queue.js +41 -0
  164. package/bundle/server/dist/routes/queue.test.js +78 -0
  165. package/bundle/server/dist/runners/claude-args.js +18 -30
  166. package/bundle/server/dist/runners/claude.js +19 -163
  167. package/bundle/server/dist/runners/claude.test.js +15 -70
  168. package/bundle/server/dist/runners/codex-args.js +59 -0
  169. package/bundle/server/dist/runners/codex-args.test.js +72 -0
  170. package/bundle/server/dist/runners/codex-jsonl.js +150 -0
  171. package/bundle/server/dist/runners/codex-jsonl.test.js +36 -0
  172. package/bundle/server/dist/runners/codex-resume.test.js +19 -0
  173. package/bundle/server/dist/runners/codex.js +46 -0
  174. package/bundle/server/dist/runners/models-codex.test.js +14 -0
  175. package/bundle/server/dist/runners/models.js +53 -1
  176. package/bundle/server/dist/runners/persist.js +4 -1
  177. package/bundle/server/dist/runners/process-registry.js +11 -0
  178. package/bundle/server/dist/runners/prompts.js +13 -280
  179. package/bundle/server/dist/runners/prompts.test.js +22 -153
  180. package/bundle/server/dist/runners/qa.js +17 -1
  181. package/bundle/server/dist/runners/reflect.js +4 -16
  182. package/bundle/server/dist/runners/spawn-cli.js +197 -0
  183. package/bundle/server/dist/runners/spawn-cli.test.js +130 -0
  184. package/bundle/server/dist/runners/stream-json.js +0 -141
  185. package/bundle/server/dist/runners/usage-cap.js +242 -0
  186. package/bundle/server/dist/runners/usage-cap.test.js +97 -0
  187. package/bundle/server/dist/server-liveness.js +37 -0
  188. package/bundle/server/dist/server-liveness.test.js +82 -0
  189. package/bundle/server/dist/static-ui.test.js +44 -0
  190. package/bundle/server/package.json +4 -2
  191. package/bundle/server/static-ui/assets/index-BTtPGYpY.js +60 -0
  192. package/bundle/server/static-ui/assets/index-BxTx4b1d.css +1 -0
  193. package/bundle/server/static-ui/index.html +2 -2
  194. package/bundle/shared/dist/agents.d.ts +262 -133
  195. package/bundle/shared/dist/agents.js +85 -19
  196. package/bundle/shared/dist/cursor-auth-health.d.ts +12 -0
  197. package/bundle/shared/dist/cursor-auth-health.js +36 -0
  198. package/bundle/shared/dist/cursor-auth-health.test.d.ts +1 -0
  199. package/bundle/shared/dist/cursor-auth-health.test.js +40 -0
  200. package/bundle/shared/dist/findings.d.ts +46 -0
  201. package/bundle/shared/dist/findings.js +17 -0
  202. package/bundle/shared/dist/findings.test.d.ts +1 -0
  203. package/bundle/shared/dist/findings.test.js +20 -0
  204. package/bundle/shared/dist/github-repo.d.ts +28 -0
  205. package/bundle/shared/dist/github-repo.js +98 -0
  206. package/bundle/shared/dist/github-repo.test.d.ts +1 -0
  207. package/bundle/shared/dist/github-repo.test.js +32 -0
  208. package/bundle/shared/dist/human-actions.d.ts +66 -0
  209. package/bundle/shared/dist/human-actions.js +51 -0
  210. package/bundle/shared/dist/human-actions.test.d.ts +1 -0
  211. package/bundle/shared/dist/human-actions.test.js +57 -0
  212. package/bundle/shared/dist/index.d.ts +567 -757
  213. package/bundle/shared/dist/index.js +22 -36
  214. package/bundle/shared/dist/issues.d.ts +385 -0
  215. package/bundle/shared/dist/issues.js +169 -0
  216. package/bundle/shared/dist/issues.test.d.ts +1 -0
  217. package/bundle/shared/dist/issues.test.js +72 -0
  218. package/bundle/shared/dist/outbound-draft.d.ts +6 -0
  219. package/bundle/shared/dist/outbound-draft.js +4 -0
  220. package/bundle/shared/dist/plan-triage.d.ts +6 -6
  221. package/bundle/shared/dist/profile-snapshot.d.ts +154 -0
  222. package/bundle/shared/dist/profile-snapshot.js +145 -0
  223. package/bundle/shared/dist/profile-snapshot.test.d.ts +1 -0
  224. package/bundle/shared/dist/profile-snapshot.test.js +67 -0
  225. package/bundle/shared/dist/queue-entries.d.ts +89 -0
  226. package/bundle/shared/dist/queue-entries.js +19 -0
  227. package/bundle/shared/dist/result-qa.d.ts +2 -2
  228. package/bundle/shared/dist/runtime-auth-health.d.ts +66 -0
  229. package/bundle/shared/dist/runtime-auth-health.js +186 -0
  230. package/bundle/shared/dist/runtime-auth-health.test.d.ts +1 -0
  231. package/bundle/shared/dist/runtime-auth-health.test.js +200 -0
  232. package/bundle/shared/dist/runtime-availability.d.ts +51 -0
  233. package/bundle/shared/dist/runtime-availability.js +13 -0
  234. package/bundle/shared/dist/runtime.d.ts +1 -1
  235. package/bundle/shared/dist/runtime.js +1 -1
  236. package/bundle/shared/dist/usage-events.d.ts +36 -0
  237. package/bundle/shared/dist/usage-events.js +13 -0
  238. package/bundle/shared/dist/worker-sessions.d.ts +140 -0
  239. package/bundle/shared/dist/worker-sessions.js +70 -0
  240. package/bundle/shared/dist/worker-sessions.test.d.ts +1 -0
  241. package/bundle/shared/dist/worker-sessions.test.js +39 -0
  242. package/bundle/shared/dist/workflow.d.ts +78 -0
  243. package/bundle/shared/dist/workflow.js +59 -0
  244. package/bundle/shared/dist/workflow.test.d.ts +1 -0
  245. package/bundle/shared/dist/workflow.test.js +61 -0
  246. package/bundle/shared/package.json +1 -1
  247. package/dist/action.d.ts +1 -0
  248. package/dist/action.js +50 -0
  249. package/dist/action.test.d.ts +1 -0
  250. package/dist/action.test.js +70 -0
  251. package/dist/agent.d.ts +1 -0
  252. package/dist/agent.js +17 -0
  253. package/dist/agent.test.d.ts +1 -0
  254. package/dist/agent.test.js +19 -0
  255. package/dist/cli-check.d.ts +7 -0
  256. package/dist/cli-check.js +40 -0
  257. package/dist/doctor.js +59 -1
  258. package/dist/http.d.ts +5 -0
  259. package/dist/http.js +19 -0
  260. package/dist/index.js +28 -0
  261. package/dist/issue.d.ts +35 -0
  262. package/dist/issue.js +143 -0
  263. package/dist/issue.test.d.ts +1 -0
  264. package/dist/issue.test.js +240 -0
  265. package/dist/lifecycle.contract.test.d.ts +1 -0
  266. package/dist/lifecycle.contract.test.js +135 -0
  267. package/dist/queue.d.ts +11 -0
  268. package/dist/queue.js +60 -0
  269. package/dist/queue.test.d.ts +1 -0
  270. package/dist/queue.test.js +46 -0
  271. package/dist/test-fetch-stub.d.ts +6 -0
  272. package/dist/test-fetch-stub.js +31 -0
  273. package/package.json +2 -1
  274. package/templates/prod.env.example +1 -1
  275. package/bundle/server/static-ui/assets/index-B_7S7xJb.js +0 -79
  276. package/bundle/server/static-ui/assets/index-QYv3zmE6.css +0 -1
@@ -0,0 +1,507 @@
1
+ // packages/server/src/coordinator/reviewer-effect.ts
2
+ //
3
+ // The real reviewer effect handler (NOT-62): detached-HEAD worktree at the pinned SHA →
4
+ // deck bind → spawn → parse the structured verdict → re-verify the PR is still at that
5
+ // SHA (stale check) → publish the review to GitHub → ReviewerOutcome. Registered via
6
+ // `registerEffectHandler("reviewer", ...)`.
7
+ //
8
+ // Unlike the developer effect, the reviewer never writes, pushes, or calls `gh` itself —
9
+ // it only returns the JSON verdict `reviewer-result.ts` parses. Diff/checks/findings are
10
+ // computed by this coordinator process and embedded in the prompt (see prompts.ts's
11
+ // module doc: a read-only claude reviewer has no Bash to run `git diff` itself), and
12
+ // publishing happens here, after the worker's session has already ended — the same
13
+ // "credentialed effect never runs inside the worker's own process" boundary NOT-61 used
14
+ // for push/PR-creation (see profile-snapshot.ts's `PermissionPolicy` doc comment).
15
+ //
16
+ // `ReviewerOutcome` (routing.ts, accepted in the NOT-59 kernel review) has no separate
17
+ // `timed_out` or `adapter_failure` kind the way `DeveloperOutcome` does — routing.ts's own
18
+ // doc comment for `session_failed` already covers "failed, timed out, or its worktree
19
+ // checkout failed," while `deck_failure` preserves explicit Agent Deck diagnostics and
20
+ // `publish_failed` covers re-verification / GitHub publication infrastructure. All three
21
+ // remain bounded infra failures; the split is for accurate operator-facing reasons.
22
+ // `deck_unavailable` (NOT-136) is the one that is NOT an infra failure — it means the deck
23
+ // never answered, so there is nothing to charge an attempt for.
24
+ import fs from "node:fs";
25
+ import os from "node:os";
26
+ import path from "node:path";
27
+ import { parseProfileSnapshot, roleCeiling } from "@agent-dealer/shared";
28
+ import { getTaskSnapshot } from "./commands.js";
29
+ import { buildReviewerPrompt, formatDiffForPrompt, TOTAL_DIFF_LIMIT } from "./prompts.js";
30
+ import { guidanceForNextSession } from "./guidance.js";
31
+ import { realReviewerSpawn, reviewerSessionLogPath } from "./spawn.js";
32
+ import { parseReviewerResult, ReviewerResult as ReviewerResultSchema } from "./reviewer-result.js";
33
+ import { createRoleWorktree, safeRemoveWorktree, isWorktreeClean, mergeBase, fetchRef, diffShas, } from "../adapters/git-worktree.js";
34
+ import { ensureIssueRepoCheckout, roleWorktreePathForResolution, resolveCheckoutBaseBranch, } from "../adapters/managed-repo.js";
35
+ import { prepareWorkerDeckConnection, releaseWorkerDeckConnection } from "../adapters/agent-deck-bind.js";
36
+ import { realGithubAdapter } from "../adapters/github.js";
37
+ import { getWorkerSession, patchRunningSession, recordSessionProcess } from "../repository/worker-sessions.js";
38
+ import { COORDINATOR_PROCESS_OWNER, readProcessStartTime } from "./process-liveness.js";
39
+ import { reviewerSessionTimeoutMs } from "./session-timeouts.js";
40
+ import { getWorkItem } from "../repository/work-items.js";
41
+ import { listFindingsForIssue } from "../repository/findings.js";
42
+ import { createIssueArtifact, latestIssueArtifact } from "../repository/artifacts.js";
43
+ import { recordUsageEvent } from "../repository/usage-events.js";
44
+ import { extractSpawnUsage } from "./usage.js";
45
+ import { classifyRunnerLogFailure } from "./failure-reason.js";
46
+ import { recordUsageCapFromLog } from "../runners/usage-cap.js";
47
+ import { emitSessionMilestone, setLiveIntent, shortWorktreePath, startActivitySampler, taskBriefIsComplete, } from "./session-progress.js";
48
+ import { syncIssueBaseBranch } from "./sync-issue-base-branch.js";
49
+ import { claimReviewPublication, getReviewPublication, reclaimFailedReviewPublication, recordReviewPublished, recordReviewPublishFailed, } from "../repository/review-publications.js";
50
+ const num = (name, dflt) => Number(process.env[name] ?? dflt);
51
+ export const reviewerEffectConfig = {
52
+ get sessionTimeoutMs() {
53
+ return reviewerSessionTimeoutMs();
54
+ },
55
+ /** How long a loser waits for an in-flight claimant before giving up as `publish_failed`. */
56
+ get publicationWaitAttempts() {
57
+ return num("REVIEWER_PUBLISH_WAIT_ATTEMPTS", 5);
58
+ },
59
+ get publicationWaitIntervalMs() {
60
+ return num("REVIEWER_PUBLISH_WAIT_INTERVAL_MS", 200);
61
+ },
62
+ };
63
+ function sleep(ms) {
64
+ return new Promise((resolve) => setTimeout(resolve, ms));
65
+ }
66
+ /**
67
+ * Serializes entry into the actual `gh pr review` call across overlapping attempts on
68
+ * the same work item (round 2/3): a read-only "does a review already exist" lookup
69
+ * alone is a check-then-publish race two such attempts could both pass before either
70
+ * has published. Gated on `leaseToken` (round 4) — only whoever currently holds the
71
+ * work item's one lease token can ever win or reclaim this claim, so a zombie whose
72
+ * lease has already been reclaimed can never win it out from under the actual current
73
+ * holder no matter how the two attempts' wall-clock timing falls (see the module doc
74
+ * on `repository/review-publications.ts`). This loops because a loser may find the
75
+ * winner still `claimed` (in flight) rather than settled yet — it waits, bounded,
76
+ * rather than guessing; if the winner instead already failed, this attempt safely
77
+ * reclaims and becomes the new winner itself.
78
+ */
79
+ async function acquireOrAwaitPublication(workItemId, leaseToken) {
80
+ for (let attempt = 0; attempt < reviewerEffectConfig.publicationWaitAttempts; attempt++) {
81
+ if (claimReviewPublication(workItemId, leaseToken))
82
+ return { owns: true };
83
+ const row = getReviewPublication(workItemId);
84
+ if (row?.state === "published")
85
+ return { owns: false, row };
86
+ if (row?.state === "failed") {
87
+ if (reclaimFailedReviewPublication(workItemId, leaseToken))
88
+ return { owns: true };
89
+ continue; // someone else reclaimed it in the same instant — loop and re-check
90
+ }
91
+ // row is null (raced right at the insert boundary) or still 'claimed' (in flight).
92
+ await sleep(reviewerEffectConfig.publicationWaitIntervalMs);
93
+ }
94
+ return { owns: false, row: getReviewPublication(workItemId) };
95
+ }
96
+ const defaultDeps = { spawn: realReviewerSpawn, github: realGithubAdapter };
97
+ const EVENT_FOR_VERDICT = {
98
+ approved: "APPROVE",
99
+ changes_requested: "REQUEST_CHANGES",
100
+ escalated: "COMMENT",
101
+ };
102
+ /** Best-effort: the worker never writes, so a non-clean checkout here is unexpected, not valuable work. */
103
+ async function bestEffortRemove(repo, worktreePath) {
104
+ try {
105
+ await safeRemoveWorktree({ repo, path: worktreePath, role: "reviewer" });
106
+ }
107
+ catch {
108
+ // leave it for crash-recovery inspection — cleanup is a courtesy, not part of the contract
109
+ }
110
+ }
111
+ function readImplementationConclusion(issueId) {
112
+ const artifact = latestIssueArtifact(issueId, "implementation_conclusion");
113
+ if (!artifact?.contentJson)
114
+ return null;
115
+ try {
116
+ return JSON.parse(artifact.contentJson).text ?? null;
117
+ }
118
+ catch {
119
+ return null;
120
+ }
121
+ }
122
+ function readChecksSummary(issueId) {
123
+ const artifact = latestIssueArtifact(issueId, "checks_evidence");
124
+ if (!artifact?.contentJson)
125
+ return null;
126
+ try {
127
+ const data = JSON.parse(artifact.contentJson);
128
+ if (!data.snapshot)
129
+ return null;
130
+ return `${data.snapshot}${data.headSha ? ` (at ${data.headSha.slice(0, 8)})` : ""}`;
131
+ }
132
+ catch {
133
+ return null;
134
+ }
135
+ }
136
+ /**
137
+ * Unique per queued reviewer work item — embedded in the published review body purely
138
+ * as a human-readable audit trail back to the exact work item that produced it.
139
+ * Duplicate-publication prevention itself is the `review_publications` DB claim
140
+ * (`acquireOrAwaitPublication`), not this marker; round 3 moved that guarantee off a
141
+ * GitHub-side lookup entirely (see `github.ts`'s `publishReview` doc comment).
142
+ */
143
+ function reviewMarker(workItemId) {
144
+ return `<!-- agent-dealer:review:${workItemId} -->`;
145
+ }
146
+ function renderReviewBody(result, marker) {
147
+ const lines = [
148
+ `**Verdict:** ${result.verdict}`,
149
+ ``,
150
+ `**Acceptance criteria assessment**`,
151
+ result.acceptanceCriteriaAssessment,
152
+ ``,
153
+ `**Evidence assessment**`,
154
+ result.evidenceAssessment,
155
+ ];
156
+ if (result.findings.length) {
157
+ lines.push(``, `**Findings**`);
158
+ for (const f of result.findings) {
159
+ const loc = f.file ? ` (${f.file}${f.line ? `:${f.line}` : ""})` : "";
160
+ lines.push(`- [${f.severity}] ${f.title}${loc}: ${f.rationale}`);
161
+ }
162
+ }
163
+ if (result.risks.length) {
164
+ lines.push(``, `**Risks**`, ...result.risks.map((r) => `- ${r}`));
165
+ }
166
+ if (result.productScopeQuestion) {
167
+ lines.push(``, `**Escalation — product scope question**`, result.productScopeQuestion);
168
+ }
169
+ lines.push(``, `_agent-dealer automated review (base ${result.baseSha.slice(0, 8)} → head ${result.headSha.slice(0, 8)})_`, marker);
170
+ return lines.join("\n");
171
+ }
172
+ export async function runReviewerEffect(ctx, deps = defaultDeps) {
173
+ const { issue, workItem, instance } = ctx;
174
+ const sessionId = workItem.workerSessionId;
175
+ if (!sessionId)
176
+ return { kind: "session_failed" };
177
+ const leaseToken = workItem.leaseToken;
178
+ if (!leaseToken)
179
+ return { kind: "session_failed" };
180
+ const session = getWorkerSession(sessionId);
181
+ const snapshot = parseProfileSnapshot(session?.profileSnapshotJson);
182
+ const taskSnapshot = getTaskSnapshot(issue);
183
+ const runtime = snapshot?.runtime ?? "claude_code";
184
+ const round = workItem.round;
185
+ const stage = issue.status;
186
+ const milestone = (type, intent, payload) => emitSessionMilestone({
187
+ issueId: issue.id,
188
+ workflowInstanceId: instance.id,
189
+ workerSessionId: sessionId,
190
+ role: "reviewer",
191
+ stage,
192
+ round,
193
+ type,
194
+ intent,
195
+ payload,
196
+ });
197
+ let payload = {};
198
+ try {
199
+ if (workItem.payloadJson)
200
+ payload = JSON.parse(workItem.payloadJson);
201
+ }
202
+ catch {
203
+ payload = {};
204
+ }
205
+ // A reviewer work item is always enqueued with an atHeadSha (commands.ts) — the live
206
+ // issue.headSha fallback only covers a legacy item queued before that was carried.
207
+ const headSha = payload.inputSha ?? issue.headSha;
208
+ if (!headSha)
209
+ return { kind: "session_failed" };
210
+ let worktreePath;
211
+ let result;
212
+ let workerAuthority = null;
213
+ let repoPath;
214
+ let baseBranch;
215
+ try {
216
+ try {
217
+ const checkout = await ensureIssueRepoCheckout(issue.repo);
218
+ repoPath = checkout.repoPath;
219
+ baseBranch = resolveCheckoutBaseBranch(issue.baseBranch, checkout);
220
+ syncIssueBaseBranch(issue, baseBranch);
221
+ const worktree = await createRoleWorktree({
222
+ repo: repoPath,
223
+ role: "reviewer",
224
+ sessionId,
225
+ ref: headSha,
226
+ worktreePath: roleWorktreePathForResolution(checkout, sessionId, "reviewer"),
227
+ });
228
+ worktreePath = worktree.path;
229
+ patchRunningSession(sessionId, { worktreePath });
230
+ milestone("worktree.ready", `Reviewer · worktree ready (round ${round})`, {
231
+ worktreePath: shortWorktreePath(worktreePath),
232
+ headSha: headSha.slice(0, 8),
233
+ });
234
+ // One resolution, two consumers: the materialized MCP config's tool surface below and
235
+ // the spawn args' tool surface further down. They were resolved separately and had to
236
+ // agree by inspection — the exact shape this stack exists to remove (NOT-134 review).
237
+ const policy = snapshot?.permissionPolicy ?? roleCeiling("reviewer");
238
+ if (!snapshot?.deckId) {
239
+ await bestEffortRemove(repoPath, worktreePath);
240
+ return {
241
+ kind: "deck_failure",
242
+ reason: "Agent profile has no Agent Deck — workers never start without one",
243
+ };
244
+ }
245
+ {
246
+ const prepared = await prepareWorkerDeckConnection({
247
+ deckId: snapshot.deckId,
248
+ worktreePath,
249
+ runtime,
250
+ policy,
251
+ verifyCallTool: deps.deckCallTool,
252
+ });
253
+ if (!prepared.ok) {
254
+ await bestEffortRemove(repoPath, worktreePath);
255
+ // NOT-136: see developer-effect — unreachable deck defers, deck errors still fail.
256
+ return prepared.kind === "deck_unavailable"
257
+ ? { kind: "deck_unavailable", reason: prepared.reason }
258
+ : { kind: "deck_failure", reason: prepared.reason };
259
+ }
260
+ workerAuthority = {
261
+ mcpConfigPath: prepared.mcpConfigPath,
262
+ mcpEnv: prepared.mcpEnv,
263
+ };
264
+ milestone("deck.connected", `Reviewer · deck connected (round ${round})`, {
265
+ deckId: snapshot.deckId,
266
+ });
267
+ }
268
+ if (taskBriefIsComplete(taskSnapshot)) {
269
+ milestone("brief.resolved", `Reviewer · brief ready (Task/AC complete)`, {
270
+ resolution: "task_complete",
271
+ });
272
+ }
273
+ else {
274
+ milestone("brief.resolved", `Reviewer · brief via Agent Deck`, {
275
+ resolution: "deferred_to_agent",
276
+ });
277
+ }
278
+ // Recomputed here rather than trusted off `issue.baseSha`: a retry_reviewer_at_new_head
279
+ // round only re-patches `headSha` (commands.ts's applyReviewer), so the issue's stored
280
+ // baseSha could describe a different head than the one this session is pinned to.
281
+ await fetchRef(worktreePath, baseBranch);
282
+ const baseSha = await mergeBase({ repo: worktreePath, base: `origin/${baseBranch}`, head: headSha });
283
+ const diff = await diffShas({ worktreePath, baseSha, headSha });
284
+ const { truncated: diffTruncated } = formatDiffForPrompt(diff);
285
+ const openFindings = listFindingsForIssue(issue.id).filter((f) => f.status === "open" || f.status === "recurring");
286
+ const guidance = guidanceForNextSession(issue.id, sessionId);
287
+ const prompt = buildReviewerPrompt({
288
+ taskSnapshot,
289
+ round: workItem.round,
290
+ baseSha,
291
+ headSha,
292
+ diff,
293
+ implementationConclusion: readImplementationConclusion(issue.id),
294
+ checksSummary: readChecksSummary(issue.id),
295
+ findings: openFindings.length ? openFindings : undefined,
296
+ worktreePath,
297
+ deckId: snapshot?.deckId ?? null,
298
+ guidance: guidance.length ? guidance : undefined,
299
+ });
300
+ // NOT-83 review finding — see developer-effect.ts's identical check for the full
301
+ // rationale: re-verify this item is still leased right before the real spawn, since an
302
+ // abort during worktree/deck-bind setup above can cancel it before ctx.signal's
303
+ // heartbeat-driven abort would ever trip. Never remove the worktree here — nothing has
304
+ // run in it yet.
305
+ if (getWorkItem(workItem.id)?.status !== "leased") {
306
+ return { kind: "session_failed" };
307
+ }
308
+ const logPath = reviewerSessionLogPath(sessionId);
309
+ patchRunningSession(sessionId, { logPath, worktreePath });
310
+ setLiveIntent(issue.id, `Reviewer · session running (round ${round})`);
311
+ const sampler = startActivitySampler({ issueId: issue.id, role: "reviewer", round, logPath });
312
+ const spawnStartedAt = Date.now();
313
+ let spawned;
314
+ try {
315
+ spawned = await deps.spawn({
316
+ sessionId,
317
+ runtime,
318
+ policy,
319
+ model: snapshot?.model ?? null,
320
+ effort: snapshot?.effort ?? null,
321
+ prompt,
322
+ cwd: worktreePath,
323
+ timeoutMs: reviewerEffectConfig.sessionTimeoutMs,
324
+ mcpConfigPath: workerAuthority?.mcpConfigPath,
325
+ mcpEnv: workerAuthority?.mcpEnv,
326
+ logPath,
327
+ // NOT-126: without this the abort stops at the handler — the CLI itself keeps
328
+ // running, editing this worktree under a session already marked failed.
329
+ signal: ctx.signal,
330
+ // NOT-124: persist the CLI's pid so recovery can verify this worker is really
331
+ // gone before presuming it dead on an expired lease.
332
+ // NOT-131: the start time is read here, while the process is known to be this
333
+ // spawn's child, so a successor coordinator can still identify it after a restart.
334
+ onSpawn: (pid) => recordSessionProcess(sessionId, pid, COORDINATOR_PROCESS_OWNER, readProcessStartTime(pid)),
335
+ });
336
+ }
337
+ finally {
338
+ sampler.stop();
339
+ }
340
+ // See developer-effect.ts's identical call: recorded before any early return so a
341
+ // failed/timed-out reviewer session still attributes its incurred cost.
342
+ const usage = extractSpawnUsage(spawned.logPath, runtime);
343
+ recordUsageEvent({
344
+ issueId: issue.id,
345
+ workerSessionId: sessionId,
346
+ role: "reviewer",
347
+ runtime,
348
+ durationMs: Date.now() - spawnStartedAt,
349
+ ...usage,
350
+ });
351
+ // See developer-effect.ts's identical release: the worker's own subprocess has
352
+ // exited, so its deck MCP config has no further legitimate use, and (cursor) its
353
+ // worktree-local MCP config must be gone well before this worktree could ever be
354
+ // reused — don't wait for this function's own return to clean it up.
355
+ if (workerAuthority) {
356
+ await releaseWorkerDeckConnection(workerAuthority);
357
+ workerAuthority = null;
358
+ }
359
+ const usageCap = recordUsageCapFromLog(spawned.logPath, runtime);
360
+ if (usageCap) {
361
+ const clean = await isWorktreeClean(worktreePath).catch(() => false);
362
+ if (!clean)
363
+ return { kind: "session_failed" };
364
+ await bestEffortRemove(repoPath, worktreePath);
365
+ return {
366
+ kind: "usage_capped",
367
+ until: usageCap.unavailableUntil,
368
+ reason: usageCap.reason,
369
+ evidence: usageCap.evidence,
370
+ };
371
+ }
372
+ if (spawned.timedOut || spawned.exitCode !== 0) {
373
+ await bestEffortRemove(repoPath, worktreePath);
374
+ // NOT-133: a reviewer that died on runtime auth reads as "Worker session failed or
375
+ // crashed" without this. Null when nothing classifies, which keeps that fallback.
376
+ return {
377
+ kind: "session_failed",
378
+ reason: classifyRunnerLogFailure(spawned.logPath, runtime) ?? undefined,
379
+ };
380
+ }
381
+ // Persisted as soon as the session itself completes, regardless of how verification/
382
+ // publication subsequently resolves — mirrors developer-effect.ts's evidence timing.
383
+ createIssueArtifact({
384
+ issueId: issue.id,
385
+ workerSessionId: sessionId,
386
+ kind: "reviewer_transcript",
387
+ author: "system",
388
+ blobPath: spawned.logPath,
389
+ });
390
+ const parsed = parseReviewerResult(spawned.transcript);
391
+ if (!parsed) {
392
+ await bestEffortRemove(repoPath, worktreePath);
393
+ return { kind: "session_failed" };
394
+ }
395
+ // The reviewer is told to echo these exact values (prompts.ts's reviewer contract);
396
+ // a result that doesn't cannot be trusted to have actually reviewed the pinned
397
+ // revision — reject it rather than let a wrong-SHA verdict approve or advance the
398
+ // issue (design's exact-SHA protocol applies to the reviewer's output too, not just
399
+ // the developer's handoff).
400
+ if (parsed.baseSha !== baseSha || parsed.headSha !== headSha) {
401
+ await bestEffortRemove(repoPath, worktreePath);
402
+ return { kind: "session_failed" };
403
+ }
404
+ result = parsed;
405
+ // A prompt instruction alone ("don't approve an incomplete diff") is not a structural
406
+ // guarantee — the same reasoning this codebase already applies to tool permissions
407
+ // (args.ts/permissions.ts). If the diff had to be truncated, the reviewer's verdict is
408
+ // overridden to "escalated" in code, regardless of what it actually reported, so a
409
+ // truncated review can never reach final_review or an automatic repair loop.
410
+ if (diffTruncated && result.verdict !== "escalated") {
411
+ createIssueArtifact({
412
+ issueId: issue.id,
413
+ workerSessionId: sessionId,
414
+ kind: "diff_truncated_evidence",
415
+ author: "system",
416
+ content: { reportedVerdict: result.verdict, overriddenTo: "escalated", diffCharLimit: TOTAL_DIFF_LIMIT },
417
+ });
418
+ result = { ...result, verdict: "escalated" };
419
+ }
420
+ }
421
+ catch {
422
+ return { kind: "session_failed" };
423
+ }
424
+ if (issue.prNumber == null) {
425
+ // Can't identify which PR to re-verify/publish against — the detached worktree has
426
+ // no branch for `gh` to fall back to resolving this from.
427
+ await bestEffortRemove(repoPath, worktreePath);
428
+ return { kind: "publish_failed" };
429
+ }
430
+ try {
431
+ const prView = await deps.github.viewPr({ cwd: worktreePath, number: issue.prNumber });
432
+ if (!prView) {
433
+ await bestEffortRemove(repoPath, worktreePath);
434
+ return { kind: "publish_failed" };
435
+ }
436
+ if (prView.headRefOid !== headSha) {
437
+ // design §5: stale output is stored as evidence, not discarded and not published.
438
+ createIssueArtifact({
439
+ issueId: issue.id,
440
+ workerSessionId: sessionId,
441
+ kind: "stale_review_evidence",
442
+ author: "agent",
443
+ content: { review: result, staleAtSha: headSha, currentHeadSha: prView.headRefOid },
444
+ });
445
+ await bestEffortRemove(repoPath, worktreePath);
446
+ return { kind: "stale", currentHeadSha: prView.headRefOid };
447
+ }
448
+ const requestedEvent = EVENT_FOR_VERDICT[result.verdict];
449
+ const claim = await acquireOrAwaitPublication(workItem.id, leaseToken);
450
+ if (!claim.owns) {
451
+ await bestEffortRemove(repoPath, worktreePath);
452
+ // The winner's *actual* published result is authoritative here — never this
453
+ // attempt's own independently-parsed `result`, which a review round found could
454
+ // genuinely disagree with the winner's (two separate reviewer sessions are two
455
+ // separate model calls, and can produce different verdicts/findings for the same
456
+ // diff). A `row` with no recorded result (still `claimed` after every wait
457
+ // attempt, or the winner itself failed) has nothing safe to report — escalate.
458
+ if (claim.row?.state === "published" && claim.row.resultJson) {
459
+ const winnerResult = ReviewerResultSchema.parse(JSON.parse(claim.row.resultJson));
460
+ return { kind: "verdict", result: winnerResult };
461
+ }
462
+ return { kind: "publish_failed" };
463
+ }
464
+ const marker = reviewMarker(workItem.id);
465
+ const bodyDir = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-review-body-"));
466
+ const bodyFilePath = path.join(bodyDir, "body.md");
467
+ fs.writeFileSync(bodyFilePath, renderReviewBody(result, marker));
468
+ const published = await deps.github.publishReview({
469
+ cwd: worktreePath,
470
+ number: issue.prNumber,
471
+ event: requestedEvent,
472
+ bodyFilePath,
473
+ });
474
+ fs.rmSync(bodyDir, { recursive: true, force: true });
475
+ if (!published.ok) {
476
+ recordReviewPublishFailed(workItem.id);
477
+ await bestEffortRemove(repoPath, worktreePath);
478
+ return { kind: "publish_failed" };
479
+ }
480
+ recordReviewPublished(workItem.id, {
481
+ resultJson: JSON.stringify(result),
482
+ event: published.event,
483
+ usedCommentFallback: published.usedCommentFallback,
484
+ });
485
+ createIssueArtifact({
486
+ issueId: issue.id,
487
+ workerSessionId: sessionId,
488
+ kind: "review_published",
489
+ author: "system",
490
+ content: { event: published.event, usedCommentFallback: published.usedCommentFallback, verdict: result.verdict },
491
+ });
492
+ await bestEffortRemove(repoPath, worktreePath);
493
+ return { kind: "verdict", result };
494
+ }
495
+ catch {
496
+ await bestEffortRemove(repoPath, worktreePath);
497
+ return { kind: "publish_failed" };
498
+ }
499
+ }
500
+ finally {
501
+ // See developer-effect.ts's identical finally: the worker's subprocess is done (or
502
+ // never started) by every path through this function, so its per-attempt MCP config
503
+ // is cleaned up here.
504
+ if (workerAuthority)
505
+ await releaseWorkerDeckConnection(workerAuthority);
506
+ }
507
+ }