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

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/src/action.ts CHANGED
@@ -19,6 +19,8 @@ import {
19
19
  makeAnthropicReviewModel,
20
20
  makeOpenAiReviewModel,
21
21
  openAiClientLayer,
22
+ validateReviewServiceTier,
23
+ type OpenAiServiceTier,
22
24
  type ReviewProvider,
23
25
  } from "./internal/providers.ts";
24
26
  import { retireStaleReviews } from "./internal/retirement.ts";
@@ -30,6 +32,7 @@ import {
30
32
  ReviewExecutionContext,
31
33
  ReviewHeadComparison,
32
34
  ReviewStateAuthenticator,
35
+ type ReviewTreeComparison,
33
36
  isLineageAncestor,
34
37
  type ReviewMode,
35
38
  type ReviewState,
@@ -90,6 +93,7 @@ export interface ResolvedActionInputs {
90
93
  readonly provider: ReviewProvider;
91
94
  readonly model: string | undefined;
92
95
  readonly effort: EffortPosition | undefined;
96
+ readonly serviceTier: OpenAiServiceTier | undefined;
93
97
  readonly post: boolean;
94
98
  readonly applyVerdict: boolean;
95
99
  readonly fanOut: boolean;
@@ -118,6 +122,12 @@ export const resolveActionInputs = Effect.fn("resolveActionInputs")(function* ()
118
122
  return yield* InvalidEffortInput.make({ input: effortRaw.value });
119
123
  }
120
124
  }
125
+ const serviceTier = yield* validateReviewServiceTier(
126
+ provider,
127
+ Option.getOrUndefined(
128
+ yield* Config.option(Config.literals(["fast"], "PR_REVIEW_SERVICE_TIER")),
129
+ ),
130
+ );
121
131
  const maxDurationMinutes = Option.getOrUndefined(
122
132
  yield* Config.option(Config.int("PR_REVIEW_MAX_DURATION_MINUTES")),
123
133
  );
@@ -149,6 +159,7 @@ export const resolveActionInputs = Effect.fn("resolveActionInputs")(function* ()
149
159
  provider,
150
160
  model: Option.getOrUndefined(model),
151
161
  effort,
162
+ serviceTier,
152
163
  post,
153
164
  applyVerdict,
154
165
  fanOut,
@@ -594,7 +605,8 @@ export const runReviewAction = <E, R, FingerprintE = never, FingerprintR = never
594
605
  }
595
606
  let comparison: ReviewHeadComparison | undefined;
596
607
  let baseComparison: ReviewHeadComparison | undefined;
597
- let contentComparison: ReviewHeadComparison | undefined;
608
+ let contentComparison: ReviewTreeComparison | undefined;
609
+ let contentComparisonFailure: string | undefined;
598
610
  if (
599
611
  (options.reviewMode ?? "incremental") === "incremental" &&
600
612
  recovered.state !== undefined
@@ -635,15 +647,34 @@ export const runReviewAction = <E, R, FingerprintE = never, FingerprintR = never
635
647
  (comparison === undefined ||
636
648
  !isLineageAncestor(comparison, recovered.state, metadata.headSha))
637
649
  ) {
638
- contentComparison = yield* history
639
- .compareTrees(recovered.state.reviewedHeadSha, metadata.headSha)
640
- .pipe(Effect.orElseSucceed(() => undefined));
641
- if (contentComparison !== undefined && reviewer.filterFiles !== undefined) {
642
- contentComparison = ReviewHeadComparison.make({
643
- ...contentComparison,
644
- files: reviewer.filterFiles(contentComparison.files),
645
- });
650
+ const comparisonPaths = new Set(
651
+ fullFiles.flatMap((file) =>
652
+ file.previousPath === undefined ? [file.path] : [file.path, file.previousPath],
653
+ ),
654
+ );
655
+ for (const finding of recovered.state.unresolvedFindings) {
656
+ comparisonPaths.add(finding.path);
657
+ }
658
+ for (const concern of recovered.state.unresolvedConcerns) {
659
+ for (const path of concern.evidencePaths ?? []) comparisonPaths.add(path);
646
660
  }
661
+ for (const path of recovered.state.unreviewedPaths) comparisonPaths.add(path);
662
+ const treeResult = yield* history
663
+ .compareTrees(recovered.state.reviewedHeadSha, metadata.headSha, [...comparisonPaths])
664
+ .pipe(
665
+ Effect.match({
666
+ onFailure: (failure) => ({
667
+ comparison: undefined,
668
+ failure: failure.reason,
669
+ }),
670
+ onSuccess: (treeComparison) => ({
671
+ comparison: treeComparison,
672
+ failure: undefined,
673
+ }),
674
+ }),
675
+ );
676
+ contentComparison = treeResult.comparison;
677
+ contentComparisonFailure = treeResult.failure;
647
678
  }
648
679
  }
649
680
  selection = {
@@ -656,6 +687,7 @@ export const runReviewAction = <E, R, FingerprintE = never, FingerprintR = never
656
687
  comparison,
657
688
  baseComparison,
658
689
  contentComparison,
690
+ contentComparisonFailure,
659
691
  lookupFailure: recovered.failure,
660
692
  }),
