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

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.
@@ -10,13 +10,15 @@ import {
10
10
  import { type Tool } from "effect/unstable/ai";
11
11
 
12
12
  import {
13
- assessReviewPipeline,
13
+ assessFlatReview,
14
+ compatibilityCoverage,
15
+ fanOutInputCoverage,
14
16
  ReviewAssurance,
15
17
  ReviewCoverage,
16
18
  ReviewInputCoverage,
17
- type ReviewShape,
18
19
  } from "./coverage.ts";
19
20
  import type { ChangedFile } from "./diff.ts";
21
+ import { runFanOutReview, type FileReviewerBinding } from "./fan-out.ts";
20
22
  import { computeChangesetFingerprint } from "./fingerprint.ts";
21
23
  import { PublishedReview, ReviewPublisher } from "./github.ts";
22
24
  import { planPublication, ReviewPublicationPlan } from "./render.ts";
@@ -30,19 +32,28 @@ import {
30
32
  import {
31
33
  fromStoredConcern,
32
34
  fromStoredFinding,
35
+ MAX_STORED_UNREVIEWED_PASSES,
36
+ MAX_STORED_UNREVIEWED_PATHS,
33
37
  ReviewExecutionContext,
34
38
  ReviewState,
39
+ StoredUnreviewedPass,
35
40
  toStoredConcern,
36
41
  toStoredFinding,
37
42
  } from "./review-state.ts";
38
- import { rankAndDedupeFindings } from "./review-units.ts";
43
+ import { rankAndDedupeConcerns, rankAndDedupeFindings } from "./review-units.ts";
39
44
  import { PullRequestSource, type PullRequestMetadata } from "./source.ts";
40
45
 
41
46
  // ---------------------------------------------------------------------------
42
- // One review run, end to end: read the pull request, run the bounded agent,
43
- // validate the review against the real diff, then (optionally) publish.
44
- // Publication happens strictly AFTER the agent loop so no model turn can
45
- // observe or influence the mutation, and a failed run publishes nothing.
47
+ // One review run, end to end: read the pull request, run the bounded review
48
+ // (one flat agent, or the host-scheduled fan-out pipeline), validate the
49
+ // review against the real diff, then (optionally) publish. Publication
50
+ // happens strictly AFTER all model work so no model turn can observe or
51
+ // influence the mutation, and a failed run publishes nothing.
52
+ //
53
+ // Continuity is monotone: every completed run that can be signed advances the
54
+ // stored baseline, carrying genuinely-unsettled scope forward explicitly. A
55
+ // flaky pass therefore costs exactly its own scope on the next run — it can
56
+ // never freeze the baseline and reopen everything reviewed since.
46
57
  // ---------------------------------------------------------------------------
47
58
 
48
59
  /**
@@ -59,12 +70,9 @@ export const reviewBudgetLimits = UsageBudgetLimits.make({
59
70
  });
60
71
 
61
72
  /**
62
- * Run-level bounds for the fan-out coordinator. This budget observes only
63
- * the COORDINATOR'S own usage delegated children are bounded separately by
64
- * the delegation's `SubagentPolicy` reservation and the child definition's
65
- * own `AgentPolicy`, never silently by the parent's budget. The duration
66
- * ceiling is wider because delegation Tool Calls hold the parent turn open
67
- * while bounded children run.
73
+ * Run-level bounds for the fan-out pipeline. One budget observes EVERY child
74
+ * pass, so the ceiling covers bounded parallel discovery and verification
75
+ * plus the one-retry allowance.
68
76
  */
69
77
  export const fanOutReviewBudgetLimits = UsageBudgetLimits.make({
70
78
  maxInputTokens: 600_000,
@@ -87,23 +95,18 @@ export class ReviewRunOutcome extends Schema.Class<ReviewRunOutcome>(
87
95
  coverage: ReviewCoverage,
88
96
  /** Exact path/evidence assignment, distinct from semantic review work. */
89
97
  inputCoverage: ReviewInputCoverage,
90
- /** Settlement of configured discovery, specialist, and verification work. */
98
+ /** Settlement of scheduled discovery, specialist, and verification work. */
91
99
  assurance: ReviewAssurance,
100
+ /** Retryable scope this run could not settle; carried to the next run. */
101
+ unreviewedPaths: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(512))).check(
102
+ Schema.isMaxLength(300),
103
+ ),
92
104
  plan: ReviewPublicationPlan,
93
105
  published: Schema.optionalKey(PublishedReview),
94
- turns: Schema.Int.check(Schema.isGreaterThan(0)),
95
- /**
96
- * The run budget's observed usage. For the fan-out reviewer this observes
97
- * the COORDINATOR only — delegated children are bounded and accounted
98
- * separately by their reservations.
99
- */
106
+ /** Total settled model turns (all child passes for the fan-out pipeline). */
107
+ turns: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
108
+ /** The run budget's observed usage across the whole run. */
100
109
  usage: Schema.optionalKey(UsageTotals),
101
- /**
102
- * What `usage` observed: the whole run, or a fan-out coordinator only.
103
- * Absent when the caller declared no scope — consumers must not present
104
- * unscoped usage as whole-run totals.
105
- */
106
- usageScope: Schema.optionalKey(Schema.Literals(["run", "coordinator"])),
107
110
  reviewMode: Schema.optionalKey(Schema.Literals(["incremental", "full"])),
108
111
  reviewReason: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(1_000))),
109
112
  state: Schema.optionalKey(ReviewState),
@@ -114,7 +117,7 @@ export interface ExecuteReviewOptions {
114
117
  readonly post: boolean;
115
118
  /** Map the model's verdict onto APPROVE/REQUEST_CHANGES instead of COMMENT. */
116
119
  readonly applyVerdict: boolean;
117
- /** Run-level usage bounds; defaults to `reviewBudgetLimits`. */
120
+ /** Run-level usage bounds; defaults to the shape's packaged limits. */
118
121
  readonly limits?: UsageBudgetLimits | undefined;
119
122
  /**
120
123
  * Host-side findings bound (fail-closed backstop for the instruction-level
@@ -132,15 +135,6 @@ export interface ExecuteReviewOptions {
132
135
  readonly modelLabel?: string | undefined;
133
136
  /** Workflow-run URL rendered into the review footer. */
134
137
  readonly runUrl?: string | undefined;
135
- /**
136
- * What the run budget observes: the whole run, or a fan-out coordinator
137
- * only. Without a declared scope the footer omits usage entirely — this
138
- * generic path cannot know what a caller's binding shape observes, and an
139
- * unlabeled number would read as whole-run totals.
140
- */
141
- readonly usageScope?: "run" | "coordinator" | undefined;
142
- /** Host-owned coverage shape; defaults to the flat reviewer. */
143
- readonly reviewShape?: ReviewShape | undefined;
144
138
  }
145
139
 
146
140
  /** Build the mission one review run frames from the source's snapshot. */
@@ -173,109 +167,43 @@ export const enforceFindingsBound = (review: CodeReview, maxFindings: number): C
173
167
  const findingKey = (finding: ReviewFinding): string =>
174
168
  `${finding.path}\u0000${finding.startLine}\u0000${finding.endLine}\u0000${finding.severity}\u0000${finding.title}`;
175
169
 
176
- const severityRank: Record<ReviewConcern["severity"], number> = {
177
- blocking: 0,
178
- important: 1,
179
- nit: 2,
180
- };
181
-
182
- const rankAndDedupeConcerns = (
183
- concerns: ReadonlyArray<ReviewConcern>,
184
- ): ReadonlyArray<ReviewConcern> => {
185
- const byContent = new Map<string, ReviewConcern>();
186
- for (const concern of concerns) {
187
- const key = `${concern.title}\u0000${concern.body}`;
188
- const previous = byContent.get(key);
189
- if (
190
- previous === undefined ||
191
- severityRank[concern.severity] < severityRank[previous.severity]
192
- ) {
193
- byContent.set(key, concern);
194
- }
195
- }
196
- return [...byContent.values()]
197
- .sort((left, right) => severityRank[left.severity] - severityRank[right.severity])
198
- .slice(0, 10);
199
- };
170
+ /** One shape-specific review result, before the shared settlement tail. */
171
+ interface ReviewCore {
172
+ readonly review: CodeReview;
173
+ readonly inputCoverage: ReviewInputCoverage;
174
+ readonly assurance: ReviewAssurance;
175
+ readonly unreviewedPaths: ReadonlyArray<string>;
176
+ readonly unreviewedPasses?: ReadonlyArray<StoredUnreviewedPass> | undefined;
177
+ readonly turns: number;
178
+ }
200
179
 
201
180
  /**
202
- * Execute one review with any explicit Agent Binding whose contract is
203
- * `ReviewMission -> CodeReview` the flat reviewer or the fan-out
204
- * coordinator; the toolkit stays generic because publication only depends on
205
- * the shared output contract. The binding stays a parameter (D-027): tests
206
- * pass scripted models, hosts pass live provider bindings, and the model
207
- * Layer's requirements stay visible in this Effect's `R`.
181
+ * The shared settlement tail: carry unchanged prior scope, decide whether
182
+ * this run's continuity state can be signed, plan the exact publication, and
183
+ * (optionally) post it. Continuity requires only that the run COMPLETED with
184
+ * a trustworthy full-surface fingerprint never that every pass settled;
185
+ * unsettled scope travels inside the state instead of freezing it.
208
186
  */
209
- export const executeReview = <
210
- Instructions,
211
- Tools extends Record<string, Tool.Any>,
212
- Provider,
213
- ModelProvides,
214
- ModelRequires,
215
- >(
216
- binding: RuntimeBinding<
217
- typeof ReviewMission,
218
- typeof CodeReview,
219
- Instructions,
220
- Tools,
221
- Provider,
222
- ModelProvides,
223
- ModelRequires
224
- >,
187
+ const settleReviewRun = (
188
+ core: ReviewCore,
189
+ context: {
190
+ readonly metadata: PullRequestMetadata;
191
+ readonly files: ReadonlyArray<ChangedFile>;
192
+ readonly anchorFiles: ReadonlyArray<ChangedFile>;
193
+ readonly fingerprint: string | undefined;
194
+ readonly usage: UsageTotals | undefined;
195
+ },
225
196
  options: ExecuteReviewOptions,
226
197
  ) =>
227
198
  Effect.gen(function* () {
228
- const source = yield* PullRequestSource;
229
- const metadata = yield* source.metadata;
230
- const files = yield* source.changedFiles;
231
- const anchorFiles = yield* source.anchorFiles;
199
+ const { metadata, files, anchorFiles, fingerprint, usage } = context;
232
200
  const executionContext = Option.getOrUndefined(
233
201
  yield* Effect.serviceOption(ReviewExecutionContext),
234
202
  );
235
- const mission = buildReviewMission(metadata, files);
236
- const fullMission = buildReviewMission(metadata, anchorFiles);
237
- const fingerprint =
238
- options.signature === undefined
239
- ? undefined
240
- : yield* computeChangesetFingerprint(anchorFiles, options.signature(fullMission));
241
-
242
- const budget = yield* makeUsageBudget(options.limits ?? reviewBudgetLimits);
243
- const detached = yield* AgentRuntime.start(binding, mission, {
244
- budget: toRunBudgetHook(budget),
245
- estimateCostMicrousd: () => Effect.succeed(500),
246
- });
247
- const result = yield* detached.await;
248
- const events = yield* detached.events;
249
-
250
- // The engine validated the terminal JSON against the output schema; this
251
- // decode recovers the typed value on this side of the generic boundary.
252
- const decoded = yield* Schema.decodeUnknownEffect(CodeReview)(result.output);
203
+ const review = enforceFindingsBound(core.review, clampMaxFindings(options.maxFindings));
204
+ const { inputCoverage, assurance } = core;
205
+ const unreviewedPaths = [...new Set(core.unreviewedPaths)].sort();
253
206
  const reviewTotalFiles = executionContext?.totalFiles ?? metadata.totalChangedFiles;
254
- const pipeline = assessReviewPipeline({
255
- shape: options.reviewShape ?? "flat",
256
- files,
257
- totalFiles: reviewTotalFiles,
258
- anchorFiles,
259
- totalAnchorFiles: metadata.totalChangedFiles,
260
- events,
261
- });
262
- // The coordinator owns prose only. Fan-out findings and concerns are
263
- // reconstructed from exact verifier-confirmed discovery candidates; an
264
- // unsupported or coordinator-invented candidate cannot reach publication.
265
- const verifiedReview =
266
- options.reviewShape !== "fan-out"
267
- ? decoded
268
- : CodeReview.make({
269
- summary: decoded.summary,
270
- verdict: decoded.verdict,
271
- findings: rankAndDedupeFindings(pipeline.confirmedFindings),
272
- ...(pipeline.confirmedConcerns.length === 0
273
- ? {}
274
- : { concerns: rankAndDedupeConcerns(pipeline.confirmedConcerns) }),
275
- ...(pipeline.walkthrough.length === 0 ? {} : { walkthrough: pipeline.walkthrough }),
276
- });
277
- const review = enforceFindingsBound(verifiedReview, clampMaxFindings(options.maxFindings));
278
- const usage = yield* budget.snapshot;
279
207
  const affectedPaths = new Set(
280
208
  executionContext?.affectedPaths ??
281
209
  files.flatMap((file) =>
@@ -315,16 +243,25 @@ export const executeReview = <
315
243
  const key = `${concern.title}\u0000${concern.body}`;
316
244
  return activeConcernKeys.has(key) && !currentConcernKeys.has(key);
317
245
  });
318
- const { assurance, coverage, inputCoverage } = pipeline;
246
+ const settled =
247
+ inputCoverage.status === "complete" &&
248
+ assurance.status !== "incomplete" &&
249
+ unreviewedPaths.length === 0;
250
+ // The fingerprint marker is standalone skip authority for fingerprint-only
251
+ // harnesses, so it is embedded only for a fully settled run.
252
+ const skipFingerprint = settled ? fingerprint : undefined;
253
+ const carriedScopeFits = unreviewedPaths.length <= MAX_STORED_UNREVIEWED_PATHS;
319
254
  const stateCandidate =
320
255
  executionContext !== undefined &&
321
- inputCoverage.status === "complete" &&
322
- assurance.status === "settled" &&
323
256
  fingerprint !== undefined &&
324
257
  metadata.baseSha !== undefined &&
258
+ // The fingerprint and stored baseline describe the FULL pull-request
259
+ // surface; a truncated anchor surface cannot make either claim.
260
+ anchorFiles.length >= metadata.totalChangedFiles &&
261
+ carriedScopeFits &&
325
262
  executionContext.stateAuthenticator?.status === "available"
326
263
  ? ReviewState.make({
327
- version: 1,
264
+ version: 2,
328
265
  repository: metadata.repository,
329
266
  pullRequestNumber: metadata.number,
330
267
  baseRef: metadata.baseRef,
@@ -336,6 +273,9 @@ export const executeReview = <
336
273
  reviewedPathCount: anchorFiles.length,
337
274
  unresolvedFindings: activeFindings.map(toStoredFinding),
338
275
  unresolvedConcerns: activeConcerns.map(toStoredConcern),
276
+ unreviewedPaths,
277
+ unreviewedPasses: (core.unreviewedPasses ?? []).slice(0, MAX_STORED_UNREVIEWED_PASSES),
278
+ settled,
339
279
  lastReviewMode: executionContext.mode,
340
280
  })
341
281
  : undefined;
@@ -345,12 +285,12 @@ export const executeReview = <
345
285
  state: undefined,
346
286
  marker: undefined,
347
287
  notice:
348
- executionContext?.stateAuthenticator?.status === "unavailable" &&
349
- inputCoverage.status === "complete" &&
350
- assurance.status === "settled"
351
- ? (executionContext.stateAuthenticator.unavailableReason ??
352
- "authenticated continuity state is unavailable")
353
- : undefined,
288
+ executionContext !== undefined && !carriedScopeFits
289
+ ? `carried unreviewed scope (${unreviewedPaths.length} paths) exceeded the ${MAX_STORED_UNREVIEWED_PATHS}-path continuity bound`
290
+ : executionContext?.stateAuthenticator?.status === "unavailable"
291
+ ? (executionContext.stateAuthenticator.unavailableReason ??
292
+ "authenticated continuity state is unavailable")
293
+ : undefined,
354
294
  }
355
295
  : yield* executionContext.stateAuthenticator.render(stateCandidate).pipe(
356
296
  Effect.match({
@@ -374,14 +314,10 @@ export const executeReview = <
374
314
  modelLabel: options.modelLabel,
375
315
  runUrl: options.runUrl,
376
316
  usage,
377
- usageScope: options.usageScope,
378
- fingerprint:
379
- inputCoverage.status === "complete" && assurance.status === "settled"
380
- ? fingerprint
381
- : undefined,
382
- coverage,
317
+ fingerprint: skipFingerprint,
383
318
  inputCoverage,
384
319
  assurance,
320
+ unreviewedPaths,
385
321
  carriedFindings,
386
322
  carriedConcerns,
387
323
  reviewMode: executionContext?.mode,
@@ -392,44 +328,165 @@ export const executeReview = <
392
328
  stateMarker: continuity.marker,
393
329
  stateNotice: continuity.notice,
394
330
  });
395
-
396
- const scope =
397
- options.usageScope === undefined ? {} : ({ usageScope: options.usageScope } as const);
398
- if (!options.post) {
399
- return ReviewRunOutcome.make({
400
- review,
401
- activeFindings,
402
- activeConcerns,
403
- coverage,
404
- inputCoverage,
405
- assurance,
406
- plan,
407
- turns: result.turns,
408
- usage,
409
- ...scope,
410
- ...(executionContext === undefined
411
- ? {}
412
- : { reviewMode: executionContext.mode, reviewReason: executionContext.reason }),
413
- ...(continuity.state === undefined ? {} : { state: continuity.state }),
414
- });
415
- }
416
- const publisher = yield* ReviewPublisher;
417
- const published = yield* publisher.publish(plan);
418
- return ReviewRunOutcome.make({
331
+ const shared = {
419
332
  review,
420
333
  activeFindings,
421
334
  activeConcerns,
422
- coverage,
335
+ coverage: compatibilityCoverage(inputCoverage, assurance),
423
336
  inputCoverage,
424
337
  assurance,
338
+ unreviewedPaths,
425
339
  plan,
426
- published,
427
- turns: result.turns,
428
- usage,
429
- ...scope,
340
+ turns: core.turns,
341
+ ...(usage === undefined ? {} : { usage }),
430
342
  ...(executionContext === undefined
431
343
  ? {}
432
344
  : { reviewMode: executionContext.mode, reviewReason: executionContext.reason }),
433
345
  ...(continuity.state === undefined ? {} : { state: continuity.state }),
346
+ };
347
+ if (!options.post) return ReviewRunOutcome.make(shared);
348
+ const publisher = yield* ReviewPublisher;
349
+ const published = yield* publisher.publish(plan);
350
+ return ReviewRunOutcome.make({ ...shared, published });
351
+ });
352
+
353
+ /**
354
+ * Execute one flat review with any explicit Agent Binding whose contract is
355
+ * `ReviewMission -> CodeReview`. The binding stays a parameter (D-027): tests
356
+ * pass scripted models, hosts pass live provider bindings, and the model
357
+ * Layer's requirements stay visible in this Effect's `R`.
358
+ */
359
+ export const executeReview = <
360
+ Instructions,
361
+ Tools extends Record<string, Tool.Any>,
362
+ Provider,
363
+ ModelProvides,
364
+ ModelRequires,
365
+ >(
366
+ binding: RuntimeBinding<
367
+ typeof ReviewMission,
368
+ typeof CodeReview,
369
+ Instructions,
370
+ Tools,
371
+ Provider,
372
+ ModelProvides,
373
+ ModelRequires
374
+ >,
375
+ options: ExecuteReviewOptions,
376
+ ) =>
377
+ Effect.gen(function* () {
378
+ const source = yield* PullRequestSource;
379
+ const metadata = yield* source.metadata;
380
+ const files = yield* source.changedFiles;
381
+ const anchorFiles = yield* source.anchorFiles;
382
+ const executionContext = Option.getOrUndefined(
383
+ yield* Effect.serviceOption(ReviewExecutionContext),
384
+ );
385
+ const mission = buildReviewMission(metadata, files);
386
+ const fullMission = buildReviewMission(metadata, anchorFiles);
387
+ const fingerprint =
388
+ options.signature === undefined
389
+ ? undefined
390
+ : yield* computeChangesetFingerprint(anchorFiles, options.signature(fullMission));
391
+
392
+ const budget = yield* makeUsageBudget(options.limits ?? reviewBudgetLimits);
393
+ const detached = yield* AgentRuntime.start(binding, mission, {
394
+ budget: toRunBudgetHook(budget),
395
+ estimateCostMicrousd: () => Effect.succeed(500),
396
+ });
397
+ const result = yield* detached.await;
398
+ const events = yield* detached.events;
399
+
400
+ // The engine validated the terminal JSON against the output schema; this
401
+ // decode recovers the typed value on this side of the generic boundary.
402
+ const review = yield* Schema.decodeUnknownEffect(CodeReview)(result.output);
403
+ const assessment = assessFlatReview({
404
+ files,
405
+ totalFiles: executionContext?.totalFiles ?? metadata.totalChangedFiles,
406
+ anchorFiles,
407
+ totalAnchorFiles: metadata.totalChangedFiles,
408
+ events,
434
409
  });
410
+ const usage = yield* budget.snapshot;
411
+ return yield* settleReviewRun(
412
+ {
413
+ review,
414
+ inputCoverage: assessment.inputCoverage,
415
+ assurance: assessment.assurance,
416
+ unreviewedPaths: assessment.unreviewedPaths,
417
+ turns: result.turns,
418
+ },
419
+ { metadata, files, anchorFiles, fingerprint, usage },
420
+ options,
421
+ );
422
+ });
423
+
424
+ /**
425
+ * Execute one host-scheduled fan-out review: deterministic planning,
426
+ * independent discovery and verification child passes with bounded retries,
427
+ * and a host-composed review from verifier-confirmed candidates only. One
428
+ * budget observes every child pass, so the reported usage is whole-run.
429
+ */
430
+ export const executeFanOutReview = <Provider, ModelProvides, ModelRequires>(
431
+ binding: FileReviewerBinding<Provider, ModelProvides, ModelRequires>,
432
+ options: ExecuteReviewOptions,
433
+ ) =>
434
+ Effect.gen(function* () {
435
+ const source = yield* PullRequestSource;
436
+ const metadata = yield* source.metadata;
437
+ const files = yield* source.changedFiles;
438
+ const anchorFiles = yield* source.anchorFiles;
439
+ const executionContext = Option.getOrUndefined(
440
+ yield* Effect.serviceOption(ReviewExecutionContext),
441
+ );
442
+ const fullMission = buildReviewMission(metadata, anchorFiles);
443
+ const fingerprint =
444
+ options.signature === undefined
445
+ ? undefined
446
+ : yield* computeChangesetFingerprint(anchorFiles, options.signature(fullMission));
447
+
448
+ const budget = yield* makeUsageBudget(options.limits ?? fanOutReviewBudgetLimits);
449
+ const totalFiles = executionContext?.totalFiles ?? metadata.totalChangedFiles;
450
+ const pipeline = yield* runFanOutReview(binding, {
451
+ files,
452
+ anchorFiles,
453
+ totalChangedFiles: totalFiles,
454
+ maxFindings: options.maxFindings,
455
+ budget: toRunBudgetHook(budget),
456
+ ...(executionContext !== undefined && executionContext.retryPaths.length > 0
457
+ ? {
458
+ retry: {
459
+ paths: executionContext.retryPaths,
460
+ stages: executionContext.retryStages,
461
+ },
462
+ }
463
+ : {}),
464
+ });
465
+ const inputCoverage = fanOutInputCoverage({
466
+ plan: pipeline.plan,
467
+ files,
468
+ totalFiles,
469
+ anchorFiles,
470
+ totalAnchorFiles: metadata.totalChangedFiles,
471
+ });
472
+ const usage = yield* budget.snapshot;
473
+ return yield* settleReviewRun(
474
+ {
475
+ review: pipeline.review,
476
+ inputCoverage,
477
+ assurance: pipeline.assurance,
478
+ unreviewedPaths: pipeline.unreviewedPaths,
479
+ unreviewedPasses: pipeline.unreviewedPasses
480
+ .slice(0, MAX_STORED_UNREVIEWED_PASSES)
481
+ .map((pass) =>
482
+ StoredUnreviewedPass.make({
483
+ stage: pass.stage,
484
+ paths: pass.paths.slice(0, 12),
485
+ }),
486
+ ),
487
+ turns: pipeline.turns,
488
+ },
489
+ { metadata, files, anchorFiles, fingerprint, usage },
490
+ options,
491
+ );
435
492
  });
@@ -1,6 +1,6 @@
1
1
  import { Context, Effect, Schema } from "effect";
2
2
 
3
- import { ChangedFile } from "./diff.ts";
3
+ import type { ChangedFile } from "./diff.ts";
4
4
 
5
5
  // ---------------------------------------------------------------------------
6
6
  // The pull-request source port: everything the review tools may observe about