@effect-agent/pr-review 0.1.0-beta.22 → 0.1.0-beta.23

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.
@@ -1,443 +1,10 @@
1
- import { An as ReviewConcern, F as buildProfileMission, Gn as resolveGuidance, H as toStoredFinding, Hn as makeReviewInstructions, Ht as reviewCandidateSubjectKey, I as computeProfileFingerprint, L as fromStoredConcern, Ln as clampMaxFindings, Mn as ReviewMission, Nt as fanOutHandlersLayerFor, O as ReviewState, On as ReadFile, Ot as assessmentSettlesSuggestionExactly, Pn as ReviewToolkitLayer, Qn as ReviewInputViolation, R as fromStoredFinding, Rn as defaultReviewPolicy, V as toStoredConcern, Xn as PullRequestSource, Y as renderFingerprintMarker, Yn as PullRequestMetadata, at as FileReviewDelegationFailure, cn as planReviewUnits, cr as isReviewableFile, d as gitHubReviewPublisherLayer, dt as FileReviewToolkitLayer, er as anchorViolation, f as gitHubReviewRetirementHostLayer, fn as CodeReview, jn as ReviewFinding, kn as ReadFileDiff, kt as confirmedFindingForPublication, l as gitHubPriorReviewsLayer, ln as rankAndDedupeFindings, lt as FileReviewRequest, mn as FileDiffView, n as GitHubApiFailure, o as PublishedReview, pn as FileDiffQuery, pt as FileReviewUnitResult, q as computeChangesetFingerprint, r as GitHubReviewTarget, s as ReviewPublisher, sn as findingAnchorInUnitEvidence, t as DEFAULT_GITHUB_REVIEW_AUTHOR_LOGIN, tt as FanOutCoordinatorToolkitLayer, u as gitHubPullRequestSourceLayer, w as ReviewExecutionContext, wt as ReviewCandidate, yn as ListChangedFiles, zt as makeFanOutReviewSuite } from "./github-BgtP7Rdv.mjs";
1
+ import { An as clampMaxFindings, Cn as ReviewConcern, Dn as ReviewToolkitLayer, Fn as makeReviewInstructions, Gn as ReviewInputViolation, Gt as rankAndDedupeConcerns, H as toStoredConcern, Hn as PullRequestMetadata, I as buildProfileMission, J as computeChangesetFingerprint, Kt as rankAndDedupeFindings, L as computeProfileFingerprint, Qt as assessFlatReview, R as fromStoredConcern, Rn as resolveGuidance, Sn as ReadFileDiff, T as ReviewExecutionContext, Tn as ReviewMission, U as toStoredFinding, Un as PullRequestSource, X as renderFingerprintMarker, Xt as ReviewCoverage, Yt as ReviewAssurance, Zt as ReviewInputCoverage, an as CodeReview, d as gitHubReviewPublisherLayer, en as compatibilityCoverage, f as gitHubReviewRetirementHostLayer, fn as ListChangedFiles, jn as defaultReviewPolicy, k as ReviewState, l as gitHubPriorReviewsLayer, n as GitHubApiFailure, o as PublishedReview, qn as anchorViolation, r as GitHubReviewTarget, s as ReviewPublisher, t as DEFAULT_GITHUB_REVIEW_AUTHOR_LOGIN, tn as fanOutInputCoverage, u as gitHubPullRequestSourceLayer, wn as ReviewFinding, wt as runFanOutReview, xn as ReadFile, xt as makeFileReviewerDefinition, z as fromStoredFinding } from "./github-BbwYzNrC.mjs";
2
2
  import { Config, Context, DateTime, Effect, FileSystem, Layer, Option, Ref, Schema } from "effect";
3
- import { Agent, AgentPolicy, AgentRuntime, IdGenerator, SubagentReservationsMemoryLive, UsageBudgetLimits, UsageTotals, getToolExecutionClass, makeUsageBudget, toRunBudgetHook } from "effect-agent";
3
+ import { Agent, AgentPolicy, AgentRuntime, IdGenerator, UsageBudgetLimits, UsageTotals, getToolExecutionClass, makeUsageBudget, toRunBudgetHook } from "effect-agent";
4
4
  import { Toolkit } from "effect/unstable/ai";
5
5
  import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
6
6
  import { AnthropicClient, AnthropicLanguageModel } from "@effect/ai-anthropic";
7
7
  import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai";