661
693
  stateAuthenticator,
@@ -798,7 +830,12 @@ export const reviewActionProgram = Effect.gen(function* () {
798
830
  onSome: webCryptoReviewStateAuthenticatorLayer,
799
831
  });
800
832
  const guidance = yield* resolveGuidance(inputs);
801
- const modelLabel = describeReviewModel(inputs.provider, inputs.model, inputs.effort);
833
+ const modelLabel = describeReviewModel(
834
+ inputs.provider,
835
+ inputs.model,
836
+ inputs.effort,
837
+ inputs.serviceTier,
838
+ );
802
839
  const defaults = inputs.fanOut ? fanOutReviewBudgetLimits : reviewBudgetLimits;
803
840
  const budget =
804
841
  inputs.maxDurationMinutes === undefined
@@ -832,7 +869,7 @@ export const reviewActionProgram = Effect.gen(function* () {
832
869
  Effect.provide(Layer.merge(stateAuthenticatorLayer, anthropicClientLayer)),
833
870
  );
834
871
  }
835
- const model = makeOpenAiReviewModel(inputs.model, inputs.effort);
872
+ const model = makeOpenAiReviewModel(inputs.model, inputs.effort, inputs.serviceTier);
836
873
  const reviewer = inputs.fanOut
837
874
  ? PrReview.makeFanOut({ ...shared, model })
838
875
  : PrReview.make({ ...shared, model });
package/src/cli.ts CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  makeAnthropicReviewModel,
17
17
  makeOpenAiReviewModel,
18
18
  openAiClientLayer,
19
+ validateReviewServiceTier,
19
20
  type ReviewProvider,
20
21
  } from "./internal/providers.ts";
21
22
  import { ReviewPublicationPlan } from "./internal/render.ts";
@@ -53,6 +54,12 @@ const effortFlag = Flag.string("effort").pipe(
53
54
  'Reasoning effort: "low", "medium", "high", "xhigh", "max", or a number in [0, 1] resolved onto the provider\'s own ladder.',
54
55
  ),
55
56
  );
