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.
- package/bundle/server/dist/adapters/agent-deck-bind.js +443 -0
- package/bundle/server/dist/adapters/agent-deck-bind.test.js +454 -0
- package/bundle/server/dist/adapters/agent-deck.js +38 -50
- package/bundle/server/dist/adapters/agent-deck.test.js +63 -0
- package/bundle/server/dist/adapters/agent-health.js +230 -33
- package/bundle/server/dist/adapters/agent-health.test.js +237 -0
- package/bundle/server/dist/adapters/authority-lifecycle.js +289 -0
- package/bundle/server/dist/adapters/codex-scoped-config.js +103 -0
- package/bundle/server/dist/adapters/execution-authority.js +103 -0
- package/bundle/server/dist/adapters/execution-authority.test.js +166 -0
- package/bundle/server/dist/adapters/external.js +0 -1
- package/bundle/server/dist/adapters/git-worktree.js +514 -0
- package/bundle/server/dist/adapters/git-worktree.test.js +502 -0
- package/bundle/server/dist/adapters/github.js +170 -0
- package/bundle/server/dist/adapters/github.test.js +205 -0
- package/bundle/server/dist/adapters/linear-inbox.js +199 -30
- package/bundle/server/dist/adapters/linear-inbox.test.js +35 -0
- package/bundle/server/dist/adapters/linear-sync.js +3 -101
- package/bundle/server/dist/adapters/managed-repo.js +168 -0
- package/bundle/server/dist/adapters/managed-repo.test.js +63 -0
- package/bundle/server/dist/adapters/outbound-delivery.js +83 -0
- package/bundle/server/dist/adapters/outbound-delivery.test.js +53 -0
- package/bundle/server/dist/adapters/reflect-authority.js +43 -0
- package/bundle/server/dist/cli-env.js +26 -0
- package/bundle/server/dist/cli-env.test.js +14 -1
- package/bundle/server/dist/coordinator/abort.integration.test.js +74 -0
- package/bundle/server/dist/coordinator/admission.js +330 -0
- package/bundle/server/dist/coordinator/admission.test.js +338 -0
- package/bundle/server/dist/coordinator/args.js +128 -0
- package/bundle/server/dist/coordinator/args.test.js +132 -0
- package/bundle/server/dist/coordinator/authority-recovery.integration.test.js +661 -0
- package/bundle/server/dist/coordinator/auto-merge.integration.test.js +363 -0
- package/bundle/server/dist/coordinator/auto-merge.js +286 -0
- package/bundle/server/dist/coordinator/auto-merge.timeout.test.js +14 -0
- package/bundle/server/dist/coordinator/branch-progress.js +81 -0
- package/bundle/server/dist/coordinator/clock-jump.js +98 -0
- package/bundle/server/dist/coordinator/commands.js +1088 -0
- package/bundle/server/dist/coordinator/commands.test.js +554 -0
- package/bundle/server/dist/coordinator/deck-outage-config.js +32 -0
- package/bundle/server/dist/coordinator/deck-outage.integration.test.js +200 -0
- package/bundle/server/dist/coordinator/dependencies.js +242 -0
- package/bundle/server/dist/coordinator/dependency-readiness.integration.test.js +535 -0
- package/bundle/server/dist/coordinator/developer-effect.js +959 -0
- package/bundle/server/dist/coordinator/developer-effect.test.js +1143 -0
- package/bundle/server/dist/coordinator/dispatcher.js +58 -0
- package/bundle/server/dist/coordinator/dispatcher.test.js +81 -0
- package/bundle/server/dist/coordinator/effect-registry.js +17 -0
- package/bundle/server/dist/coordinator/execution-env.integration.test.js +122 -0
- package/bundle/server/dist/coordinator/failure-reason.js +273 -0
- package/bundle/server/dist/coordinator/failure-reason.test.js +234 -0
- package/bundle/server/dist/coordinator/guidance.js +33 -0
- package/bundle/server/dist/coordinator/guidance.test.js +110 -0
- package/bundle/server/dist/coordinator/human-resolution.js +88 -0
- package/bundle/server/dist/coordinator/human-resolution.test.js +72 -0
- package/bundle/server/dist/coordinator/intent-forecast.js +13 -0
- package/bundle/server/dist/coordinator/intent-forecast.test.js +34 -0
- package/bundle/server/dist/coordinator/latest-failure.js +100 -0
- package/bundle/server/dist/coordinator/latest-failure.test.js +125 -0
- package/bundle/server/dist/coordinator/metrics.js +35 -0
- package/bundle/server/dist/coordinator/metrics.test.js +52 -0
- package/bundle/server/dist/coordinator/permissions.js +127 -0
- package/bundle/server/dist/coordinator/permissions.test.js +184 -0
- package/bundle/server/dist/coordinator/presumed-dead-budget.integration.test.js +146 -0
- package/bundle/server/dist/coordinator/process-liveness.js +191 -0
- package/bundle/server/dist/coordinator/profile-snapshot.js +20 -0
- package/bundle/server/dist/coordinator/profile-snapshot.test.js +118 -0
- package/bundle/server/dist/coordinator/projection.js +147 -0
- package/bundle/server/dist/coordinator/projection.test.js +121 -0
- package/bundle/server/dist/coordinator/prompts.js +164 -0
- package/bundle/server/dist/coordinator/prompts.test.js +177 -0
- package/bundle/server/dist/coordinator/queue-by-default.integration.test.js +271 -0
- package/bundle/server/dist/coordinator/reclaim-republish.integration.test.js +372 -0
- package/bundle/server/dist/coordinator/recovery.js +466 -0
- package/bundle/server/dist/coordinator/recovery.test.js +164 -0
- package/bundle/server/dist/coordinator/reflect-trigger.js +218 -0
- package/bundle/server/dist/coordinator/reflect-trigger.test.js +324 -0
- package/bundle/server/dist/coordinator/repair-cycle.integration.test.js +234 -0
- package/bundle/server/dist/coordinator/restart-recovery.integration.test.js +363 -0
- package/bundle/server/dist/coordinator/reviewer-effect.js +507 -0
- package/bundle/server/dist/coordinator/reviewer-effect.test.js +612 -0
- package/bundle/server/dist/coordinator/reviewer-result.js +38 -0
- package/bundle/server/dist/coordinator/reviewer-result.test.js +45 -0
- package/bundle/server/dist/coordinator/routing.js +160 -0
- package/bundle/server/dist/coordinator/routing.test.js +336 -0
- package/bundle/server/dist/coordinator/session-lifecycle.integration.test.js +127 -0
- package/bundle/server/dist/coordinator/session-lifecycle.js +280 -0
- package/bundle/server/dist/coordinator/session-lifecycle.test.js +121 -0
- package/bundle/server/dist/coordinator/session-progress.js +351 -0
- package/bundle/server/dist/coordinator/session-progress.test.js +190 -0
- package/bundle/server/dist/coordinator/session-timeouts.js +33 -0
- package/bundle/server/dist/coordinator/sleep-recovery.integration.test.js +233 -0
- package/bundle/server/dist/coordinator/spawn.js +49 -0
- package/bundle/server/dist/coordinator/sync-issue-base-branch.js +18 -0
- package/bundle/server/dist/coordinator/usage-cap-config.js +10 -0
- package/bundle/server/dist/coordinator/usage-cap-defer.js +189 -0
- package/bundle/server/dist/coordinator/usage-cap.integration.test.js +193 -0
- package/bundle/server/dist/coordinator/usage.js +40 -0
- package/bundle/server/dist/coordinator/usage.test.js +69 -0
- package/bundle/server/dist/coordinator/verification-receipt.js +333 -0
- package/bundle/server/dist/coordinator/verification-receipt.test.js +382 -0
- package/bundle/server/dist/coordinator/worker-loop.js +363 -0
- package/bundle/server/dist/coordinator/worker-loop.test.js +327 -0
- package/bundle/server/dist/coordinator/workflow-instances.test.js +45 -0
- package/bundle/server/dist/coordinator/worktree-owner-liveness.js +82 -0
- package/bundle/server/dist/coordinator/worktree-owner-liveness.test.js +199 -0
- package/bundle/server/dist/db/artifacts-issue-columns.test.js +18 -0
- package/bundle/server/dist/db/authority-attempts-stale-at-migration.test.js +65 -0
- package/bundle/server/dist/db/idempotency-index-migration.test.js +57 -0
- package/bundle/server/dist/db/index.js +359 -7
- package/bundle/server/dist/db/linear-inbox-unlock.test.js +67 -0
- package/bundle/server/dist/db/migrate-to-issues.js +824 -0
- package/bundle/server/dist/db/migrate-to-issues.test.js +842 -0
- package/bundle/server/dist/db/schema.sql +311 -1
- package/bundle/server/dist/db/schema.test.js +66 -0
- package/bundle/server/dist/db/worker-session-pid-migration.test.js +112 -0
- package/bundle/server/dist/dev-review-cli-happy-path.integration.test.js +338 -0
- package/bundle/server/dist/direct-start-liveness.integration.test.js +350 -0
- package/bundle/server/dist/index.js +58 -5
- package/bundle/server/dist/paths.js +40 -4
- package/bundle/server/dist/paths.test.js +42 -0
- package/bundle/server/dist/pid-marker-claim-child.js +18 -0
- package/bundle/server/dist/pid-marker-hold-arbiter-child.js +24 -0
- package/bundle/server/dist/pid-marker.js +141 -0
- package/bundle/server/dist/pid-marker.test.js +208 -0
- package/bundle/server/dist/queue/approve-deliver.js +128 -28
- package/bundle/server/dist/queue/reflect-schedule.js +25 -0
- package/bundle/server/dist/queue/result-qa.js +1 -1
- package/bundle/server/dist/queue/send-gate.test.js +172 -36
- package/bundle/server/dist/repository/agents.js +55 -18
- package/bundle/server/dist/repository/artifacts-for-issue.js +42 -0
- package/bundle/server/dist/repository/artifacts-for-issue.test.js +48 -0
- package/bundle/server/dist/repository/artifacts.js +47 -0
- package/bundle/server/dist/repository/artifacts.test.js +48 -0
- package/bundle/server/dist/repository/authority-attempts.js +189 -0
- package/bundle/server/dist/repository/findings.js +70 -0
- package/bundle/server/dist/repository/findings.test.js +68 -0
- package/bundle/server/dist/repository/human-actions.js +142 -0
- package/bundle/server/dist/repository/human-actions.test.js +123 -0
- package/bundle/server/dist/repository/intake-settings.js +5 -59
- package/bundle/server/dist/repository/issues.js +305 -0
- package/bundle/server/dist/repository/issues.test.js +69 -0
- package/bundle/server/dist/repository/outbound-drafts.js +28 -0
- package/bundle/server/dist/repository/queue-entries.js +162 -0
- package/bundle/server/dist/repository/review-publications.js +153 -0
- package/bundle/server/dist/repository/review-publications.test.js +78 -0
- package/bundle/server/dist/repository/runs-external-id.test.js +5 -3
- package/bundle/server/dist/repository/runs.js +4 -211
- package/bundle/server/dist/repository/runtime-availability.js +54 -0
- package/bundle/server/dist/repository/usage-events.js +60 -0
- package/bundle/server/dist/repository/usage-events.test.js +56 -0
- package/bundle/server/dist/repository/work-items.js +305 -0
- package/bundle/server/dist/repository/work-items.test.js +142 -0
- package/bundle/server/dist/repository/worker-sessions.js +219 -0
- package/bundle/server/dist/repository/worker-sessions.test.js +60 -0
- package/bundle/server/dist/repository/workflow-events.js +193 -0
- package/bundle/server/dist/repository/workflow-events.test.js +89 -0
- package/bundle/server/dist/resolve-tsx-bin.js +19 -0
- package/bundle/server/dist/routes/human-actions.js +68 -0
- package/bundle/server/dist/routes/human-actions.test.js +284 -0
- package/bundle/server/dist/routes/index.js +29 -496
- package/bundle/server/dist/routes/issues.js +240 -0
- package/bundle/server/dist/routes/issues.test.js +451 -0
- package/bundle/server/dist/routes/queue.js +41 -0
- package/bundle/server/dist/routes/queue.test.js +78 -0
- package/bundle/server/dist/runners/claude-args.js +18 -30
- package/bundle/server/dist/runners/claude.js +19 -163
- package/bundle/server/dist/runners/claude.test.js +15 -70
- package/bundle/server/dist/runners/codex-args.js +59 -0
- package/bundle/server/dist/runners/codex-args.test.js +72 -0
- package/bundle/server/dist/runners/codex-jsonl.js +150 -0
- package/bundle/server/dist/runners/codex-jsonl.test.js +36 -0
- package/bundle/server/dist/runners/codex-resume.test.js +19 -0
- package/bundle/server/dist/runners/codex.js +46 -0
- package/bundle/server/dist/runners/models-codex.test.js +14 -0
- package/bundle/server/dist/runners/models.js +53 -1
- package/bundle/server/dist/runners/persist.js +4 -1
- package/bundle/server/dist/runners/process-registry.js +11 -0
- package/bundle/server/dist/runners/prompts.js +13 -280
- package/bundle/server/dist/runners/prompts.test.js +22 -153
- package/bundle/server/dist/runners/qa.js +17 -1
- package/bundle/server/dist/runners/reflect.js +4 -16
- package/bundle/server/dist/runners/spawn-cli.js +197 -0
- package/bundle/server/dist/runners/spawn-cli.test.js +130 -0
- package/bundle/server/dist/runners/stream-json.js +0 -141
- package/bundle/server/dist/runners/usage-cap.js +242 -0
- package/bundle/server/dist/runners/usage-cap.test.js +97 -0
- package/bundle/server/dist/server-liveness.js +37 -0
- package/bundle/server/dist/server-liveness.test.js +82 -0
- package/bundle/server/dist/static-ui.test.js +44 -0
- package/bundle/server/package.json +4 -2
- package/bundle/server/static-ui/assets/index-BTtPGYpY.js +60 -0
- package/bundle/server/static-ui/assets/index-BxTx4b1d.css +1 -0
- package/bundle/server/static-ui/index.html +2 -2
- package/bundle/shared/dist/agents.d.ts +262 -133
- package/bundle/shared/dist/agents.js +85 -19
- package/bundle/shared/dist/cursor-auth-health.d.ts +12 -0
- package/bundle/shared/dist/cursor-auth-health.js +36 -0
- package/bundle/shared/dist/cursor-auth-health.test.d.ts +1 -0
- package/bundle/shared/dist/cursor-auth-health.test.js +40 -0
- package/bundle/shared/dist/findings.d.ts +46 -0
- package/bundle/shared/dist/findings.js +17 -0
- package/bundle/shared/dist/findings.test.d.ts +1 -0
- package/bundle/shared/dist/findings.test.js +20 -0
- package/bundle/shared/dist/github-repo.d.ts +28 -0
- package/bundle/shared/dist/github-repo.js +98 -0
- package/bundle/shared/dist/github-repo.test.d.ts +1 -0
- package/bundle/shared/dist/github-repo.test.js +32 -0
- package/bundle/shared/dist/human-actions.d.ts +66 -0
- package/bundle/shared/dist/human-actions.js +51 -0
- package/bundle/shared/dist/human-actions.test.d.ts +1 -0
- package/bundle/shared/dist/human-actions.test.js +57 -0
- package/bundle/shared/dist/index.d.ts +567 -757
- package/bundle/shared/dist/index.js +22 -36
- package/bundle/shared/dist/issues.d.ts +385 -0
- package/bundle/shared/dist/issues.js +169 -0
- package/bundle/shared/dist/issues.test.d.ts +1 -0
- package/bundle/shared/dist/issues.test.js +72 -0
- package/bundle/shared/dist/outbound-draft.d.ts +6 -0
- package/bundle/shared/dist/outbound-draft.js +4 -0
- package/bundle/shared/dist/plan-triage.d.ts +6 -6
- package/bundle/shared/dist/profile-snapshot.d.ts +154 -0
- package/bundle/shared/dist/profile-snapshot.js +145 -0
- package/bundle/shared/dist/profile-snapshot.test.d.ts +1 -0
- package/bundle/shared/dist/profile-snapshot.test.js +67 -0
- package/bundle/shared/dist/queue-entries.d.ts +89 -0
- package/bundle/shared/dist/queue-entries.js +19 -0
- package/bundle/shared/dist/result-qa.d.ts +2 -2
- package/bundle/shared/dist/runtime-auth-health.d.ts +66 -0
- package/bundle/shared/dist/runtime-auth-health.js +186 -0
- package/bundle/shared/dist/runtime-auth-health.test.d.ts +1 -0
- package/bundle/shared/dist/runtime-auth-health.test.js +200 -0
- package/bundle/shared/dist/runtime-availability.d.ts +51 -0
- package/bundle/shared/dist/runtime-availability.js +13 -0
- package/bundle/shared/dist/runtime.d.ts +1 -1
- package/bundle/shared/dist/runtime.js +1 -1
- package/bundle/shared/dist/usage-events.d.ts +36 -0
- package/bundle/shared/dist/usage-events.js +13 -0
- package/bundle/shared/dist/worker-sessions.d.ts +140 -0
- package/bundle/shared/dist/worker-sessions.js +70 -0
- package/bundle/shared/dist/worker-sessions.test.d.ts +1 -0
- package/bundle/shared/dist/worker-sessions.test.js +39 -0
- package/bundle/shared/dist/workflow.d.ts +78 -0
- package/bundle/shared/dist/workflow.js +59 -0
- package/bundle/shared/dist/workflow.test.d.ts +1 -0
- package/bundle/shared/dist/workflow.test.js +61 -0
- package/bundle/shared/package.json +1 -1
- package/dist/action.d.ts +1 -0
- package/dist/action.js +50 -0
- package/dist/action.test.d.ts +1 -0
- package/dist/action.test.js +70 -0
- package/dist/agent.d.ts +1 -0
- package/dist/agent.js +17 -0
- package/dist/agent.test.d.ts +1 -0
- package/dist/agent.test.js +19 -0
- package/dist/cli-check.d.ts +7 -0
- package/dist/cli-check.js +40 -0
- package/dist/doctor.js +59 -1
- package/dist/http.d.ts +5 -0
- package/dist/http.js +19 -0
- package/dist/index.js +28 -0
- package/dist/issue.d.ts +35 -0
- package/dist/issue.js +143 -0
- package/dist/issue.test.d.ts +1 -0
- package/dist/issue.test.js +240 -0
- package/dist/lifecycle.contract.test.d.ts +1 -0
- package/dist/lifecycle.contract.test.js +135 -0
- package/dist/queue.d.ts +11 -0
- package/dist/queue.js +60 -0
- package/dist/queue.test.d.ts +1 -0
- package/dist/queue.test.js +46 -0
- package/dist/test-fetch-stub.d.ts +6 -0
- package/dist/test-fetch-stub.js +31 -0
- package/package.json +2 -1
- package/templates/prod.env.example +1 -1
- package/bundle/server/static-ui/assets/index-B_7S7xJb.js +0 -79
- 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
|
+
}
|