8
- //#region src/internal/coverage.ts
9
- const ReviewShape = Schema.Literals(["flat", "fan-out"]);
10
- var FailedReviewUnit = class extends Schema.Class("@effect-agent/pr-review/FailedReviewUnit")({
11
- unitId: Schema.NonEmptyString.check(Schema.isMaxLength(32)),
12
- errorTag: Schema.NonEmptyString.check(Schema.isMaxLength(256))
13
- }) {};
14
- /**
15
- * Compatibility diagnostic retained for callers that consumed the original
16
- * `coverage` field. New UI and state decisions use ReviewInputCoverage and
17
- * ReviewAssurance directly.
18
- */
19
- var ReviewCoverage = class extends Schema.Class("@effect-agent/pr-review/ReviewCoverage")({
20
- status: Schema.Literals(["complete", "incomplete"]),
21
- requiredPaths: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(512))).check(Schema.isMaxLength(300)),
22
- reviewedPaths: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(512))).check(Schema.isMaxLength(300)),
23
- unreviewedPaths: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(512))).check(Schema.isMaxLength(300)),
24
- failedUnits: Schema.Array(FailedReviewUnit).check(Schema.isMaxLength(8)),
25
- reasons: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(1e3))).check(Schema.isMaxLength(32))
26
- }) {};
27
- var ReviewInputCoverage = class extends Schema.Class("@effect-agent/pr-review/ReviewInputCoverage")({
28
- status: Schema.Literals(["complete", "incomplete"]),
29
- requiredPaths: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(512))).check(Schema.isMaxLength(300)),
30
- assignedPaths: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(512))).check(Schema.isMaxLength(300)),
31
- /** Assigned paths whose model-visible diff was truncated by the evidence bound. */
32
- partialPaths: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(512))).check(Schema.isMaxLength(300)),
33
- unassignedPaths: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(512))).check(Schema.isMaxLength(300)),
34
- reasons: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(1e3))).check(Schema.isMaxLength(20))
35
- }) {};
36
- var FailedReviewPass = class extends Schema.Class("@effect-agent/pr-review/FailedReviewPass")({
37
- workId: Schema.NonEmptyString.check(Schema.isMaxLength(96)),
38
- stage: Schema.Literals([
39
- "discovery",
40
- "specialist",
41
- "verification"
42
- ]),
43
- errorTag: Schema.NonEmptyString.check(Schema.isMaxLength(256))
44
- }) {};
45
- var ReviewAssurance = class extends Schema.Class("@effect-agent/pr-review/ReviewAssurance")({
46
- status: Schema.Literals([
47
- "settled",
48
- "incomplete",
49
- "unverified"
50
- ]),
51
- requiredGeneralDiscoveryPasses: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
52
- completedGeneralDiscoveryPasses: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
53
- requiredSpecialistPasses: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
54
- completedSpecialistPasses: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
55
- requiredVerificationPasses: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
56
- completedVerificationPasses: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
57
- discoveredCandidates: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
58
- confirmedCandidates: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
59
- rejectedCandidates: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
60
- unsettledCandidates: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
61
- /** Every failure remains visible within the coordinator's 32-call hard bound. */
62
- failedPasses: Schema.Array(FailedReviewPass).check(Schema.isMaxLength(64)),
63
- reasons: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(1e3))).check(Schema.isMaxLength(32))
64
- }) {};
65
- const toolTrace = (events) => {
66
- const declared = /* @__PURE__ */ new Map();
67
- const succeeded = /* @__PURE__ */ new Map();
68
- const failed = /* @__PURE__ */ new Map();
69
- for (const event of events) {
70
- if (event._tag === "ToolCallDeclared") declared.set(event.toolCallId, event);
71
- if (event._tag === "ToolCallSucceeded") succeeded.set(event.toolCallId, event);
72
- if (event._tag === "ToolCallFailed") failed.set(event.toolCallId, event);
73
- }
74
- return {
75
- declared,
76
- succeeded,
77
- failed
78
- };
79
- };
80
- const sortedUnique = (values) => [...new Set(values)].sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
81
- const boundedListReason = (label, values) => {
82
- const items = sortedUnique(values);
83
- let rendered = `${label} (${items.length}): `;
84
- for (let index = 0; index < items.length; index += 1) {
85
- const item = items[index] ?? "";
86
- const separator = index === 0 ? "" : ", ";
87
- const omitted = items.length - index - 1;
88
- const suffix = omitted === 0 ? "" : ` … (+${omitted} more)`;
89
- if (`${rendered}${separator}${item}${suffix}`.length > 1e3) {
90
- const omission = `… (+${items.length - index} more)`;
91
- return `${rendered.slice(0, 1e3 - omission.length)}${omission}`;
92
- }
93
- rendered = `${rendered}${separator}${item}`;
94
- }
95
- return rendered;
96
- };
97
- const flatInputCoverage = (files, totalFiles, trace) => {
98
- const requiredPaths = sortedUnique(files.map((file) => file.path));
99
- const assigned = /* @__PURE__ */ new Set();
100
- const partial = /* @__PURE__ */ new Set();
101
- const failedPaths = /* @__PURE__ */ new Set();
102
- for (const [toolCallId, declaration] of trace.declared) {
103
- if (declaration.toolName !== "read_file_diff") continue;
104
- const query = Schema.decodeUnknownOption(FileDiffQuery)(declaration.parameters);
105
- if (Option.isNone(query)) continue;
106
- const success = trace.succeeded.get(toolCallId);
107
- if (success !== void 0) {
108
- assigned.add(query.value.path);
109
- const view = Schema.decodeUnknownOption(FileDiffView)(success.result);
110
- if (Option.isSome(view) && view.value.truncated) partial.add(query.value.path);
111
- }
112
- if (trace.failed.has(toolCallId)) failedPaths.add(query.value.path);
113
- }
114
- const undiffable = files.filter((file) => !isReviewableFile(file)).map((file) => file.path);
115
- const unassigned = requiredPaths.filter((path) => !assigned.has(path) || undiffable.includes(path) || failedPaths.has(path));
116
- const reasons = [];
117
- if (files.length < totalFiles) reasons.push(`review range exposed ${files.length} of ${totalFiles} required files`);
118
- if (undiffable.length > 0) reasons.push(boundedListReason("required paths have no reviewable diff or bounded text", undiffable));
119
- if (failedPaths.size > 0) reasons.push(boundedListReason("diff reads failed", failedPaths));
120
- if (partial.size > 0) reasons.push(boundedListReason("model-visible diff evidence was truncated", partial));
121
- if (unassigned.length > 0) reasons.push(boundedListReason("required paths received no successful diff input", unassigned));
122
- return ReviewInputCoverage.make({
123
- status: reasons.length === 0 ? "complete" : "incomplete",
124
- requiredPaths,
125
- assignedPaths: sortedUnique(assigned),
126
- partialPaths: sortedUnique(partial),
127
- unassignedPaths: sortedUnique(unassigned),
128
- reasons
129
- });
130
- };
131
- const fanOutInputCoverage = (files, totalFiles) => {
132
- const plan = planReviewUnits(files, { totalChangedFiles: totalFiles });
133
- const assignedPaths = sortedUnique(plan.units.flatMap((unit) => unit.paths));
134
- const unassignedPaths = sortedUnique([...plan.undiffablePaths, ...plan.unassignedPaths]);
135
- const reasons = [];
136
- if (plan.truncated) reasons.push(`review range exposed ${files.length} of ${totalFiles} required files`);
137
- if (plan.undiffablePaths.length > 0) reasons.push(boundedListReason("required paths have no reviewable diff or bounded text", plan.undiffablePaths));
138
- if (plan.partialEvidencePaths.length > 0) reasons.push(boundedListReason("fan-out capacity left some deterministic evidence shards unassigned", plan.partialEvidencePaths));
139
- if (plan.unassignedEvidenceShardCount > 0) {
140
- reasons.push(`${plan.unassignedEvidenceShardCount} deterministic evidence shard(s) exceeded fan-out capacity`);
141
- reasons.push(boundedListReason(`unassigned evidence shard identifier sample (${plan.unassignedEvidenceShardIds.length} of ${plan.unassignedEvidenceShardCount})`, plan.unassignedEvidenceShardIds));
142
- }
143
- if (plan.unassignedPaths.length > 0) reasons.push(boundedListReason("fan-out capacity left paths unassigned", plan.unassignedPaths));
144
- return ReviewInputCoverage.make({
145
- status: reasons.length === 0 ? "complete" : "incomplete",
146
- requiredPaths: sortedUnique(files.map((file) => file.path)),
147
- assignedPaths,
148
- partialPaths: plan.partialEvidencePaths,
149
- unassignedPaths,
150
- reasons
151
- });
152
- };
153
- const sameStrings = (left, right) => left.length === right.length && left.every((value, index) => value === right[index]);
154
- const candidateKey = (candidate) => JSON.stringify(Schema.encodeSync(ReviewCandidate)(candidate));
155
- const sameCandidates = (left, right) => left.length === right.length && left.every((candidate, index) => {
156
- const corresponding = right[index];
157
- return corresponding !== void 0 && candidateKey(candidate) === candidateKey(corresponding);
158
- });
159
- const delegationDeclarations = (trace) => [...trace.declared].flatMap(([id, declaration]) => {
160
- if (declaration.toolName !== "delegate_file_review") return [];
161
- const request = Schema.decodeUnknownOption(FileReviewRequest)(declaration.parameters);
162
- return Option.isNone(request) ? [] : [{
163
- id,
164
- request: request.value
165
- }];
166
- });
167
- const failureTag = (trace, id) => {
168
- const failed = trace.failed.get(id);
169
- if (failed !== void 0) return failed.errorTag;
170
- const succeeded = trace.succeeded.get(id);
171
- if (succeeded === void 0) return void 0;
172
- const returned = Schema.decodeUnknownOption(FileReviewDelegationFailure)(succeeded.result);
173
- if (Option.isNone(returned)) return void 0;
174
- return returned.value._tag === "FileReviewUnitFailed" ? `${returned.value._tag}:${returned.value.childErrorTag}` : returned.value._tag;
175
- };
176
- const exactDiscoveryRequest = (request, pass) => request.phase === "discovery" && request.workId === pass.passId && request.unitId === pass.unitId && request.perspective === pass.perspective && sameStrings(request.paths, pass.paths) && sameStrings(request.evidenceShardIds, pass.evidenceShardIds) && sameStrings(request.riskCategories, pass.riskCategories) && request.candidates.length === 0;
177
- const validCandidate = (candidate, pass, unit, files, anchorFiles) => {
178
- const allowed = new Set(pass.paths);
179
- const kind = candidate._tag === "FindingCandidate" ? "finding" : "concern";
180
- const idPrefix = `${pass.passId}:${kind}:`;
181
- return candidate.candidateId.startsWith(idPrefix) && /^\d{3}$/.test(candidate.candidateId.slice(idPrefix.length)) && candidate.workId === pass.passId && candidate.unitId === pass.unitId && candidate.evidencePaths.length > 0 && candidate.evidencePaths.every((path) => allowed.has(path)) && (candidate._tag !== "FindingCandidate" || allowed.has(candidate.finding.path) && anchorViolation(candidate.finding, anchorFiles) === void 0 && findingAnchorInUnitEvidence(candidate.finding, unit, files));
182
- };
183
- const flatAssurance = () => ({
184
- assurance: ReviewAssurance.make({
185
- status: "unverified",
186
- requiredGeneralDiscoveryPasses: 1,
187
- completedGeneralDiscoveryPasses: 1,
188
- requiredSpecialistPasses: 0,
189
- completedSpecialistPasses: 0,
190
- requiredVerificationPasses: 1,
191
- completedVerificationPasses: 0,
192
- discoveredCandidates: 0,
193
- confirmedCandidates: 0,
194
- rejectedCandidates: 0,
195
- unsettledCandidates: 0,
196
- failedPasses: [],
197
- reasons: ["flat review has no independent candidate-verification pass; use the fan-out pipeline for a settled assurance result"]
198
- }),
199
- confirmedFindings: [],
200
- confirmedConcerns: [],
201
- walkthrough: []
202
- });
203
- const fanOutAssurance = (files, totalFiles, anchorFiles, trace) => {
204
- const plan = planReviewUnits(files, { totalChangedFiles: totalFiles });
205
- const declarations = delegationDeclarations(trace);
206
- const consumedDeclarationIds = /* @__PURE__ */ new Set();
207
- const failedPasses = [];
208
- const reasons = [];
209
- const candidatesByUnit = /* @__PURE__ */ new Map();
210
- const walkthrough = [];
211
- let completedGeneralDiscoveryPasses = 0;
212
- let completedSpecialistPasses = 0;
213
- for (const pass of plan.discoveryPasses) {
214
- const unit = plan.units.find((candidate) => candidate.unitId === pass.unitId);
215
- const matching = declarations.filter(({ request }) => exactDiscoveryRequest(request, pass));
216
- const stage = pass.perspective === "risk-specialist" ? "specialist" : "discovery";
217
- if (matching.length !== 1) {
218
- failedPasses.push(FailedReviewPass.make({
219
- workId: pass.passId,
220
- stage,
221
- errorTag: matching.length === 0 ? "PassNotAssigned" : "PassAssignedMultipleTimes"
222
- }));
223
- continue;
224
- }
225
- const call = matching[0];
226
- if (call === void 0) {
227
- failedPasses.push(FailedReviewPass.make({
228
- workId: pass.passId,
229
- stage,
230
- errorTag: "PassLookupInvariantFailed"
231
- }));
232
- continue;
233
- }
234
- consumedDeclarationIds.add(call.id);
235
- const failure = failureTag(trace, call.id);
236
- const succeeded = trace.succeeded.get(call.id);
237
- const result = succeeded === void 0 ? Option.none() : Schema.decodeUnknownOption(FileReviewUnitResult)(succeeded.result);
238
- if (failure !== void 0 || Option.isNone(result) || result.value.phase !== "discovery" || result.value.workId !== pass.passId || result.value.unitId !== pass.unitId || result.value.assessments.length !== 0) {
239
- failedPasses.push(FailedReviewPass.make({
240
- workId: pass.passId,
241
- stage,
242
- errorTag: failure ?? "DiscoveryDidNotSettleExactly"
243
- }));
244
- continue;
245
- }
246
- const ids = /* @__PURE__ */ new Set();
247
- let candidatesValid = true;
248
- for (const candidate of result.value.candidates) {
249
- if (unit === void 0 || ids.has(candidate.candidateId) || !validCandidate(candidate, pass, unit, files, anchorFiles)) {
250
- candidatesValid = false;
251
- break;
252
- }
253
- ids.add(candidate.candidateId);
254
- }
255
- const unitCandidates = candidatesByUnit.get(pass.unitId) ?? [];
256
- const unitIds = new Set(unitCandidates.map((candidate) => candidate.candidateId));
257
- if (result.value.candidates.some((candidate) => unitIds.has(candidate.candidateId))) candidatesValid = false;
258
- if (!candidatesValid) {
259
- failedPasses.push(FailedReviewPass.make({
260
- workId: pass.passId,
261
- stage,
262
- errorTag: "DiscoveryCandidateMismatch"
263
- }));
264
- continue;
265
- }
266
- if (stage === "specialist") completedSpecialistPasses += 1;
267
- else completedGeneralDiscoveryPasses += 1;
268
- const subjectKeys = new Set(unitCandidates.map(reviewCandidateSubjectKey));
269
- for (const candidate of result.value.candidates) {
270
- const subjectKey = reviewCandidateSubjectKey(candidate);
271
- if (subjectKeys.has(subjectKey)) continue;
272
- subjectKeys.add(subjectKey);
273
- unitCandidates.push(candidate);
274
- }
275
- candidatesByUnit.set(pass.unitId, unitCandidates);
276
- if (pass.perspective === "general") {
277
- const allowed = new Set(pass.paths);
278
- walkthrough.push(...result.value.fileSummaries.filter((entry) => allowed.has(entry.path)));
279
- }
280
- }
281
- const confirmedCandidates = [];
282
- let rejectedCandidates = 0;
283
- let unsettledCandidates = 0;
284
- let requiredVerificationPasses = 0;
285
- let completedVerificationPasses = 0;
286
- for (const unit of plan.units) {
287
- const candidates = candidatesByUnit.get(unit.unitId) ?? [];
288
- if (candidates.length === 0) continue;
289
- requiredVerificationPasses += 1;
290
- const workId = `${unit.unitId}-verification`;
291
- const matching = declarations.filter(({ request }) => request.phase === "verification" && request.workId === workId && request.unitId === unit.unitId && request.perspective === "candidate-verification" && sameStrings(request.paths, unit.paths) && sameStrings(request.evidenceShardIds, unit.evidenceShards.map((shard) => shard.shardId)) && sameStrings(request.riskCategories, unit.riskCategories) && sameCandidates(request.candidates, candidates));
292
- if (matching.length !== 1) {
293
- unsettledCandidates += candidates.length;
294
- failedPasses.push(FailedReviewPass.make({
295
- workId,
296
- stage: "verification",
297
- errorTag: matching.length === 0 ? "VerificationNotAssignedOrCandidateMismatch" : "VerificationAssignedMultipleTimes"
298
- }));
299
- continue;
300
- }
301
- const call = matching[0];
302
- if (call === void 0) {
303
- unsettledCandidates += candidates.length;
304
- failedPasses.push(FailedReviewPass.make({
305
- workId,
306
- stage: "verification",
307
- errorTag: "PassLookupInvariantFailed"
308
- }));
309
- continue;
310
- }
311
- consumedDeclarationIds.add(call.id);
312
- const failure = failureTag(trace, call.id);
313
- const succeeded = trace.succeeded.get(call.id);
314
- const result = succeeded === void 0 ? Option.none() : Schema.decodeUnknownOption(FileReviewUnitResult)(succeeded.result);
315
- if (failure !== void 0 || Option.isNone(result) || result.value.phase !== "verification" || result.value.workId !== workId || result.value.unitId !== unit.unitId || result.value.candidates.length !== 0 || result.value.fileSummaries.length !== 0) {
316
- unsettledCandidates += candidates.length;
317
- failedPasses.push(FailedReviewPass.make({
318
- workId,
319
- stage: "verification",
320
- errorTag: failure ?? "VerificationDidNotSettleExactly"
321
- }));
322
- continue;
323
- }
324
- const byId = new Map(candidates.map((candidate) => [candidate.candidateId, candidate]));
325
- const assessedIds = /* @__PURE__ */ new Set();
326
- let suggestionSettlementExact = true;
327
- const exactAssessments = result.value.assessments.every((assessment) => {
328
- const candidate = byId.get(assessment.candidateId);
329
- if (candidate === void 0 || assessedIds.has(assessment.candidateId)) return false;
330
- assessedIds.add(assessment.candidateId);
331
- if (!assessmentSettlesSuggestionExactly(assessment, candidate)) suggestionSettlementExact = false;
332
- return true;
333
- });
334
- if (byId.size !== candidates.length || !exactAssessments || assessedIds.size !== byId.size || !suggestionSettlementExact) {
335
- unsettledCandidates += candidates.length;
336
- failedPasses.push(FailedReviewPass.make({
337
- workId,
338
- stage: "verification",
339
- errorTag: exactAssessments && !suggestionSettlementExact ? "SuggestionSettlementMismatch" : "VerificationAssessmentMismatch"
340
- }));
341
- continue;
342
- }
343
- completedVerificationPasses += 1;
344
- for (const assessment of result.value.assessments) if (assessment.disposition === "confirmed") {
345
- const candidate = byId.get(assessment.candidateId);
346
- if (candidate !== void 0) confirmedCandidates.push({
347
- assessment,
348
- candidate
349
- });
350
- } else rejectedCandidates += 1;
351
- }
352
- const unexpected = [...trace.declared].filter(([id, declaration]) => declaration.toolName === "delegate_file_review" && !consumedDeclarationIds.has(id));
353
- for (const [, declaration] of unexpected) {
354
- const request = Schema.decodeUnknownOption(FileReviewRequest)(declaration.parameters);
355
- failedPasses.push(FailedReviewPass.make({
356
- workId: Option.isSome(request) ? request.value.workId : "invalid-delegation-request",
357
- stage: Option.isSome(request) && request.value.phase === "verification" ? "verification" : "discovery",
358
- errorTag: "UnexpectedPass"
359
- }));
360
- }
361
- if (failedPasses.length > 0) reasons.push(boundedListReason("configured review passes did not settle", failedPasses.map((pass) => `${pass.workId} (${pass.errorTag})`)));
362
- if (unsettledCandidates > 0) reasons.push(`${unsettledCandidates} discovered candidate(s) did not receive exact verification`);
363
- const requiredSpecialistPasses = plan.discoveryPasses.filter((pass) => pass.perspective === "risk-specialist").length;
364
- const requiredGeneralDiscoveryPasses = plan.discoveryPasses.length - requiredSpecialistPasses;
365
- const discoveredCandidates = [...candidatesByUnit.values()].reduce((total, candidates) => total + candidates.length, 0);
366
- return {
367
- assurance: ReviewAssurance.make({
368
- status: reasons.length === 0 ? "settled" : "incomplete",
369
- requiredGeneralDiscoveryPasses,
370
- completedGeneralDiscoveryPasses,
371
- requiredSpecialistPasses,
372
- completedSpecialistPasses,
373
- requiredVerificationPasses,
374
- completedVerificationPasses,
375
- discoveredCandidates,
376
- confirmedCandidates: confirmedCandidates.length,
377
- rejectedCandidates,
378
- unsettledCandidates,
379
- failedPasses,
380
- reasons
381
- }),
382
- confirmedFindings: confirmedCandidates.flatMap(({ assessment, candidate }) => candidate._tag === "FindingCandidate" ? [confirmedFindingForPublication(assessment, candidate)] : []),
383
- confirmedConcerns: confirmedCandidates.flatMap(({ candidate }) => candidate._tag === "ConcernCandidate" ? [candidate.concern] : []),
384
- walkthrough
385
- };
386
- };
387
- const compatibilityCoverage = (inputCoverage, assurance) => {
388
- const assuranceIncomplete = assurance.status === "incomplete";
389
- const failedUnits = /* @__PURE__ */ new Map();
390
- for (const pass of assurance.failedPasses) {
391
- const unitId = pass.workId.slice(0, 8);
392
- if (!failedUnits.has(unitId)) failedUnits.set(unitId, FailedReviewUnit.make({
393
- unitId,
394
- errorTag: `${pass.stage}:${pass.errorTag}`
395
- }));
396
- }
397
- return ReviewCoverage.make({
398
- status: inputCoverage.status === "complete" && !assuranceIncomplete ? "complete" : "incomplete",
399
- requiredPaths: inputCoverage.requiredPaths,
400
- reviewedPaths: inputCoverage.assignedPaths,
401
- unreviewedPaths: sortedUnique([...inputCoverage.partialPaths, ...inputCoverage.unassignedPaths]),
402
- failedUnits: [...failedUnits.values()].slice(0, 8),
403
- reasons: [...inputCoverage.reasons, ...assuranceIncomplete ? assurance.reasons : []]
404
- });
405
- };
406
- /** Assess one settled run without trusting coordinator prose or findings. */
407
- const assessReviewPipeline = (input) => {
408
- const trace = toolTrace(input.events);
409
- let inputCoverage = input.shape === "fan-out" ? fanOutInputCoverage(input.files, input.totalFiles) : flatInputCoverage(input.files, input.totalFiles, trace);
410
- if (input.anchorFiles.length < input.totalAnchorFiles) inputCoverage = ReviewInputCoverage.make({
411
- ...inputCoverage,
412
- status: "incomplete",
413
- reasons: [...inputCoverage.reasons, `full pull-request anchor surface exposed ${input.anchorFiles.length} of ${input.totalAnchorFiles} required files`]
414
- });
415
- const assessed = input.shape === "fan-out" ? fanOutAssurance(input.files, input.totalFiles, input.anchorFiles, trace) : flatAssurance();
416
- return {
417
- inputCoverage,
418
- assurance: assessed.assurance,
419
- coverage: compatibilityCoverage(inputCoverage, assessed.assurance),
420
- confirmedFindings: assessed.confirmedFindings,
421
- confirmedConcerns: assessed.confirmedConcerns,
422
- walkthrough: assessed.walkthrough
423
- };
424
- };
425
- /** Compatibility helper; prefer assessReviewPipeline for precise claims. */
426
- const assessReviewCoverage = (input) => assessReviewPipeline(input).coverage;
427
- /** Host-verified summaries from successful general discovery passes only. */
428
- const collectUnitFileSummaries = (events) => {
429
- const trace = toolTrace(events);
430
- return delegationDeclarations(trace).flatMap(({ id, request }) => {
431
- if (request.phase !== "discovery" || request.perspective !== "general") return [];
432
- const success = trace.succeeded.get(id);
433
- if (success === void 0 || trace.failed.has(id)) return [];
434
- const result = Schema.decodeUnknownOption(FileReviewUnitResult)(success.result);
435
- if (Option.isNone(result) || result.value.phase !== "discovery" || result.value.workId !== request.workId || result.value.unitId !== request.unitId) return [];
436
- const assigned = new Set(request.paths);
437
- return result.value.fileSummaries.filter((entry) => assigned.has(entry.path));
438
- });
439
- };
440
- //#endregion
441
8
  //#region src/internal/effort.ts