57
+ const serviceTierFlag = Flag.choice("service-tier", ["fast"]).pipe(
58
+ Flag.optional,
59
+ Flag.withDescription(
60
+ 'OpenAI Responses service tier. The only supported value is "fast"; omit it to use the OpenAI project default.',
61
+ ),
62
+ );
56
63
  const postFlag = Flag.boolean("post").pipe(
57
64
  Flag.withDefault(false),
58
65
  Flag.withDescription("Post the review to GitHub; without it the plan prints to stdout."),
@@ -108,6 +115,7 @@ const command = CliCommand.make(
108
115
  provider: providerFlag,
109
116
  model: modelFlag,
110
117
  effort: effortFlag,
118
+ serviceTier: serviceTierFlag,
111
119
  post: postFlag,
112
120
  applyVerdict: applyVerdictFlag,
113
121
  fanOut: fanOutFlag,
@@ -118,6 +126,10 @@ const command = CliCommand.make(
118
126
  (flags) =>
119
127
  Effect.gen(function* () {
120
128
  const provider = yield* decodeProvider(flags.provider);
129
+ const serviceTier = yield* validateReviewServiceTier(
130
+ provider,
131
+ Option.getOrUndefined(flags.serviceTier),
132
+ );
121
133
  const target = yield* resolveReviewTarget({
122
134
  repository: Option.getOrUndefined(flags.repo),
123
135
  number: Option.getOrUndefined(flags.pr),
@@ -138,7 +150,7 @@ const command = CliCommand.make(
138
150
  .map((pattern) => pattern.trim())
139
151
  .filter((pattern) => pattern.length > 0),
140
152
  maxFindings: Option.getOrUndefined(flags.maxFindings),
141
- modelLabel: describeReviewModel(provider, model, effort),
153
+ modelLabel: describeReviewModel(provider, model, effort, serviceTier),
142
154
  };
143
155
 
144
156
  yield* Console.log(
@@ -176,7 +188,7 @@ const command = CliCommand.make(
176
188
  Effect.provide(Layer.merge(githubLayers, anthropicClientLayer)),
177
189
  );
178
190
  } else {
179
- const boundModel = makeOpenAiReviewModel(model, effort);
191
+ const boundModel = makeOpenAiReviewModel(model, effort, serviceTier);
180
192
  const reviewer = flags.fanOut
181
193
  ? PrReview.makeFanOut({ ...shared, model: boundModel })
182
194
  : PrReview.make({ ...shared, model: boundModel });
@@ -14,6 +14,7 @@ const INPUT_TO_ENV: ReadonlyArray<readonly [input: string, env: string]> = [
14
14
  ["INPUT_PROVIDER", "PR_REVIEW_PROVIDER"],
15
15
  ["INPUT_MODEL", "PR_REVIEW_MODEL"],
16
16
  ["INPUT_EFFORT", "PR_REVIEW_EFFORT"],
17
+ ["INPUT_SERVICE-TIER", "PR_REVIEW_SERVICE_TIER"],
17
18
  ["INPUT_MAX-DURATION-MINUTES", "PR_REVIEW_MAX_DURATION_MINUTES"],
18
19
  ["INPUT_POST", "PR_REVIEW_POST"],
19
20
  ["INPUT_APPLY-VERDICT", "PR_REVIEW_APPLY_VERDICT"],
@@ -61,7 +61,8 @@ export interface PrReviewSharedOptions {
61
61
  /** Run-level usage bounds; defaults to the shape's packaged limits. */
62
62
  readonly budget?: UsageBudgetLimits | undefined;
63
63
  /**
64
- * Human-readable descriptor of the bound model (provider, model id, effort)
64
+ * Human-readable descriptor of the bound model (provider, model id, effort,
65
+ * and request profile such as service tier)
65
66
  * rendered into the review footer and included in the fingerprint
66
67
  * signature, so changing the binding re-reviews instead of skipping.
67
68
  */
@@ -403,13 +403,20 @@ export interface FanOutPipelineInput {
403
403
  /** Shared run budget observed by every child pass. */
404
404
  readonly budget?: RunBudgetHook<BudgetExceeded | BudgetAdapterError> | undefined;
405
405
  /**
406
- * Unchanged leftover paths from a prior failed pass. Those units retry only
407
- * the recorded stages no second general discovery on files nobody touched.
406
+ * Unchanged leftovers from prior failed passes. Every stage stays attached
407
+ * to its own paths; failed verification reopens both discovery perspectives
408
+ * for only those paths because candidate payloads are not persisted.
408
409
  */
409
410
  readonly retry?:
410
411
  | {
411
- readonly paths: ReadonlyArray<string>;
412
- readonly stages: ReadonlyArray<FailedReviewPass["stage"]>;
412
+ readonly passes?: ReadonlyArray<{
413
+ readonly stage: FailedReviewPass["stage"];
414
+ readonly paths: ReadonlyArray<string>;
415
+ }>;
416
+ /** @deprecated Pass path-bound `passes`; this flat form cannot preserve ownership. */
417
+ readonly paths?: ReadonlyArray<string>;
418
+ /** @deprecated Pass path-bound `passes`; this flat form cannot preserve ownership. */
419
+ readonly stages?: ReadonlyArray<FailedReviewPass["stage"]>;
413
420
  }
414
421
  | undefined;
415
422
  /**
@@ -840,11 +847,38 @@ const scheduleFanOutWork = (
840
847
  ): {
841
848
  readonly plan: ReviewUnitPlan;
842
849
  readonly passesByUnit: Map<string, ReadonlyArray<ReviewDiscoveryPass>>;
843
- readonly overflowRetryPaths: ReadonlyArray<string>;
850
+ readonly overflowRetryPasses: ReadonlyArray<{
851
+ readonly stage: FailedReviewPass["stage"];
852
+ readonly paths: ReadonlyArray<string>;
853
+ }>;
844
854
  } => {
845
- const retryPathSet = new Set(input.retry?.paths ?? []);
846
- const retryStages = new Set(input.retry?.stages ?? []);
847
- if (retryPathSet.size === 0) {
855
+ const requestedRetryPasses =
856
+ input.retry?.passes ??
857
+ input.retry?.stages?.map((stage) => ({ stage, paths: input.retry?.paths ?? [] })) ??
858
+ [];
859
+ const requestedStagesByPath = new Map<string, Set<FailedReviewPass["stage"]>>();
860
+ for (const pass of requestedRetryPasses) {
861
+ for (const path of pass.paths) {
862
+ const stages = requestedStagesByPath.get(path) ?? new Set<FailedReviewPass["stage"]>();
863
+ stages.add(pass.stage);
864
+ requestedStagesByPath.set(path, stages);
865
+ }
866
+ }
867
+ const canonicalPathByKnownPath = new Map<string, string>();
868
+ const retryStagesByPath = new Map<string, Set<FailedReviewPass["stage"]>>();
869
+ for (const file of input.files) {
870
+ canonicalPathByKnownPath.set(file.path, file.path);
871
+ if (file.previousPath !== undefined) {
872
+ canonicalPathByKnownPath.set(file.previousPath, file.path);
873
+ }
874
+ const requested = [
875
+ requestedStagesByPath.get(file.path),
876
+ ...(file.previousPath === undefined ? [] : [requestedStagesByPath.get(file.previousPath)]),
877
+ ];
878
+ const stages = new Set(requested.flatMap((entry) => [...(entry ?? [])]));
879
+ if (stages.size > 0) retryStagesByPath.set(file.path, stages);
880
+ }
881
+ if (retryStagesByPath.size === 0) {
848
882
  const plan = planReviewUnits(input.files, { totalChangedFiles: input.totalChangedFiles });
849
883
  const passesByUnit = new Map<string, Array<ReviewDiscoveryPass>>();
850
884
  for (const pass of plan.discoveryPasses) {
@@ -852,65 +886,96 @@ const scheduleFanOutWork = (
852
886
  passes.push(pass);
853
887
  passesByUnit.set(pass.unitId, passes);
854
888
  }
855
- return { plan, passesByUnit, overflowRetryPaths: [] };
889
+ return { plan, passesByUnit, overflowRetryPasses: [] };
856
890
  }
857
- const freshFiles = input.files.filter((file) => !retryPathSet.has(file.path));
858
- const retryFiles = input.files.filter((file) => retryPathSet.has(file.path));
859
- const freshPlan = planReviewUnits(freshFiles, { totalChangedFiles: input.totalChangedFiles });
860
- const retryPlan = remapPlanUnitIds(
861
- planReviewUnits(retryFiles, { totalChangedFiles: input.totalChangedFiles }),
862
- freshPlan.units.length,
863
- );
864
- const acceptedFresh = freshPlan.units.slice(0, MAX_REVIEW_UNITS);
865
- const acceptedRetry = retryPlan.units.slice(
866
- 0,
867
- Math.max(0, MAX_REVIEW_UNITS - acceptedFresh.length),
868
- );
869
- const overflowRetryPaths = retryPlan.units
870
- .slice(acceptedRetry.length)
871
- .flatMap((unit) => [...unit.paths]);
872
- const retryPassFilter = (pass: ReviewDiscoveryPass): boolean => {
873
- const stage = pass.perspective === "risk-specialist" ? "specialist" : "discovery";
874
- return retryStages.has(stage);
891
+
892
+ type DiscoveryStage = "discovery" | "specialist";
893
+ const discoveryStagesFor = (
894
+ stages: ReadonlySet<FailedReviewPass["stage"]>,
895
+ ): ReadonlyArray<DiscoveryStage> => {
896
+ if (stages.has("verification")) return ["discovery", "specialist"];
897
+ return [
898
+ ...(stages.has("discovery") ? (["discovery"] as const) : []),
899
+ ...(stages.has("specialist") ? (["specialist"] as const) : []),
900
+ ];
875
901
  };
876
- const acceptedRetryIds = new Set(acceptedRetry.map((unit) => unit.unitId));
877
- let retryPasses = retryPlan.discoveryPasses.filter(
878
- (pass) => acceptedRetryIds.has(pass.unitId) && retryPassFilter(pass),
879
- );
880
- if (
881
- retryStages.has("verification") &&
882
- !retryStages.has("discovery") &&
883
- !retryStages.has("specialist")
884
- ) {
885
- retryPasses = retryPlan.discoveryPasses.filter((pass) => acceptedRetryIds.has(pass.unitId));
902
+ const freshFiles = input.files.filter((file) => !retryStagesByPath.has(file.path));
903
+ const retryGroups = new Map<
904
+ string,
905
+ { readonly stages: ReadonlyArray<DiscoveryStage>; readonly files: Array<ChangedFile> }
906
+ >();
907
+ for (const file of input.files) {
908
+ const retryStages = retryStagesByPath.get(file.path);
909
+ if (retryStages === undefined) continue;
910
+ const stages = discoveryStagesFor(retryStages);
911
+ const key = stages.join("|");
912
+ const group = retryGroups.get(key) ?? { stages, files: [] };
913
+ group.files.push(file);
914
+ retryGroups.set(key, group);
886
915
  }
887
- const acceptedFreshIds = new Set(acceptedFresh.map((unit) => unit.unitId));
888
- const freshPasses = freshPlan.discoveryPasses.filter((pass) => acceptedFreshIds.has(pass.unitId));
889
- const discoveryPasses = [...freshPasses, ...retryPasses];
916
+
917
+ const batches: ReadonlyArray<{
918
+ readonly stages: ReadonlyArray<DiscoveryStage>;
919
+ readonly files: ReadonlyArray<ChangedFile>;
920
+ }> = [
921
+ ...(freshFiles.length === 0
922
+ ? []
923
+ : [{ stages: ["discovery", "specialist"] as const, files: freshFiles }]),
924
+ ...[...retryGroups.entries()]
925
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
926
+ .map(([, group]) => group),
927
+ ];
928
+ const subplans: Array<ReviewUnitPlan> = [];
929
+ const acceptedUnits: Array<ReviewUnit> = [];
930
+ const rejectedUnits: Array<ReviewUnit> = [];
931
+ const discoveryPasses: Array<ReviewDiscoveryPass> = [];
932
+ for (const batch of batches) {
933
+ const batchPlan = remapPlanUnitIds(
934
+ planReviewUnits(batch.files, { totalChangedFiles: batch.files.length }),
935
+ acceptedUnits.length,
936
+ );
937
+ subplans.push(batchPlan);
938
+ const accepted = batchPlan.units.slice(0, Math.max(0, MAX_REVIEW_UNITS - acceptedUnits.length));
939
+ acceptedUnits.push(...accepted);
940
+ rejectedUnits.push(...batchPlan.units.slice(accepted.length));
941
+ const acceptedIds = new Set(accepted.map((unit) => unit.unitId));
942
+ discoveryPasses.push(
943
+ ...batchPlan.discoveryPasses.filter((pass) => {
944
+ const stage = pass.perspective === "risk-specialist" ? "specialist" : "discovery";
945
+ return acceptedIds.has(pass.unitId) && batch.stages.includes(stage);
946
+ }),
947
+ );
948
+ }
949
+
950
+ const acceptedPaths = new Set(acceptedUnits.flatMap((unit) => unit.paths));
951
+ const incompletePlannedPaths = new Set([
952
+ ...subplans.flatMap((plan) => plan.partialEvidencePaths),
953
+ ...subplans.flatMap((plan) => plan.unassignedPaths),
954
+ ...rejectedUnits.flatMap((unit) => unit.paths),
955
+ ]);
956
+ const partialEvidencePaths = [...incompletePlannedPaths]
957
+ .filter((path) => acceptedPaths.has(path))
958
+ .sort();
959
+ const unassignedPaths = [...incompletePlannedPaths]
960
+ .filter((path) => !acceptedPaths.has(path))
961
+ .sort();
962
+ const rejectedEvidenceShards = rejectedUnits.flatMap((unit) => unit.evidenceShards);
963
+ const undiffablePaths = [...new Set(subplans.flatMap((plan) => plan.undiffablePaths))].sort();
890
964
  const plan = ReviewUnitPlan.make({
891
965
  totalFiles: input.files.length,
892
- truncated: freshPlan.truncated || retryPlan.truncated,
893
- units: [...acceptedFresh, ...acceptedRetry],
966
+ truncated: input.files.length < input.totalChangedFiles,
967
+ units: acceptedUnits,
894
968
  discoveryPasses,
895
- undiffablePaths: [
896
- ...new Set([...freshPlan.undiffablePaths, ...retryPlan.undiffablePaths]),
897
- ].sort(),
898
- partialEvidencePaths: [
899
- ...new Set([...freshPlan.partialEvidencePaths, ...retryPlan.partialEvidencePaths]),
900
- ].sort(),
969
+ undiffablePaths,
970
+ partialEvidencePaths,
901
971
  unassignedEvidenceShardCount:
902
- freshPlan.unassignedEvidenceShardCount + retryPlan.unassignedEvidenceShardCount,
972
+ subplans.reduce((total, item) => total + item.unassignedEvidenceShardCount, 0) +
973
+ rejectedEvidenceShards.length,
903
974
  unassignedEvidenceShardIds: [
904
- ...freshPlan.unassignedEvidenceShardIds,
905
- ...retryPlan.unassignedEvidenceShardIds,
975
+ ...subplans.flatMap((item) => item.unassignedEvidenceShardIds),
976
+ ...rejectedEvidenceShards.map((shard) => shard.shardId),
906
977
  ].slice(0, MAX_REPORTED_UNASSIGNED_EVIDENCE_SHARDS),
907
- unassignedPaths: [
908
- ...new Set([
909
- ...freshPlan.unassignedPaths,
910
- ...retryPlan.unassignedPaths,
911
- ...overflowRetryPaths,
912
- ]),
913
- ].sort(),
978
+ unassignedPaths,
914
979
  });
915
980
  const passesByUnit = new Map<string, Array<ReviewDiscoveryPass>>();
916
981
  for (const pass of discoveryPasses) {
@@ -918,7 +983,31 @@ const scheduleFanOutWork = (
918
983
  passes.push(pass);
919
984
  passesByUnit.set(pass.unitId, passes);
920
985
  }
921
- return { plan, passesByUnit, overflowRetryPaths };
986
+ const incompletePaths = new Set([
987
+ ...partialEvidencePaths,
988
+ ...unassignedPaths,
989
+ ...undiffablePaths,
990
+ ]);
991
+ const overflowRetryPathsByStage = new Map<FailedReviewPass["stage"], Set<string>>();
992
+ for (const pass of requestedRetryPasses) {
993
+ for (const path of pass.paths) {
994
+ const canonicalPath = canonicalPathByKnownPath.get(path);
995
+ if (canonicalPath === undefined || !incompletePaths.has(canonicalPath)) continue;
996
+ const paths = overflowRetryPathsByStage.get(pass.stage) ?? new Set<string>();
997
+ paths.add(canonicalPath);
998
+ overflowRetryPathsByStage.set(pass.stage, paths);
999
+ }
1000
+ }
1001
+ const overflowRetryPasses = (["discovery", "specialist", "verification"] as const).flatMap(
1002
+ (stage) => {
1003
+ const paths = [...(overflowRetryPathsByStage.get(stage) ?? [])].sort();
1004
+ return Array.from({ length: Math.ceil(paths.length / MAX_UNIT_FILES) }, (_, index) => ({
1005
+ stage,
1006
+ paths: paths.slice(index * MAX_UNIT_FILES, (index + 1) * MAX_UNIT_FILES),
1007
+ }));
1008
+ },
1009
+ );
1010
+ return { plan, passesByUnit, overflowRetryPasses };
922
1011
  };
923
1012
 
924
1013
  /**
@@ -932,7 +1021,7 @@ export const runFanOutReview = <Provider, ModelProvides, ModelRequires>(
932
1021
  input: FanOutPipelineInput,
933
1022
  ) =>
934
1023
  Effect.gen(function* () {
935
- const { plan, passesByUnit, overflowRetryPaths } = scheduleFanOutWork(input);
1024
+ const { plan, passesByUnit, overflowRetryPasses } = scheduleFanOutWork(input);
936
1025
  const outcomes = yield* Effect.forEach(
937
1026
  plan.units,
938
1027
  (unit) => reviewUnit(binding, unit, passesByUnit.get(unit.unitId) ?? [], input),
@@ -1052,12 +1141,7 @@ export const runFanOutReview = <Provider, ModelProvides, ModelRequires>(
1052
1141
  ].sort(),
1053
1142
  unreviewedPasses: [
1054
1143
  ...outcomes.flatMap((outcome) => outcome.unreviewedPasses),
1055
- ...(overflowRetryPaths.length === 0
1056
- ? []
1057
- : (input.retry?.stages.length
1058
- ? input.retry.stages
1059
- : (["discovery", "specialist", "verification"] as const)
1060
- ).map((stage) => ({ stage, paths: overflowRetryPaths }))),
1144
+ ...overflowRetryPasses,
1061
1145
  ],
1062
1146
  turns: outcomes.reduce((total, outcome) => total + outcome.turns, 0),
1063
1147
  } satisfies FanOutPipelineOutcome;
@@ -8,7 +8,7 @@ import {
8
8
  ReviewPublisher,
9
9
  } from "./github.ts";
10
10
  import type { ReviewPublicationPlan } from "./render.ts";
11
- import type { ReviewHeadComparison, ReviewState } from "./review-state.ts";
11
+ import type { ReviewHeadComparison, ReviewState, ReviewTreeComparison } from "./review-state.ts";
12
12
  import {
13
13
  MAX_CHANGED_FILES,
14
14
  MAX_FILE_CHARS,
@@ -120,7 +120,7 @@ export const staticPriorReviews = (
120
120
  options: {
121
121
  readonly state?: Option.Option<ReviewState> | undefined;
122
122
  readonly comparison?: ReviewHeadComparison | undefined;
123
- readonly treeComparison?: ReviewHeadComparison | undefined;
123
+ readonly treeComparison?: ReviewTreeComparison | undefined;
124
124
  } = {},
125
125
  ): PriorReviews["Service"] =>
126
126
  PriorReviews.of({
@@ -142,7 +142,7 @@ export const staticPriorReviewsLayer = (
142
142
  options: {
143
143
  readonly state?: Option.Option<ReviewState> | undefined;
144
144
  readonly comparison?: ReviewHeadComparison | undefined;
145
- readonly treeComparison?: ReviewHeadComparison | undefined;
145
+ readonly treeComparison?: ReviewTreeComparison | undefined;
146
146
  } = {},
147
147
  ): Layer.Layer<PriorReviews> =>
148
148
  Layer.succeed(PriorReviews)(staticPriorReviews(fingerprint, options));