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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +83 -195
  2. package/dist/action.d.mts +27 -18
  3. package/dist/action.mjs +60 -39
  4. package/dist/action.mjs.map +1 -1
  5. package/dist/cli.mjs +3 -3
  6. package/dist/cli.mjs.map +1 -1
  7. package/dist/{fan-out-BJBTAYuh.d.mts → fan-out-CMEsbFLk.d.mts} +455 -177
  8. package/dist/{github-BbwYzNrC.mjs → github-NjgxGqwM.mjs} +2163 -1518
  9. package/dist/github-NjgxGqwM.mjs.map +1 -0
  10. package/dist/index.d.mts +30 -20
  11. package/dist/index.mjs +3 -3
  12. package/dist/{providers-NyP-4rS6.mjs → providers-CODZQCmL.mjs} +202 -80
  13. package/dist/providers-CODZQCmL.mjs.map +1 -0
  14. package/dist/testing.d.mts +3 -1
  15. package/dist/testing.mjs +3 -2
  16. package/dist/testing.mjs.map +1 -1
  17. package/package.json +2 -2
  18. package/src/action.ts +141 -78
  19. package/src/cli.ts +6 -1
  20. package/src/index.ts +1 -0
  21. package/src/internal/adjudication.ts +415 -0
  22. package/src/internal/coverage.ts +41 -60
  23. package/src/internal/factory.ts +4 -4
  24. package/src/internal/fan-out.ts +208 -14
  25. package/src/internal/fingerprint.ts +16 -10
  26. package/src/internal/fixtures.ts +6 -0
  27. package/src/internal/github-env.ts +9 -0
  28. package/src/internal/github.ts +243 -7
  29. package/src/internal/progress.ts +1 -1
  30. package/src/internal/render.ts +186 -42
  31. package/src/internal/retirement.ts +16 -17
  32. package/src/internal/review-agent.ts +39 -4
  33. package/src/internal/review-state.ts +315 -105
  34. package/src/internal/review-units.ts +10 -9
  35. package/src/internal/run.ts +197 -63
  36. package/dist/github-BbwYzNrC.mjs.map +0 -1
  37. package/dist/providers-NyP-4rS6.mjs.map +0 -1
@@ -1,4 +1,4 @@
1
- import { Effect, Option, Schema } from "effect";
1
+ import { Effect, Schema } from "effect";
2
2
  import {
3
3
  makeUsageBudget,
4
4
  toRunBudgetHook,
@@ -9,12 +9,16 @@ import {
9
9
  } from "effect-agent";
10
10
  import { type Tool } from "effect/unstable/ai";
11
11
 
12
+ import {
13
+ collectReviewAdjudications,
14
+ renderAdjudicationContextLine,
15
+ renderPriorFindingContextLine,
16
+ buildPriorReviewContext,
17
+ } from "./adjudication.ts";
12
18
  import {
13
19
  assessFlatReview,
14
- compatibilityCoverage,
15
20
  fanOutInputCoverage,
16
21
  ReviewAssurance,
17
- ReviewCoverage,
18
22
  ReviewInputCoverage,
19
23
  } from "./coverage.ts";
20
24
  import type { ChangedFile } from "./diff.ts";
@@ -30,15 +34,21 @@ import {
30
34
  ReviewMission,
31
35
  } from "./review-agent.ts";
32
36
  import {
37
+ adjudicationIdentity,
38
+ concernIdentity,
39
+ findingIdentity,
33
40
  fromStoredConcern,
34
41
  fromStoredFinding,
42
+ MAX_STORED_UNREVIEWED_PASSES,
35
43
  MAX_STORED_UNREVIEWED_PATHS,
36
44
  ReviewExecutionContext,
37
45
  ReviewState,
46
+ StoredAdjudication,
47
+ StoredUnreviewedPass,
38
48
  toStoredConcern,
39
49
  toStoredFinding,
40
50
  } from "./review-state.ts";
41
- import { rankAndDedupeConcerns, rankAndDedupeFindings } from "./review-units.ts";
51
+ import { rankAndDedupeConcerns, rankAndDedupeFindings, reviewConcernKey } from "./review-units.ts";
42
52
  import { PullRequestSource, type PullRequestMetadata } from "./source.ts";
43
53
 
44
54
  // ---------------------------------------------------------------------------
@@ -89,8 +99,6 @@ export class ReviewRunOutcome extends Schema.Class<ReviewRunOutcome>(
89
99
  activeFindings: Schema.Array(ReviewFinding).check(Schema.isMaxLength(20)),
90
100
  /** All currently unresolved concerns, including concerns carried to final audit. */
91
101
  activeConcerns: Schema.Array(ReviewConcern).check(Schema.isMaxLength(10)),
92
- /** Host-owned structural coverage used by the Actions check conclusion. */
93
- coverage: ReviewCoverage,
94
102
  /** Exact path/evidence assignment, distinct from semantic review work. */
95
103
  inputCoverage: ReviewInputCoverage,
96
104
  /** Settlement of scheduled discovery, specialist, and verification work. */
@@ -108,6 +116,8 @@ export class ReviewRunOutcome extends Schema.Class<ReviewRunOutcome>(
108
116
  reviewMode: Schema.optionalKey(Schema.Literals(["incremental", "full"])),
109
117
  reviewReason: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(1_000))),
110
118
  state: Schema.optionalKey(ReviewState),
119
+ /** Maintainer adjudications standing against this run's identities. */
120
+ adjudications: Schema.optionalKey(Schema.Array(StoredAdjudication).check(Schema.isMaxLength(20))),
111
121
  }) {}