442
9
  /**
443
10
  * Names accepted on user-facing surfaces (the action input, the CLI flag),
@@ -566,7 +133,7 @@ const severityEmoji = {
566
133
  important: "⚠️",
567
134
  nit: "💅"
568
135
  };
569
- const severityRank$1 = {
136
+ const severityRank = {
570
137
  blocking: 0,
571
138
  important: 1,
572
139
  nit: 2
@@ -666,9 +233,9 @@ const severityCounts = (review, carriedFindings = [], carriedConcerns = []) => {
666
233
  */
667
234
  const renderVerdictCallout = (review, options) => {
668
235
  const counts = severityCounts(review, options.carriedFindings, options.carriedConcerns);
669
- if (options.inputCoverage?.status === "incomplete" || options.inputCoverage === void 0 && options.coverage?.status === "incomplete") return `> [!CAUTION]\n> Input coverage is incomplete — the check must not pass.${counts.blocking > 0 ? ` It also has ${countNoun(counts.blocking, "blocking finding")}.` : ""}`;
670
- if (options.assurance !== void 0 && options.assurance.status !== "settled") return `> [!CAUTION]\n> Configured review assurance did not settle — the check must not pass.${counts.blocking > 0 ? ` It also has ${countNoun(counts.blocking, "blocking finding")}.` : ""}`;
671
236
  if (counts.blocking > 0) return `> [!CAUTION]\n> ${countNoun(counts.blocking, "blocking finding")} — do not merge before addressing ${counts.blocking === 1 ? "it" : "them"}.`;
237
+ const carried = options.unreviewedPaths?.length ?? 0;
238
+ if (options.inputCoverage?.status === "incomplete" || options.assurance?.status === "incomplete") return `> [!WARNING]\n> Review infrastructure did not settle — a reviewer-side gap, NOT a request to change code.${carried > 0 ? ` ${countNoun(carried, "affected path")} ${carried === 1 ? "is" : "are"} carried forward and retried automatically on the next run.` : ""} The check reports "incomplete" until a run settles.`;
672
239
  if (counts.important > 0) return `> [!IMPORTANT]\n> ${countNoun(counts.important, "important finding")} to address before merging.`;
673
240
  if (counts.total > 0) return "> ℹ️ Minor suggestions only — mergeable as-is.";
674
241
  return review.verdict === "approve" ? "> ✅ No issues found." : "> ℹ️ No findings — see the summary.";
@@ -785,14 +352,11 @@ const planPublication = (review, files, options) => {
785
352
  });
786
353
  }
787
354
  const walkthrough = planWalkthrough(review.walkthrough, files);
788
- const sortedConcerns = [...review.concerns ?? []].sort((a, b) => severityRank$1[a.severity] - severityRank$1[b.severity]);
789
- const sortedDemoted = [...demoted].sort((a, b) => severityRank$1[a.finding.severity] - severityRank$1[b.finding.severity]);
355
+ const sortedConcerns = [...review.concerns ?? []].sort((a, b) => severityRank[a.severity] - severityRank[b.severity]);
356
+ const sortedDemoted = [...demoted].sort((a, b) => severityRank[a.finding.severity] - severityRank[b.finding.severity]);
790
357
  const footerParts = ["Automated review by @effect-agent/pr-review"];
791
358
  if (options.modelLabel !== void 0) footerParts.push(options.modelLabel);
792
- if (options.usage !== void 0 && options.usageScope !== void 0) {
793
- const scope = options.usageScope === "coordinator" ? " (coordinator)" : "";
794
- footerParts.push(`${options.usage.inputTokens} in / ${options.usage.outputTokens} out tokens${scope}`);
795
- }
359
+ if (options.usage !== void 0) footerParts.push(`${options.usage.inputTokens} in / ${options.usage.outputTokens} out tokens`);
796
360
  if (options.runUrl !== void 0) footerParts.push(`[run](${options.runUrl})`);