112
122
 
113
123
  export interface ExecuteReviewOptions {
@@ -135,10 +145,19 @@ export interface ExecuteReviewOptions {
135
145
  readonly runUrl?: string | undefined;
136
146
  }
137
147
 
138
- /** Build the mission one review run frames from the source's snapshot. */
148
+ /**
149
+ * Build the mission one review run frames from the source's snapshot. The
150
+ * optional continuity context (adjudicated identities, prior-round findings
151
+ * on re-reviewed scope) reaches only RUN missions — fingerprint missions stay
152
+ * plain so an adjudication never invalidates skip-unchanged authority.
153
+ */
139
154
  export const buildReviewMission = (
140
155
  metadata: PullRequestMetadata,
141
156
  files: ReadonlyArray<ChangedFile>,
157
+ context?: {
158
+ readonly adjudicated?: ReadonlyArray<string> | undefined;
159
+ readonly priorFindings?: ReadonlyArray<string> | undefined;
160
+ },
142
161
  ): ReviewMission =>
143
162
  ReviewMission.make({
144
163
  repository: metadata.repository,
@@ -148,6 +167,12 @@ export const buildReviewMission = (
148
167
  baseRef: metadata.baseRef,
149
168
  headRef: metadata.headRef,
150
169
  changedFileCount: files.length,
170
+ ...(context?.adjudicated !== undefined && context.adjudicated.length > 0
171
+ ? { adjudicatedContext: context.adjudicated.slice(0, 20) }
172
+ : {}),
173
+ ...(context?.priorFindings !== undefined && context.priorFindings.length > 0
174
+ ? { priorFindingContext: context.priorFindings.slice(0, 20) }
175
+ : {}),
151
176
  });
152
177
 
153
178
  /** Enforce the configured findings bound on an already-validated review. */
@@ -165,12 +190,32 @@ export const enforceFindingsBound = (review: CodeReview, maxFindings: number): C
165
190
  const findingKey = (finding: ReviewFinding): string =>
166
191
  `${finding.path}\u0000${finding.startLine}\u0000${finding.endLine}\u0000${finding.severity}\u0000${finding.title}`;
167
192
 
193
+ /**
194
+ * Continuity inputs resolved BEFORE any model work: the standing maintainer
195
+ * adjudications (fresh host listing merged later-wins over the prior state's
196
+ * stored set) and the prior-round findings whose paths this run re-reviews.
197
+ * The latter are dropped from the carry (the new round re-decides them) but
198
+ * injected as prompt context so successive rounds do not silently contradict
199
+ * each other — context ONLY, never auto-carried into active findings.
200
+ */
201
+ const resolveReviewContinuityContext = Effect.fn("resolveReviewContinuityContext")(function* () {
202
+ const executionContext = yield* ReviewExecutionContext;
203
+ const priorState =
204
+ executionContext.mode === "incremental" ? executionContext.priorState : undefined;
205
+ const adjudications = yield* collectReviewAdjudications(priorState?.adjudications ?? []);
206
+ const affectedPaths = new Set(executionContext.affectedPaths);
207
+ const priorFindingsOnScope =
208
+ priorState?.unresolvedFindings.filter((finding) => affectedPaths.has(finding.path)) ?? [];
209
+ return { adjudications, priorFindingsOnScope };
210
+ });
211
+
168
212
  /** One shape-specific review result, before the shared settlement tail. */
169
213
  interface ReviewCore {
170
214
  readonly review: CodeReview;
171
215
  readonly inputCoverage: ReviewInputCoverage;
172
216
  readonly assurance: ReviewAssurance;
173
217
  readonly unreviewedPaths: ReadonlyArray<string>;
218
+ readonly unreviewedPasses?: ReadonlyArray<StoredUnreviewedPass> | undefined;
174
219
  readonly turns: number;
175
220
  }
176
221
 
@@ -189,76 +234,137 @@ const settleReviewRun = (
189
234
  readonly anchorFiles: ReadonlyArray<ChangedFile>;
190
235
  readonly fingerprint: string | undefined;
191
236
  readonly usage: UsageTotals | undefined;
237
+ readonly adjudications?: ReadonlyArray<StoredAdjudication> | undefined;
192
238
  },
193
239
  options: ExecuteReviewOptions,
194
240
  ) =>
195
241
  Effect.gen(function* () {
196
242
  const { metadata, files, anchorFiles, fingerprint, usage } = context;
197
- const executionContext = Option.getOrUndefined(
198
- yield* Effect.serviceOption(ReviewExecutionContext),
199
- );
200
- const review = enforceFindingsBound(core.review, clampMaxFindings(options.maxFindings));
243
+ const executionContext = yield* ReviewExecutionContext;
244
+ const adjudications = context.adjudications ?? [];
245
+ const adjudicatedIdentities = new Set(adjudications.map(adjudicationIdentity));
246
+ const isAdjudicatedFinding = (finding: ReviewFinding): boolean =>
247
+ adjudicatedIdentities.has(findingIdentity(finding));
248
+ const isAdjudicatedConcern = (concern: ReviewConcern): boolean =>
249
+ adjudicatedIdentities.has(concernIdentity(concern));
250
+ // Suppress adjudicated model output before any ranking or bounding. A
251
+ // suppressed blocker must never consume the slot of an active finding.
252
+ const filteredReview =
253
+ adjudicatedIdentities.size === 0
254
+ ? core.review
255
+ : CodeReview.make({
256
+ summary: core.review.summary,
257
+ verdict: core.review.verdict,
258
+ findings: core.review.findings.filter((finding) => !isAdjudicatedFinding(finding)),
259
+ ...(core.review.concerns === undefined
260
+ ? {}
261
+ : {
262
+ concerns: core.review.concerns.filter(
263
+ (concern) => !isAdjudicatedConcern(concern),
264
+ ),
265
+ }),
266
+ ...(core.review.walkthrough === undefined
267
+ ? {}
268
+ : { walkthrough: core.review.walkthrough }),
269
+ });
270
+ const reviewPaths = new Set(files.map((file) => file.path));
271
+ const normalizedReview = CodeReview.make({
272
+ ...filteredReview,
273
+ ...(filteredReview.concerns === undefined
274
+ ? {}
275
+ : {
276
+ concerns: filteredReview.concerns.map((concern) => {
277
+ const evidencePaths = concern.evidencePaths;
278
+ if (
279
+ evidencePaths === undefined ||
280
+ evidencePaths.some((path) => !reviewPaths.has(path))
281
+ ) {
282
+ const { evidencePaths: _invalid, ...pathless } = concern;
283
+ return ReviewConcern.make(pathless);
284
+ }
285
+ return ReviewConcern.make({
286
+ ...concern,
287
+ evidencePaths: [...new Set(evidencePaths)].sort(),
288
+ });
289
+ }),
290
+ }),
291
+ });
292
+ // Adjudicated identities leave the published review entirely: no inline
293
+ // comment, no severity count, no verdict influence — they render only in
294
+ // the plan's collapsed adjudicated section. Only identity-equal items are
295
+ // suppressed; a materially different finding at the same location (a
296
+ // different title) is untouched.
297
+ const review = enforceFindingsBound(normalizedReview, clampMaxFindings(options.maxFindings));
201
298
  const { inputCoverage, assurance } = core;
202
299
  const unreviewedPaths = [...new Set(core.unreviewedPaths)].sort();
203
- const reviewTotalFiles = executionContext?.totalFiles ?? metadata.totalChangedFiles;
204
- const affectedPaths = new Set(
205
- executionContext?.affectedPaths ??
206
- files.flatMap((file) =>
207
- file.previousPath === undefined ? [file.path] : [file.path, file.previousPath],
208
- ),
209
- );
300
+ const reviewTotalFiles = executionContext.totalFiles;
301
+ const affectedPaths = new Set(executionContext.affectedPaths);
210
302
  const priorState =
211
- executionContext?.mode === "incremental" ? executionContext.priorState : undefined;
303
+ executionContext.mode === "incremental" ? executionContext.priorState : undefined;
212
304
  const carriedCandidates =
213
305
  priorState?.unresolvedFindings
214
306
  .filter((finding) => !affectedPaths.has(finding.path))
215
307
  .map(fromStoredFinding) ?? [];
216
- const activeFindings = rankAndDedupeFindings([...carriedCandidates, ...review.findings]).slice(
217
- 0,
218
- clampMaxFindings(options.maxFindings),
308
+ const eligibleCarriedCandidates = carriedCandidates.filter(
309
+ (finding) => !isAdjudicatedFinding(finding),
219
310
  );
311
+ const activeFindings = rankAndDedupeFindings([
312
+ ...eligibleCarriedCandidates,
313
+ ...review.findings.filter((finding) => !isAdjudicatedFinding(finding)),
314
+ ]).slice(0, clampMaxFindings(options.maxFindings));
220
315
  const activeFindingKeys = new Set(activeFindings.map(findingKey));
221
316
  const currentFindingKeys = new Set(review.findings.map(findingKey));
222
- const carriedFindings = carriedCandidates.filter(
317
+ const carriedFindings = eligibleCarriedCandidates.filter(
223
318
  (finding) =>
224
319
  activeFindingKeys.has(findingKey(finding)) && !currentFindingKeys.has(findingKey(finding)),
225
320
  );
226
- // Non-anchored concerns cannot be mapped safely to one affected path, so
227
- // incremental runs carry them conservatively until the explicit final audit.
228
- const carriedConcernCandidates = priorState?.unresolvedConcerns.map(fromStoredConcern) ?? [];
321
+ // A concern remains active only while every host-validated evidence path
322
+ // is unchanged. Touching or removing any one invalidates the old claim;
323
+ // range selection reopens its remaining current paths for fresh context.
324
+ const carriedConcernCandidates =
325
+ priorState?.unresolvedConcerns
326
+ .filter(
327
+ (concern) =>
328
+ concern.evidencePaths !== undefined &&
329
+ concern.evidencePaths.every((path) => !affectedPaths.has(path)),
330
+ )
331
+ .map(fromStoredConcern) ?? [];
332
+ const eligibleCarriedConcernCandidates = carriedConcernCandidates.filter(
333
+ (concern) => !isAdjudicatedConcern(concern),
334
+ );
229
335
  const activeConcerns = rankAndDedupeConcerns([
230
- ...carriedConcernCandidates,
231
- ...(review.concerns ?? []),
336
+ ...eligibleCarriedConcernCandidates,
337
+ ...(review.concerns ?? []).filter((concern) => !isAdjudicatedConcern(concern)),
232
338
  ]);
233
- const currentConcernKeys = new Set(
234
- (review.concerns ?? []).map((concern) => `${concern.title}\u0000${concern.body}`),
235
- );
236
- const activeConcernKeys = new Set(
237
- activeConcerns.map((concern) => `${concern.title}\u0000${concern.body}`),
238
- );
239
- const carriedConcerns = carriedConcernCandidates.filter((concern) => {
240
- const key = `${concern.title}\u0000${concern.body}`;
339
+ const currentConcernKeys = new Set((review.concerns ?? []).map(reviewConcernKey));
340
+ const activeConcernKeys = new Set(activeConcerns.map(reviewConcernKey));
341
+ const carriedConcerns = eligibleCarriedConcernCandidates.filter((concern) => {
342
+ const key = reviewConcernKey(concern);
241
343
  return activeConcernKeys.has(key) && !currentConcernKeys.has(key);
242
344
  });
243
345
  const settled =
244
346
  inputCoverage.status === "complete" &&
245
347
  assurance.status !== "incomplete" &&
246
348
  unreviewedPaths.length === 0;
349
+ const concernsHaveEvidencePaths = activeConcerns.every(
350
+ (concern) => concern.evidencePaths !== undefined,
351
+ );
247
352
  // The fingerprint marker is standalone skip authority for fingerprint-only
248
353
  // harnesses, so it is embedded only for a fully settled run.
249
- const skipFingerprint = settled ? fingerprint : undefined;
354
+ const skipFingerprint = settled && concernsHaveEvidencePaths ? fingerprint : undefined;
250
355
  const carriedScopeFits = unreviewedPaths.length <= MAX_STORED_UNREVIEWED_PATHS;
251
356
  const stateCandidate =
252
- executionContext !== undefined &&
253
357
  fingerprint !== undefined &&
358
+ executionContext.profileFingerprint !== undefined &&
254
359
  metadata.baseSha !== undefined &&
255
360
  // The fingerprint and stored baseline describe the FULL pull-request
256
361
  // surface; a truncated anchor surface cannot make either claim.
257
362
  anchorFiles.length >= metadata.totalChangedFiles &&
258
363
  carriedScopeFits &&
364
+ concernsHaveEvidencePaths &&
259
365
  executionContext.stateAuthenticator?.status === "available"
260
366
  ? ReviewState.make({
261
- version: 2,
367
+ version: 1,
262
368
  repository: metadata.repository,
263
369
  pullRequestNumber: metadata.number,
264
370
  baseRef: metadata.baseRef,
@@ -266,24 +372,27 @@ const settleReviewRun = (
266
372
  headRef: metadata.headRef,
267
373
  reviewedHeadSha: metadata.headSha,
268
374
  profileFingerprint: executionContext.profileFingerprint,
269
- acceptedScopeFingerprint: fingerprint,
375
+ settledScopeFingerprint: fingerprint,
270
376
  reviewedPathCount: anchorFiles.length,
271
377
  unresolvedFindings: activeFindings.map(toStoredFinding),
272
378
  unresolvedConcerns: activeConcerns.map(toStoredConcern),
273
379
  unreviewedPaths,
380
+ unreviewedPasses: (core.unreviewedPasses ?? []).slice(0, MAX_STORED_UNREVIEWED_PASSES),
274
381
  settled,
275
382
  lastReviewMode: executionContext.mode,
383
+ ...(adjudications.length === 0 ? {} : { adjudications }),
276
384
  })
277
385
  : undefined;
278
386
  const continuity =
279
- stateCandidate === undefined || executionContext?.stateAuthenticator === undefined
387
+ stateCandidate === undefined || executionContext.stateAuthenticator === undefined
280
388
  ? {
281
389
  state: undefined,
282
390
  marker: undefined,
283
- notice:
284
- executionContext !== undefined && !carriedScopeFits
285
- ? `carried unreviewed scope (${unreviewedPaths.length} paths) exceeded the ${MAX_STORED_UNREVIEWED_PATHS}-path continuity bound`
286
- : executionContext?.stateAuthenticator?.status === "unavailable"
391
+ notice: !carriedScopeFits
392
+ ? `carried unreviewed scope (${unreviewedPaths.length} paths) exceeded the ${MAX_STORED_UNREVIEWED_PATHS}-path continuity bound`
393
+ : !concernsHaveEvidencePaths
394
+ ? "one or more review concerns lacked host-validated affected paths"
395
+ : executionContext.stateAuthenticator?.status === "unavailable"
287
396
  ? (executionContext.stateAuthenticator.unavailableReason ??
288
397
  "authenticated continuity state is unavailable")
289
398
  : undefined,
@@ -316,29 +425,29 @@ const settleReviewRun = (
316
425
  unreviewedPaths,
317
426
  carriedFindings,
318
427
  carriedConcerns,
319
- reviewMode: executionContext?.mode,
320
- reviewReason: executionContext?.reason,
321
- baselineSha: executionContext?.baselineSha,
428
+ reviewMode: executionContext.mode,
429
+ reviewReason: executionContext.reason,
430
+ baselineSha: executionContext.baselineSha,
322
431
  reviewFilesVisible: files.length,
323
432
  reviewTotalFiles,
324
433
  stateMarker: continuity.marker,
325
434
  stateNotice: continuity.notice,
435
+ ...(adjudications.length === 0 ? {} : { adjudications }),
326
436
  });
327
437
  const shared = {
328
438
  review,
329
439
  activeFindings,
330
440
  activeConcerns,
331
- coverage: compatibilityCoverage(inputCoverage, assurance),
332
441
  inputCoverage,
333
442
  assurance,
334
443
  unreviewedPaths,
335
444
  plan,
336
445
  turns: core.turns,
337
446
  ...(usage === undefined ? {} : { usage }),
338
- ...(executionContext === undefined
339
- ? {}
340
- : { reviewMode: executionContext.mode, reviewReason: executionContext.reason }),
447
+ reviewMode: executionContext.mode,
448
+ reviewReason: executionContext.reason,
341
449
  ...(continuity.state === undefined ? {} : { state: continuity.state }),
450
+ ...(adjudications.length === 0 ? {} : { adjudications }),
342
451
  };
343
452
  if (!options.post) return ReviewRunOutcome.make(shared);
344
453
  const publisher = yield* ReviewPublisher;
@@ -375,10 +484,12 @@ export const executeReview = <
375
484
  const metadata = yield* source.metadata;
376
485
  const files = yield* source.changedFiles;
377
486
  const anchorFiles = yield* source.anchorFiles;
378
- const executionContext = Option.getOrUndefined(
379
- yield* Effect.serviceOption(ReviewExecutionContext),
380
- );
381
- const mission = buildReviewMission(metadata, files);
487
+ const executionContext = yield* ReviewExecutionContext;
488
+ const continuity = yield* resolveReviewContinuityContext();
489
+ const mission = buildReviewMission(metadata, files, {
490
+ adjudicated: continuity.adjudications.map(renderAdjudicationContextLine),
491
+ priorFindings: continuity.priorFindingsOnScope.map(renderPriorFindingContextLine),
492
+ });
382
493
  const fullMission = buildReviewMission(metadata, anchorFiles);
383
494
  const fingerprint =
384
495
  options.signature === undefined
@@ -398,7 +509,7 @@ export const executeReview = <
398
509
  const review = yield* Schema.decodeUnknownEffect(CodeReview)(result.output);
399
510
  const assessment = assessFlatReview({
400
511
  files,
401
- totalFiles: executionContext?.totalFiles ?? metadata.totalChangedFiles,
512
+ totalFiles: executionContext.totalFiles,
402
513
  anchorFiles,
403
514
  totalAnchorFiles: metadata.totalChangedFiles,
404
515
  events,
@@ -412,7 +523,7 @@ export const executeReview = <
412
523
  unreviewedPaths: assessment.unreviewedPaths,
413
524
  turns: result.turns,
414
525
  },
415
- { metadata, files, anchorFiles, fingerprint, usage },
526
+ { metadata, files, anchorFiles, fingerprint, usage, adjudications: continuity.adjudications },
416
527
  options,
417
528
  );
418
529
  });
@@ -432,9 +543,7 @@ export const executeFanOutReview = <Provider, ModelProvides, ModelRequires>(
432
543
  const metadata = yield* source.metadata;
433
544
  const files = yield* source.changedFiles;
434
545
  const anchorFiles = yield* source.anchorFiles;
435
- const executionContext = Option.getOrUndefined(
436
- yield* Effect.serviceOption(ReviewExecutionContext),
437
- );
546
+ const executionContext = yield* ReviewExecutionContext;
438
547
  const fullMission = buildReviewMission(metadata, anchorFiles);
439
548
  const fingerprint =
440
549
  options.signature === undefined
@@ -442,13 +551,30 @@ export const executeFanOutReview = <Provider, ModelProvides, ModelRequires>(
442
551
  : yield* computeChangesetFingerprint(anchorFiles, options.signature(fullMission));
443
552
 
444
553
  const budget = yield* makeUsageBudget(options.limits ?? fanOutReviewBudgetLimits);
445
- const totalFiles = executionContext?.totalFiles ?? metadata.totalChangedFiles;
554
+ const totalFiles = executionContext.totalFiles;
555
+ const continuity = yield* resolveReviewContinuityContext();
446
556
  const pipeline = yield* runFanOutReview(binding, {
447
557
  files,
448
558
  anchorFiles,
449
559
  totalChangedFiles: totalFiles,
450
560
  maxFindings: options.maxFindings,
451
561
  budget: toRunBudgetHook(budget),
562
+ ...(executionContext.retryPaths.length > 0
563
+ ? {
564
+ retry: {
565
+ paths: executionContext.retryPaths,
566
+ stages: executionContext.retryStages,
567
+ },
568
+ }
569
+ : {}),
570
+ ...(continuity.adjudications.length > 0 || continuity.priorFindingsOnScope.length > 0
571
+ ? {
572
+ priorContext: buildPriorReviewContext(
573
+ continuity.adjudications,
574
+ continuity.priorFindingsOnScope,
575
+ ),
576
+ }
577
+ : {}),
452
578
  });
453
579
  const inputCoverage = fanOutInputCoverage({
454
580
  plan: pipeline.plan,
@@ -464,9 +590,17 @@ export const executeFanOutReview = <Provider, ModelProvides, ModelRequires>(
464
590
  inputCoverage,
465
591
  assurance: pipeline.assurance,
466
592
  unreviewedPaths: pipeline.unreviewedPaths,
593
+ unreviewedPasses: pipeline.unreviewedPasses
594
+ .slice(0, MAX_STORED_UNREVIEWED_PASSES)
595
+ .map((pass) =>
596
+ StoredUnreviewedPass.make({
597
+ stage: pass.stage,
598
+ paths: pass.paths.slice(0, 12),
599
+ }),
600
+ ),
467
601
  turns: pipeline.turns,
468
602
  },
469
- { metadata, files, anchorFiles, fingerprint, usage },
603
+ { metadata, files, anchorFiles, fingerprint, usage, adjudications: continuity.adjudications },
470
604
  options,
471
605
  );
472
606
  });