797
361
  footerParts.push(`reviewed at ${options.headSha.slice(0, 7)}`);
798
362
  const footer = `_${footerParts.join(" · ")}._`;
@@ -810,20 +374,19 @@ const planPublication = (review, files, options) => {
810
374
  const parts = [renderVerdictCallout(review, {
811
375
  carriedFindings,
812
376
  carriedConcerns,
813
- coverage: options.coverage,
814
377
  inputCoverage: options.inputCoverage,
815
- assurance: options.assurance
378
+ assurance: options.assurance,
379
+ unreviewedPaths: options.unreviewedPaths
816
380
  })];
817
381
  if (options.reviewMode !== void 0 && options.reviewReason !== void 0) parts.push("", options.reviewMode === "incremental" ? `**Incremental scope:** reopened ${options.reviewFilesVisible ?? files.length} affected file(s) ${options.reviewReason}. Unchanged settled scope was preserved and not reopened.` : `**Full-diff scope:** ${options.reviewReason}.`);
818
382
  if (options.stateNotice !== void 0) parts.push("", `⚠️ Continuity state was not stored (${options.stateNotice.slice(0, 1e3)}); the next run will safely review the full diff.`);
819
383
  parts.push("", renderReviewStats(files, options.totalChangedFiles, counts));
820
- if (options.inputCoverage !== void 0 && options.assurance !== void 0) parts.push("", `**Input coverage:** ${options.inputCoverage.status} (${options.inputCoverage.assignedPaths.length}/${options.inputCoverage.requiredPaths.length} paths assigned, ${options.inputCoverage.partialPaths.length} partial) · **Review assurance:** ${options.assurance.status} (${options.assurance.completedGeneralDiscoveryPasses}/${options.assurance.requiredGeneralDiscoveryPasses} general discovery, ${options.assurance.completedSpecialistPasses}/${options.assurance.requiredSpecialistPasses} specialist, ${options.assurance.completedVerificationPasses}/${options.assurance.requiredVerificationPasses} verification; ${options.assurance.confirmedCandidates} confirmed / ${options.assurance.rejectedCandidates} rejected / ${options.assurance.unsettledCandidates} unsettled candidates)`);
384
+ if (options.inputCoverage !== void 0 && options.assurance !== void 0) parts.push("", `**Input coverage:** ${options.inputCoverage.status} (${options.inputCoverage.assignedPaths.length}/${options.inputCoverage.requiredPaths.length} paths assigned, ${options.inputCoverage.partialPaths.length} partial) · **Review assurance:** ${options.assurance.status} (${options.assurance.completedGeneralDiscoveryPasses}/${options.assurance.requiredGeneralDiscoveryPasses} general discovery, ${options.assurance.completedSpecialistPasses}/${options.assurance.requiredSpecialistPasses} specialist, ${options.assurance.completedVerificationPasses}/${options.assurance.requiredVerificationPasses} verification; ${options.assurance.confirmedCandidates} confirmed / ${options.assurance.rejectedCandidates} rejected / ${options.assurance.unsettledCandidates} unsettled${options.assurance.discardedInvalidFindings > 0 ? ` / ${options.assurance.discardedInvalidFindings} discarded` : ""} candidates)`);
821
385
  parts.push("", review.summary);
822
386
  if (walkthroughKept && walkthrough.length > 0) parts.push("", renderWalkthrough(walkthrough));
823
387
  else if (walkthrough.length > 0) parts.push("", "⚠️ Walkthrough omitted — the body exceeded GitHub's review size cap.");
824
- if (options.inputCoverage?.status === "incomplete") parts.push("", "### 🛑 Incomplete input coverage", "", ...options.inputCoverage.reasons.map((reason) => `- ${reason}`));
825
- else if (options.inputCoverage === void 0 && options.coverage?.status === "incomplete") parts.push("", "### 🛑 Incomplete coverage", "", ...options.coverage.reasons.map((reason) => `- ${reason}`));
826
- if (options.assurance !== void 0 && options.assurance.status !== "settled") parts.push("", "### 🛑 Incomplete review assurance", "", ...options.assurance.reasons.map((reason) => `- ${reason}`));
388
+ if (options.inputCoverage?.status === "incomplete") parts.push("", "### ⚠️ Incomplete input coverage", "", ...options.inputCoverage.reasons.map((reason) => `- ${reason}`));
389
+ if (options.assurance?.status === "incomplete") parts.push("", "### ⚠️ Unsettled review passes", "", "The passes below failed on the reviewer's side after a bounded retry. Their paths are carried forward and re-reviewed automatically on the next run — do not change code to satisfy this section.", "", ...options.assurance.reasons.map((reason) => `- ${reason}`));
827
390
  if (carriedFindings.length > 0) parts.push("", "<details>", `<summary>Unresolved findings carried from unchanged scope (${carriedFindings.length})</summary>`, "", ...carriedFindings.map(renderCarriedFinding), "", "</details>");
828
391
  if (carriedConcerns.length > 0) {
829
392
  parts.push("", "### Unresolved concerns carried to the final audit");
@@ -838,7 +401,8 @@ const planPublication = (review, files, options) => {
838
401
  return parts.join("\n");
839
402
  };
840
403
  const counts = severityCounts(review, options.carriedFindings ?? [], options.carriedConcerns ?? []);
841
- const event = !options.applyVerdict ? "COMMENT" : options.inputCoverage?.status === "incomplete" || options.inputCoverage === void 0 && options.coverage?.status === "incomplete" || options.assurance !== void 0 && options.assurance.status !== "settled" || counts.blocking > 0 ? "REQUEST_CHANGES" : review.verdict === "approve" && counts.important === 0 ? "APPROVE" : "COMMENT";
404
+ const unclean = options.inputCoverage?.status === "incomplete" || options.assurance?.status === "incomplete";
405
+ const event = !options.applyVerdict ? "COMMENT" : counts.blocking > 0 ? "REQUEST_CHANGES" : review.verdict === "approve" && counts.important === 0 && !unclean ? "APPROVE" : "COMMENT";
842
406
  const tail = [
843
407
  renderReviewMetadata({
844
408
  headSha: options.headSha,
@@ -895,12 +459,9 @@ const reviewBudgetLimits = UsageBudgetLimits.make({
895
459
  maxDurationMillis: 48e4
896
460
  });
897
461
  /**
898
- * Run-level bounds for the fan-out coordinator. This budget observes only
899
- * the COORDINATOR'S own usage delegated children are bounded separately by
900
- * the delegation's `SubagentPolicy` reservation and the child definition's
901
- * own `AgentPolicy`, never silently by the parent's budget. The duration
902
- * ceiling is wider because delegation Tool Calls hold the parent turn open
903
- * while bounded children run.
462
+ * Run-level bounds for the fan-out pipeline. One budget observes EVERY child
463
+ * pass, so the ceiling covers bounded parallel discovery and verification
464
+ * plus the one-retry allowance.
904
465
  */
905
466
  const fanOutReviewBudgetLimits = UsageBudgetLimits.make({
906
467
  maxInputTokens: 6e5,
@@ -920,23 +481,16 @@ var ReviewRunOutcome = class extends Schema.Class("@effect-agent/pr-review/Revie
920
481
  coverage: ReviewCoverage,
921
482
  /** Exact path/evidence assignment, distinct from semantic review work. */
922
483
  inputCoverage: ReviewInputCoverage,
923
- /** Settlement of configured discovery, specialist, and verification work. */
484
+ /** Settlement of scheduled discovery, specialist, and verification work. */
924
485
  assurance: ReviewAssurance,
486
+ /** Retryable scope this run could not settle; carried to the next run. */
487
+ unreviewedPaths: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(512))).check(Schema.isMaxLength(300)),
925
488
  plan: ReviewPublicationPlan,
926
489
  published: Schema.optionalKey(PublishedReview),
927
- turns: Schema.Int.check(Schema.isGreaterThan(0)),
928
- /**
929
- * The run budget's observed usage. For the fan-out reviewer this observes
930
- * the COORDINATOR only — delegated children are bounded and accounted
931
- * separately by their reservations.
932
- */
490
+ /** Total settled model turns (all child passes for the fan-out pipeline). */
491
+ turns: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
492
+ /** The run budget's observed usage across the whole run. */
933
493
  usage: Schema.optionalKey(UsageTotals),
934
- /**
935
- * What `usage` observed: the whole run, or a fan-out coordinator only.
936
- * Absent when the caller declared no scope — consumers must not present
937
- * unscoped usage as whole-run totals.
938
- */
939
- usageScope: Schema.optionalKey(Schema.Literals(["run", "coordinator"])),
940
494
  reviewMode: Schema.optionalKey(Schema.Literals(["incremental", "full"])),
941
495
  reviewReason: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(1e3))),
942
496
  state: Schema.optionalKey(ReviewState)
@@ -960,63 +514,20 @@ const enforceFindingsBound = (review, maxFindings) => review.findings.length <=
960
514
  ...review.walkthrough !== void 0 ? { walkthrough: review.walkthrough } : {}
961
515
  });
962
516
  const findingKey = (finding) => `${finding.path}\u0000${finding.startLine}\u0000${finding.endLine}\u0000${finding.severity}\u0000${finding.title}`;
963
- const severityRank = {
964
- blocking: 0,
965
- important: 1,
966
- nit: 2
967
- };
968
- const rankAndDedupeConcerns = (concerns) => {
969
- const byContent = /* @__PURE__ */ new Map();
970
- for (const concern of concerns) {
971
- const key = `${concern.title}\u0000${concern.body}`;
972
- const previous = byContent.get(key);
973
- if (previous === void 0 || severityRank[concern.severity] < severityRank[previous.severity]) byContent.set(key, concern);
974
- }
975
- return [...byContent.values()].sort((left, right) => severityRank[left.severity] - severityRank[right.severity]).slice(0, 10);
976
- };
977
517
  /**
978
- * Execute one review with any explicit Agent Binding whose contract is
979
- * `ReviewMission -> CodeReview` the flat reviewer or the fan-out
980
- * coordinator; the toolkit stays generic because publication only depends on
981
- * the shared output contract. The binding stays a parameter (D-027): tests
982
- * pass scripted models, hosts pass live provider bindings, and the model
983
- * Layer's requirements stay visible in this Effect's `R`.
518
+ * The shared settlement tail: carry unchanged prior scope, decide whether
519
+ * this run's continuity state can be signed, plan the exact publication, and
520
+ * (optionally) post it. Continuity requires only that the run COMPLETED with
521
+ * a trustworthy full-surface fingerprint never that every pass settled;
522
+ * unsettled scope travels inside the state instead of freezing it.
984
523
  */
985
- const executeReview = (binding, options) => Effect.gen(function* () {
986
- const source = yield* PullRequestSource;
987
- const metadata = yield* source.metadata;
988
- const files = yield* source.changedFiles;
989
- const anchorFiles = yield* source.anchorFiles;
524
+ const settleReviewRun = (core, context, options) => Effect.gen(function* () {
525
+ const { metadata, files, anchorFiles, fingerprint, usage } = context;
990
526
  const executionContext = Option.getOrUndefined(yield* Effect.serviceOption(ReviewExecutionContext));
991
- const mission = buildReviewMission(metadata, files);
992
- const fullMission = buildReviewMission(metadata, anchorFiles);
993
- const fingerprint = options.signature === void 0 ? void 0 : yield* computeChangesetFingerprint(anchorFiles, options.signature(fullMission));
994
- const budget = yield* makeUsageBudget(options.limits ?? reviewBudgetLimits);
995
- const detached = yield* AgentRuntime.start(binding, mission, {
996
- budget: toRunBudgetHook(budget),
997
- estimateCostMicrousd: () => Effect.succeed(500)
998
- });
999
- const result = yield* detached.await;
1000
- const events = yield* detached.events;
1001
- const decoded = yield* Schema.decodeUnknownEffect(CodeReview)(result.output);
527
+ const review = enforceFindingsBound(core.review, clampMaxFindings(options.maxFindings));
528
+ const { inputCoverage, assurance } = core;
529
+ const unreviewedPaths = [...new Set(core.unreviewedPaths)].sort();
1002
530
  const reviewTotalFiles = executionContext?.totalFiles ?? metadata.totalChangedFiles;
1003
- const pipeline = assessReviewPipeline({
1004
- shape: options.reviewShape ?? "flat",
1005
- files,
1006
- totalFiles: reviewTotalFiles,
1007
- anchorFiles,
1008
- totalAnchorFiles: metadata.totalChangedFiles,
1009
- events
1010
- });
1011
- const verifiedReview = options.reviewShape !== "fan-out" ? decoded : CodeReview.make({
1012
- summary: decoded.summary,
1013
- verdict: decoded.verdict,
1014
- findings: rankAndDedupeFindings(pipeline.confirmedFindings),
1015
- ...pipeline.confirmedConcerns.length === 0 ? {} : { concerns: rankAndDedupeConcerns(pipeline.confirmedConcerns) },
1016
- ...pipeline.walkthrough.length === 0 ? {} : { walkthrough: pipeline.walkthrough }
1017
- });
1018
- const review = enforceFindingsBound(verifiedReview, clampMaxFindings(options.maxFindings));
1019
- const usage = yield* budget.snapshot;
1020
531
  const affectedPaths = new Set(executionContext?.affectedPaths ?? files.flatMap((file) => file.previousPath === void 0 ? [file.path] : [file.path, file.previousPath]));
1021
532
  const priorState = executionContext?.mode === "incremental" ? executionContext.priorState : void 0;
1022
533
  const carriedCandidates = priorState?.unresolvedFindings.filter((finding) => !affectedPaths.has(finding.path)).map(fromStoredFinding) ?? [];
@@ -1032,9 +543,11 @@ const executeReview = (binding, options) => Effect.gen(function* () {
1032
543
  const key = `${concern.title}\u0000${concern.body}`;
1033
544
  return activeConcernKeys.has(key) && !currentConcernKeys.has(key);
1034
545
  });
1035
- const { assurance, coverage, inputCoverage } = pipeline;
1036
- const stateCandidate = executionContext !== void 0 && inputCoverage.status === "complete" && assurance.status === "settled" && fingerprint !== void 0 && metadata.baseSha !== void 0 && executionContext.stateAuthenticator?.status === "available" ? ReviewState.make({
1037
- version: 1,
546
+ const settled = inputCoverage.status === "complete" && assurance.status !== "incomplete" && unreviewedPaths.length === 0;
547
+ const skipFingerprint = settled ? fingerprint : void 0;
548
+ const carriedScopeFits = unreviewedPaths.length <= 100;
549
+ const stateCandidate = executionContext !== void 0 && fingerprint !== void 0 && metadata.baseSha !== void 0 && anchorFiles.length >= metadata.totalChangedFiles && carriedScopeFits && executionContext.stateAuthenticator?.status === "available" ? ReviewState.make({
550
+ version: 2,
1038
551
  repository: metadata.repository,
1039
552
  pullRequestNumber: metadata.number,
1040
553
  baseRef: metadata.baseRef,
@@ -1046,12 +559,14 @@ const executeReview = (binding, options) => Effect.gen(function* () {
1046
559
  reviewedPathCount: anchorFiles.length,
1047
560
  unresolvedFindings: activeFindings.map(toStoredFinding),
1048
561
  unresolvedConcerns: activeConcerns.map(toStoredConcern),
562
+ unreviewedPaths,
563
+ settled,
1049
564
  lastReviewMode: executionContext.mode
1050
565
  }) : void 0;
1051
566
  const continuity = stateCandidate === void 0 || executionContext?.stateAuthenticator === void 0 ? {
1052
567
  state: void 0,
1053
568
  marker: void 0,
1054
- notice: executionContext?.stateAuthenticator?.status === "unavailable" && inputCoverage.status === "complete" && assurance.status === "settled" ? executionContext.stateAuthenticator.unavailableReason ?? "authenticated continuity state is unavailable" : void 0
569
+ notice: executionContext !== void 0 && !carriedScopeFits ? `carried unreviewed scope (${unreviewedPaths.length} paths) exceeded the 100-path continuity bound` : executionContext?.stateAuthenticator?.status === "unavailable" ? executionContext.stateAuthenticator.unavailableReason ?? "authenticated continuity state is unavailable" : void 0
1055
570
  } : yield* executionContext.stateAuthenticator.render(stateCandidate).pipe(Effect.match({
1056
571
  onFailure: (error) => ({
1057
572
  state: void 0,
@@ -1073,11 +588,10 @@ const executeReview = (binding, options) => Effect.gen(function* () {
1073
588
  modelLabel: options.modelLabel,
1074
589
  runUrl: options.runUrl,
1075
590
  usage,
1076
- usageScope: options.usageScope,
1077
- fingerprint: inputCoverage.status === "complete" && assurance.status === "settled" ? fingerprint : void 0,
1078
- coverage,
591
+ fingerprint: skipFingerprint,
1079
592
  inputCoverage,
1080
593
  assurance,
594
+ unreviewedPaths,
1081
595
  carriedFindings,
1082
596
  carriedConcerns,
1083
597
  reviewMode: executionContext?.mode,
@@ -1088,43 +602,119 @@ const executeReview = (binding, options) => Effect.gen(function* () {
1088
602
  stateMarker: continuity.marker,
1089
603
  stateNotice: continuity.notice
1090
604
  });
1091
- const scope = options.usageScope === void 0 ? {} : { usageScope: options.usageScope };
1092
- if (!options.post) return ReviewRunOutcome.make({
605
+ const shared = {
1093
606
  review,
1094
607
  activeFindings,
1095
608
  activeConcerns,
1096
- coverage,
609
+ coverage: compatibilityCoverage(inputCoverage, assurance),
1097
610
  inputCoverage,
1098
611
  assurance,
612
+ unreviewedPaths,
1099
613
  plan,
1100
- turns: result.turns,
1101
- usage,
1102
- ...scope,
614
+ turns: core.turns,
615
+ ...usage === void 0 ? {} : { usage },
1103
616
  ...executionContext === void 0 ? {} : {
1104
617
  reviewMode: executionContext.mode,
1105
618
  reviewReason: executionContext.reason
1106
619
  },
1107
620
  ...continuity.state === void 0 ? {} : { state: continuity.state }
1108
- });
621
+ };
622
+ if (!options.post) return ReviewRunOutcome.make(shared);
1109
623
  const published = yield* (yield* ReviewPublisher).publish(plan);
1110
624
  return ReviewRunOutcome.make({
625
+ ...shared,
626
+ published
627
+ });
628
+ });
629
+ /**
630
+ * Execute one flat review with any explicit Agent Binding whose contract is
631
+ * `ReviewMission -> CodeReview`. The binding stays a parameter (D-027): tests
632
+ * pass scripted models, hosts pass live provider bindings, and the model
633
+ * Layer's requirements stay visible in this Effect's `R`.
634
+ */
635
+ const executeReview = (binding, options) => Effect.gen(function* () {
636
+ const source = yield* PullRequestSource;
637
+ const metadata = yield* source.metadata;
638
+ const files = yield* source.changedFiles;
639
+ const anchorFiles = yield* source.anchorFiles;
640
+ const executionContext = Option.getOrUndefined(yield* Effect.serviceOption(ReviewExecutionContext));
641
+ const mission = buildReviewMission(metadata, files);
642
+ const fullMission = buildReviewMission(metadata, anchorFiles);
643
+ const fingerprint = options.signature === void 0 ? void 0 : yield* computeChangesetFingerprint(anchorFiles, options.signature(fullMission));
644
+ const budget = yield* makeUsageBudget(options.limits ?? reviewBudgetLimits);
645
+ const detached = yield* AgentRuntime.start(binding, mission, {
646
+ budget: toRunBudgetHook(budget),
647
+ estimateCostMicrousd: () => Effect.succeed(500)
648
+ });
649
+ const result = yield* detached.await;
650
+ const events = yield* detached.events;
651
+ const review = yield* Schema.decodeUnknownEffect(CodeReview)(result.output);
652
+ const assessment = assessFlatReview({
653
+ files,
654
+ totalFiles: executionContext?.totalFiles ?? metadata.totalChangedFiles,
655
+ anchorFiles,
656
+ totalAnchorFiles: metadata.totalChangedFiles,
657
+ events
658
+ });
659
+ const usage = yield* budget.snapshot;
660
+ return yield* settleReviewRun({
1111
661
  review,
1112
- activeFindings,
1113
- activeConcerns,
1114
- coverage,
1115
- inputCoverage,
1116
- assurance,
1117
- plan,
1118
- published,
1119
- turns: result.turns,
1120
- usage,
1121
- ...scope,
1122
- ...executionContext === void 0 ? {} : {
1123
- reviewMode: executionContext.mode,
1124
- reviewReason: executionContext.reason
1125
- },
1126
- ...continuity.state === void 0 ? {} : { state: continuity.state }
662
+ inputCoverage: assessment.inputCoverage,
663
+ assurance: assessment.assurance,
664
+ unreviewedPaths: assessment.unreviewedPaths,
665
+ turns: result.turns
666
+ }, {
667
+ metadata,
668
+ files,
669
+ anchorFiles,
670
+ fingerprint,
671
+ usage
672
+ }, options);
673
+ });
674
+ /**
675
+ * Execute one host-scheduled fan-out review: deterministic planning,
676
+ * independent discovery and verification child passes with bounded retries,
677
+ * and a host-composed review from verifier-confirmed candidates only. One
678
+ * budget observes every child pass, so the reported usage is whole-run.
679
+ */
680
+ const executeFanOutReview = (binding, options) => Effect.gen(function* () {
681
+ const source = yield* PullRequestSource;
682
+ const metadata = yield* source.metadata;
683
+ const files = yield* source.changedFiles;
684
+ const anchorFiles = yield* source.anchorFiles;
685
+ const executionContext = Option.getOrUndefined(yield* Effect.serviceOption(ReviewExecutionContext));
686
+ const fullMission = buildReviewMission(metadata, anchorFiles);
687
+ const fingerprint = options.signature === void 0 ? void 0 : yield* computeChangesetFingerprint(anchorFiles, options.signature(fullMission));
688
+ const budget = yield* makeUsageBudget(options.limits ?? fanOutReviewBudgetLimits);
689
+ const totalFiles = executionContext?.totalFiles ?? metadata.totalChangedFiles;
690
+ const pipeline = yield* runFanOutReview(binding, {
691
+ files,
692
+ anchorFiles,
693
+ totalChangedFiles: totalFiles,
694
+ maxFindings: options.maxFindings,
695
+ budget: toRunBudgetHook(budget)
1127
696
  });
697
+ const inputCoverage = fanOutInputCoverage({
698
+ plan: pipeline.plan,
699
+ files,
700
+ totalFiles,
701
+ anchorFiles,
702
+ totalAnchorFiles: metadata.totalChangedFiles
703
+ });
704
+ const usage = yield* budget.snapshot;
705
+ return yield* settleReviewRun({
706
+ review: pipeline.review,
707
+ inputCoverage,
708
+ assurance: pipeline.assurance,
709
+ unreviewedPaths: pipeline.unreviewedPaths,
710
+ turns: pipeline.turns
711
+ }, {
712
+ metadata,
713
+ files,
714
+ anchorFiles,
715
+ fingerprint,
716
+ usage
717
+ }, options);
1128
718
  });
1129
719
  //#endregion
1130
720
  //#region src/internal/factory.ts
@@ -1211,9 +801,7 @@ const make = (options) => {
1211
801
  maxFindings: clampMaxFindings(options.maxFindings),
1212
802
  signature,
1213
803
  modelLabel: options.modelLabel,
1214
- runUrl: runOptions.runUrl,
1215
- usageScope: "run",
1216
- reviewShape: "flat"
804
+ runUrl: runOptions.runUrl
1217
805
  }).pipe(Effect.provide(Layer.mergeAll(ReviewToolkitLayer, IdGenerator.layer)), Effect.scoped), options.ignore);
1218
806
  return {
1219
807
  definition,
@@ -1229,56 +817,47 @@ const make = (options) => {
1229
817
  };
1230
818
  };
1231
819
  /**
1232
- * Build the fan-out reviewer: a coordinator schedules host-planned general
1233
- * and specialist discovery plus independent candidate verification through
1234
- * attached ephemeral children. Host code reconstructs publication only from
1235
- * exactly confirmed candidates. Child and coordinator execution bounds are
1236
- * packaged and not configurable here the delegation reservation mirrors
1237
- * the child policy, and letting the two drift apart is a published-API hazard.
820
+ * Build the fan-out reviewer: host code schedules host-planned general and
821
+ * specialist discovery plus independent candidate verification directly as
822
+ * bounded child runs there is no coordinator model and no delegation tool,
823
+ * so review assurance cannot fail on scheduling compliance. Host code
824
+ * composes publication only from exactly confirmed candidates. Child
825
+ * execution bounds are packaged and not configurable here.
1238
826
  */
1239
827
  const makeFanOut = (options) => {
1240
- const suite = makeFanOutReviewSuite({
1241
- guidance: options.guidance,
1242
- maxFindings: options.maxFindings
1243
- });
1244
- const binding = Object.freeze({
1245
- definition: suite.parent,
1246
- model: options.model
1247
- });
828
+ const child = makeFileReviewerDefinition({ guidance: options.guidance });
1248
829
  const childBinding = Object.freeze({
1249
- definition: suite.child,
830
+ definition: child,
1250
831
  model: options.model
1251
832
  });
1252
833
  const guidanceLines = options.guidance === void 0 ? [] : typeof options.guidance === "string" ? [options.guidance] : options.guidance;
1253
834
  const signature = (mission) => [
1254
- suite.parent.instructions(mission),
835
+ "pr-review-fan-out-host-scheduled-v1",
836
+ JSON.stringify(Schema.encodeSync(ReviewMission)(mission)),
1255
837
  `childGuidance=${JSON.stringify(guidanceLines)}`,
838
+ `maxFindings=${clampMaxFindings(options.maxFindings)}`,
1256
839
  `applyVerdict=${String(options.applyVerdict ?? false)}`,
1257
840
  ...options.modelLabel === void 0 ? [] : [`model=${options.modelLabel}`]
1258
841
  ].join(" ");
1259
842
  const profileSignature = (_mission) => [
1260
- "pr-review-profile-v3-sharded-request-bound-assurance",
843
+ "pr-review-profile-v4-host-scheduled",
1261
844
  JSON.stringify(guidanceLines),
1262
845
  JSON.stringify(options.ignore ?? []),
1263
846
  `maxFindings=${clampMaxFindings(options.maxFindings)}`,
1264
847
  `applyVerdict=${String(options.applyVerdict ?? false)}`,
1265
848
  ...options.modelLabel === void 0 ? [] : [`model=${options.modelLabel}`]
1266
- ].join("\0");
1267
- const delegationLayer = fanOutHandlersLayerFor(suite.delegation)(childBinding).pipe(Layer.provide(Layer.mergeAll(FileReviewToolkitLayer, SubagentReservationsMemoryLive, IdGenerator.layer)));
1268
- const run = (runOptions = {}) => provideIgnore(executeReview(binding, {
849
+ ].join(" ");
850
+ const run = (runOptions = {}) => provideIgnore(executeFanOutReview(childBinding, {
1269
851
  post: runOptions.post ?? false,
1270
852
  applyVerdict: options.applyVerdict ?? false,
1271
853
  limits: options.budget ?? fanOutReviewBudgetLimits,
1272
854
  maxFindings: clampMaxFindings(options.maxFindings),
1273
855
  signature,
1274
856
  modelLabel: options.modelLabel,
1275
- runUrl: runOptions.runUrl,
1276
- usageScope: "coordinator",
1277
- reviewShape: "fan-out"
1278
- }).pipe(Effect.provide(Layer.mergeAll(FanOutCoordinatorToolkitLayer, delegationLayer, IdGenerator.layer)), Effect.scoped), options.ignore);
857
+ runUrl: runOptions.runUrl
858
+ }).pipe(Effect.provide(IdGenerator.layer), Effect.scoped), options.ignore);
1279
859
  return {
1280
- definition: suite.parent,
1281
- binding,
860
+ definition: child,
1282
861
  childBinding,
1283
862
  run,
1284
863
  fingerprint: makeFingerprint(signature, options.ignore),
@@ -1620,6 +1199,6 @@ const openAiClientLayer = OpenAiClient.layerConfig({ apiKey: Config.redacted(PRO
1620
1199
  /** The Anthropic client Layer, credential from `ANTHROPIC_API_KEY`. */
1621
1200
  const anthropicClientLayer = AnthropicClient.layerConfig({ apiKey: Config.redacted(PROVIDER_CREDENTIAL_ENV.anthropic) }).pipe(Layer.provide(FetchHttpClient.layer));
1622
1201
  //#endregion
1623
- export { collectUnitFileSummaries as $, AGENT_PROMPT_PREAMBLE as A, EFFORT_ALIASES as B, PrReview as C, executeReview as D, enforceFindingsBound as E, planPublication as F, FailedReviewPass as G, isEffortPosition as H, planWalkthrough as I, ReviewCoverage as J, FailedReviewUnit as K, renderAgentPrompt as L, ReviewEvent as M, ReviewPublicationPlan as N, fanOutReviewBudgetLimits as O, estimateReviewEffort as P, assessReviewPipeline as Q, compileIgnoreGlobs as R, renderProgressSettleBody as S, buildReviewMission as T, parseEffortPosition as U, InvalidEffortInput as V, resolveEffortRung as W, ReviewShape as X, ReviewInputCoverage as Y, assessReviewCoverage as Z, gitHubReviewProgressLayer as _, anthropicClientLayer as a, renderProgressBeginBody as b, makeOpenAiReviewModel as c, ReviewTargetUnresolved as d, gitHubReviewLayers as f, ReviewProgressReporter as g, PROGRESS_COMMENT_MARKER_PREFIX as h, PROVIDER_EFFORT_RUNGS as i, ReviewCommentDraft as j, reviewBudgetLimits as k, openAiClientLayer as l, resolveReviewTarget as m, DEFAULT_PROVIDER as n, describeReviewModel as o, readGitHubEvent as p, ReviewAssurance as q, PROVIDER_CREDENTIAL_ENV as r, makeAnthropicReviewModel as s, DEFAULT_MODEL as t, GitHubEventWire as u, noopReviewProgressReporterLayer as v, ReviewRunOutcome as w, renderProgressClaimMarker as x, parseProgressClaim as y, ignoringPullRequestSourceLayer as z };
1202
+ export { reviewBudgetLimits as A, ignoringPullRequestSourceLayer as B, PrReview as C, executeFanOutReview as D, enforceFindingsBound as E, estimateReviewEffort as F, resolveEffortRung as G, InvalidEffortInput as H, planPublication as I, planWalkthrough as L, ReviewCommentDraft as M, ReviewEvent as N, executeReview as O, ReviewPublicationPlan as P, renderAgentPrompt as R, renderProgressSettleBody as S, buildReviewMission as T, isEffortPosition as U, EFFORT_ALIASES as V, parseEffortPosition as W, gitHubReviewProgressLayer as _, anthropicClientLayer as a, renderProgressBeginBody as b, makeOpenAiReviewModel as c, ReviewTargetUnresolved as d, gitHubReviewLayers as f, ReviewProgressReporter as g, PROGRESS_COMMENT_MARKER_PREFIX as h, PROVIDER_EFFORT_RUNGS as i, AGENT_PROMPT_PREAMBLE as j, fanOutReviewBudgetLimits as k, openAiClientLayer as l, resolveReviewTarget as m, DEFAULT_PROVIDER as n, describeReviewModel as o, readGitHubEvent as p, PROVIDER_CREDENTIAL_ENV as r, makeAnthropicReviewModel as s, DEFAULT_MODEL as t, GitHubEventWire as u, noopReviewProgressReporterLayer as v, ReviewRunOutcome as w, renderProgressClaimMarker as x, parseProgressClaim as y, compileIgnoreGlobs as z };
1624
1203
 
1625
- //# sourceMappingURL=providers-BguZK4B_.mjs.map
1204
+ //# sourceMappingURL=providers-NyP-4rS6.mjs.map