@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 { 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";
1
+ import { $n as ReviewFinding, At as buildPriorReviewContext, Ct as anchorViolation, Dn as toStoredConcern, On as toStoredFinding, Pn as CodeReview, Qn as ReviewConcern, Rt as renderAdjudicationContextLine, St as splitCarriedScope, Vn as ListChangedFiles, W as makeFileReviewerDefinition, Xn as ReadFile, Zn as ReadFileDiff, _ as computeProfileFingerprint, _n as buildProfileMission, _r as PullRequestMetadata, _t as ReviewInputCoverage, ar as clampMaxFindings, bn as fromStoredConcern, br as ReviewInputViolation, bt as fanOutInputCoverage, d as gitHubReviewAdjudicationHostLayer, dn as StoredAdjudication, er as ReviewMission, f as gitHubReviewPublisherLayer, ft as rankAndDedupeConcerns, g as computeChangesetFingerprint, gn as adjudicationIdentity, gt as ReviewAssurance, jt as collectReviewAdjudications, l as gitHubPriorReviewsLayer, mn as StoredUnreviewedPass, mt as reviewConcernKey, n as GitHubApiFailure, nn as ReviewExecutionContext, nr as ReviewToolkitLayer, o as PublishedReview, on as ReviewState, or as defaultReviewPolicy, p as gitHubReviewRetirementHostLayer, pr as resolveGuidance, pt as rankAndDedupeFindings, q as runFanOutReview, r as GitHubReviewTarget, s as ReviewPublisher, t as DEFAULT_GITHUB_REVIEW_AUTHOR_LOGIN, u as gitHubPullRequestSourceLayer, ur as makeReviewInstructions, vn as concernIdentity, vr as PullRequestSource, vt as assessFlatReview, xn as fromStoredFinding, y as renderFingerprintMarker, yn as findingIdentity, zt as renderPriorFindingContextLine } from "./github-NjgxGqwM.mjs";
2
2
  import { Config, Context, DateTime, Effect, FileSystem, Layer, Option, Ref, Schema } from "effect";
3
3
  import { Agent, AgentPolicy, AgentRuntime, IdGenerator, UsageBudgetLimits, UsageTotals, getToolExecutionClass, makeUsageBudget, toRunBudgetHook } from "effect-agent";
4
4
  import { Toolkit } from "effect/unstable/ai";
@@ -211,20 +211,43 @@ const renderDemoted = (finding, reason) => {
211
211
  return `- ${`\`${finding.path}:${finding.startLine}${finding.endLine !== finding.startLine ? `-${finding.endLine}` : ""}\``} **[${findingLabel(finding)}] ${finding.title}** — ${finding.body} _(demoted: ${reason})_`;
212
212
  };
213
213
  const countNoun = (count, noun) => `${count} ${noun}${count === 1 ? "" : "s"}`;
214
- /** The validated finding + concern severities, tallied for callout and event. */
214
+ const tallySeverities = (items) => {
215
+ const blocking = items.filter((item) => item.severity === "blocking").length;
216
+ const important = items.filter((item) => item.severity === "important").length;
217
+ return {
218
+ blocking,
219
+ important,
220
+ nit: items.length - blocking - important,
221
+ total: items.length
222
+ };
223
+ };
224
+ /** The validated finding + concern severities, kept separate by provenance. */
215
225
  const severityCounts = (review, carriedFindings = [], carriedConcerns = []) => {
216
- const severities = [
217
- ...review.findings.map((finding) => finding.severity),
218
- ...(review.concerns ?? []).map((concern) => concern.severity),
219
- ...carriedFindings.map((finding) => finding.severity),
220
- ...carriedConcerns.map((concern) => concern.severity)
221
- ];
226
+ const findings = tallySeverities(review.findings);
227
+ const concerns = tallySeverities(review.concerns ?? []);
228
+ const priorFindings = tallySeverities(carriedFindings);
229
+ const priorConcerns = tallySeverities(carriedConcerns);
222
230
  return {
223
- blocking: severities.filter((severity) => severity === "blocking").length,
224
- important: severities.filter((severity) => severity === "important").length,
225
- total: severities.length
231
+ findings,
232
+ concerns,
233
+ carriedFindings: priorFindings,
234
+ carriedConcerns: priorConcerns,
235
+ total: {
236
+ blocking: findings.blocking + concerns.blocking + priorFindings.blocking + priorConcerns.blocking,
237
+ important: findings.important + concerns.important + priorFindings.important + priorConcerns.important,
238
+ nit: findings.nit + concerns.nit + priorFindings.nit + priorConcerns.nit,
239
+ total: findings.total + concerns.total + priorFindings.total + priorConcerns.total
240
+ }
226
241
  };
227
242
  };
243
+ const joinItemCounts = (items) => items.length <= 1 ? items[0] ?? "none" : items.length === 2 ? `${items[0]} and ${items[1]}` : `${items.slice(0, -1).join(", ")}, and ${items.at(-1)}`;
244
+ const severityItemParts = (counts, severity) => [
245
+ ...counts.findings[severity] === 0 ? [] : [countNoun(counts.findings[severity], `${severity} finding`)],
246
+ ...counts.concerns[severity] === 0 ? [] : [countNoun(counts.concerns[severity], `${severity} concern`)],
247
+ ...counts.carriedFindings[severity] === 0 ? [] : [countNoun(counts.carriedFindings[severity], `carried ${severity} finding`)],
248
+ ...counts.carriedConcerns[severity] === 0 ? [] : [countNoun(counts.carriedConcerns[severity], `carried ${severity} concern`)]
249
+ ];
250
+ const renderSeverityItems = (counts, severity) => joinItemCounts(severityItemParts(counts, severity));
228
251
  /**
229
252
  * The opening callout: the review's overall tier, derived HOST-SIDE from the
230
253
  * validated severities (never from model prose), described by what GitHub
@@ -233,20 +256,38 @@ const severityCounts = (review, carriedFindings = [], carriedConcerns = []) => {
233
256
  */
234
257
  const renderVerdictCallout = (review, options) => {
235
258
  const counts = severityCounts(review, options.carriedFindings, options.carriedConcerns);
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.`;
239
- if (counts.important > 0) return `> [!IMPORTANT]\n> ${countNoun(counts.important, "important finding")} to address before merging.`;
240
- if (counts.total > 0) return "> ℹ️ Minor suggestions onlymergeable as-is.";
241
- return review.verdict === "approve" ? "> No issues found." : "> ℹ️ No findings see the summary.";
259
+ if (counts.total.blocking > 0) return `> [!CAUTION]\n> ${renderSeverityItems(counts, "blocking")}. Do not merge before addressing ${counts.total.blocking === 1 ? "it" : "them"}.`;
260
+ if (options.inputCoverage?.status === "incomplete" || options.assurance?.status === "incomplete") {
261
+ const scope = splitCarriedScope(options);
262
+ const undiffableCount = scope.undiffablePaths.length;
263
+ const undiffableNote = undiffableCount > 0 ? ` ${countNoun(undiffableCount, "unreviewable file")} (binary or oversized) ${undiffableCount === 1 ? "has" : "have"} no diff a retry could settle remove ${undiffableCount === 1 ? "it" : "them"} from the pull request or exclude ${undiffableCount === 1 ? "it" : "them"} with ignore globs.` : "";
264
+ if (!scope.retryableGap) return `> [!WARNING]\n>${undiffableNote} The check reports "incomplete" while ${undiffableCount === 1 ? "it remains" : "they remain"} part of the pull request.`;
265
+ const retryableCount = scope.retryablePaths.length;
266
+ return `> [!WARNING]\n> Review infrastructure did not settle. This is a reviewer-side gap, NOT a request to change code.${retryableCount > 0 ? ` ${countNoun(retryableCount, "affected path")} ${retryableCount === 1 ? "is" : "are"} carried forward and retried automatically on the next run.` : ""}${undiffableNote} The check reports "incomplete" until a run settles.`;
267
+ }
268
+ if (counts.total.important > 0) return `> [!IMPORTANT]\n> ${renderSeverityItems(counts, "important")} to address before merging.`;
269
+ if (counts.total.total > 0) return `> ℹ️ ${renderSeverityItems(counts, "nit")}; mergeable as-is.`;
270
+ return review.verdict === "approve" ? "> ✅ No issues found." : "> ℹ️ No review items. See the summary.";
242
271
  };
243
272
  const renderConcern = (concern) => [
244
273
  `### ${severityEmoji[concern.severity]} ${concern.title}`,
274
+ ...concern.evidencePaths === void 0 ? [] : ["", `_Affected paths: ${concern.evidencePaths.map((path) => `\`${path}\``).join(", ")}_`],
245
275
  "",
246
276
  concern.body
247
277
  ].join("\n");
248
278
  const renderCarriedFinding = (finding) => `- \`${finding.path}:${finding.startLine}${finding.endLine === finding.startLine ? "" : `-${finding.endLine}`}\` **[${findingLabel(finding)}] ${finding.title}** — ${finding.body}`;
249
279
  /**
280
+ * One adjudicated identity: its location (absent for unanchored concerns),
281
+ * title, maintainer disposition, actor, and the optional reason. Rendered in
282
+ * the collapsed adjudicated section so the audit distinguishes fixed,
283
+ * adjudicated, and still-open items.
284
+ */
285
+ const renderAdjudicated = (adjudication) => {
286
+ const location = adjudication.path !== void 0 && adjudication.startLine !== void 0 && adjudication.endLine !== void 0 ? `\`${adjudication.path}:${adjudication.startLine}${adjudication.endLine === adjudication.startLine ? "" : `-${adjudication.endLine}`}\` ` : "";
287
+ const reason = adjudication.reason === void 0 ? "" : `: ${adjudication.reason}`;
288
+ return `- ${location}${adjudication.title} — ${adjudication.disposition} by @${adjudication.actor}${reason}`;
289
+ };
290
+ /**
250
291
  * Validate the model's walkthrough against the real changeset: entries whose
251
292
  * path is not a changed file are dropped (the walkthrough analogue of anchor
252
293
  * validation), duplicates keep the first entry, and the result is ordered by
@@ -299,14 +340,13 @@ const renderReviewStats = (files, totalChangedFiles, counts) => {
299
340
  const additions = files.reduce((total, file) => total + file.additions, 0);
300
341
  const deletions = files.reduce((total, file) => total + file.deletions, 0);
301
342
  const fileCount = files.length < totalChangedFiles ? `${files.length} of ${totalChangedFiles} files` : countNoun(files.length, "file");
302
- const nits = counts.total - counts.blocking - counts.important;
303
- const tally = counts.total === 0 ? "none" : [
304
- ...counts.blocking > 0 ? [`${counts.blocking} blocking`] : [],
305
- ...counts.important > 0 ? [`${counts.important} important`] : [],
306
- ...nits > 0 ? [`${nits} nit`] : []
307
- ].join(", ");
343
+ const tally = counts.total.total === 0 ? "none" : joinItemCounts([
344
+ "blocking",
345
+ "important",
346
+ "nit"
347
+ ].flatMap((severity) => severityItemParts(counts, severity)));
308
348
  const effort = estimateReviewEffort(files);
309
- return `**Changeset:** ${fileCount} (+${additions} / −${deletions}) · **Findings:** ${tally} · **Review effort:** ${effort.score}/5 (${effort.label})`;
349
+ return `**Changeset:** ${fileCount} (+${additions} / −${deletions}) · **Review items:** ${tally} · **Review effort:** ${effort.score}/5 (${effort.label})`;
310
350
  };
311
351
  /** HTML comments must not contain `--`; interpolated values are sanitized. */
312
352
  const commentSafe = (value) => value.replaceAll("--", "- -");
@@ -368,7 +408,7 @@ const planPublication = (review, files, options) => {
368
408
  writtenAtSha: options.baselineSha
369
409
  }))];
370
410
  const consolidatedPromptWanted = promptEntries.length >= 2 || demoted.length > 0;
371
- const renderHead = (concernsKept, demotedKept, omitted, walkthroughKept, promptsKept) => {
411
+ const renderHead = (concernsKept, demotedKept, omitted, walkthroughKept, promptsKept, adjudicationsKept) => {
372
412
  const carriedFindings = options.carriedFindings ?? [];
373
413
  const carriedConcerns = options.carriedConcerns ?? [];
374
414
  const parts = [renderVerdictCallout(review, {
@@ -389,10 +429,19 @@ const planPublication = (review, files, options) => {
389
429
  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}`));
390
430
  if (carriedFindings.length > 0) parts.push("", "<details>", `<summary>Unresolved findings carried from unchanged scope (${carriedFindings.length})</summary>`, "", ...carriedFindings.map(renderCarriedFinding), "", "</details>");
391
431
  if (carriedConcerns.length > 0) {
392
- parts.push("", "### Unresolved concerns carried to the final audit");
432
+ parts.push("", `### Unresolved concerns from unchanged paths (${carriedConcerns.length})`, "", "These concerns were reported in an earlier review and were not reverified in this incremental pass. They remain active because none of their affected paths changed.");
393
433
  for (const concern of carriedConcerns) parts.push("", renderConcern(concern));
394
434
  }
395
435
  for (const concern of sortedConcerns.slice(0, concernsKept)) parts.push("", renderConcern(concern));
436
+ const adjudications = options.adjudications ?? [];
437
+ if (adjudications.length > 0) parts.push("", adjudicationsKept ? [
438
+ "<details>",
439
+ `<summary>Adjudicated (${adjudications.length})</summary>`,
440
+ "",
441
+ ...adjudications.map(renderAdjudicated),
442
+ "",
443
+ "</details>"
444
+ ].join("\n") : "⚠️ Adjudicated section omitted — the body exceeded GitHub's review size cap.");
396
445
  if (files.length < options.totalChangedFiles) parts.push("", `⚠️ Input exposed ${files.length} of ${options.totalChangedFiles} changed files — the changeset exceeded the reviewer's file bound.`);
397
446
  if (demotedKept > 0) parts.push("", "<details>", `<summary>Findings without a valid diff anchor (${demotedKept})</summary>`, "", ...sortedDemoted.slice(0, demotedKept).map(({ finding, reason }) => renderDemoted(finding, reason)), "", "</details>");
398
447
  if (consolidatedPromptWanted) parts.push("", promptsKept ? renderConsolidatedAgentPrompt(promptEntries) : "⚠️ Consolidated agent prompt omitted — the body exceeded GitHub's review size cap.");
@@ -402,7 +451,7 @@ const planPublication = (review, files, options) => {
402
451
  };
403
452
  const counts = severityCounts(review, options.carriedFindings ?? [], options.carriedConcerns ?? []);
404
453
  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";
454
+ const event = !options.applyVerdict ? "COMMENT" : counts.total.blocking > 0 ? "REQUEST_CHANGES" : review.verdict === "approve" && counts.total.important === 0 && !unclean ? "APPROVE" : "COMMENT";
406
455
  const tail = [
407
456
  renderReviewMetadata({
408
457
  headSha: options.headSha,
@@ -417,15 +466,18 @@ const planPublication = (review, files, options) => {
417
466
  ...options.stateMarker === void 0 ? [] : [options.stateMarker]
418
467
  ].join("\n");
419
468
  const headBudget = 6e4 - tail.length - 1;
469
+ const adjudicationCount = options.adjudications?.length ?? 0;
420
470
  let concernsKept = sortedConcerns.length;
421
471
  let demotedKept = sortedDemoted.length;
422
472
  let omitted = 0;
423
473
  let walkthroughKept = true;
424
474
  let promptsKept = true;
425
- let head = renderHead(concernsKept, demotedKept, omitted, walkthroughKept, promptsKept);
426
- while (head.length > headBudget && (promptsKept && consolidatedPromptWanted || walkthroughKept && walkthrough.length > 0 || demotedKept > 0 || concernsKept > 0)) {
475
+ let adjudicationsKept = true;
476
+ let head = renderHead(concernsKept, demotedKept, omitted, walkthroughKept, promptsKept, adjudicationsKept);
477
+ while (head.length > headBudget && (promptsKept && consolidatedPromptWanted || walkthroughKept && walkthrough.length > 0 || adjudicationsKept && adjudicationCount > 0 || demotedKept > 0 || concernsKept > 0)) {
427
478
  if (promptsKept && consolidatedPromptWanted) promptsKept = false;
428
479
  else if (walkthroughKept && walkthrough.length > 0) walkthroughKept = false;
480
+ else if (adjudicationsKept && adjudicationCount > 0) adjudicationsKept = false;
429
481
  else if (demotedKept > 0) {
430
482
  demotedKept -= 1;
431
483
  omitted += 1;
@@ -433,7 +485,7 @@ const planPublication = (review, files, options) => {
433
485
  concernsKept -= 1;
434
486
  omitted += 1;
435
487
  }
436
- head = renderHead(concernsKept, demotedKept, omitted, walkthroughKept, promptsKept);
488
+ head = renderHead(concernsKept, demotedKept, omitted, walkthroughKept, promptsKept, adjudicationsKept);
437
489
  }
438
490
  const body = `${head.slice(0, headBudget)}\n${tail}`;
439
491
  return ReviewPublicationPlan.make({
@@ -477,8 +529,6 @@ var ReviewRunOutcome = class extends Schema.Class("@effect-agent/pr-review/Revie
477
529
  activeFindings: Schema.Array(ReviewFinding).check(Schema.isMaxLength(20)),
478
530
  /** All currently unresolved concerns, including concerns carried to final audit. */
479
531
  activeConcerns: Schema.Array(ReviewConcern).check(Schema.isMaxLength(10)),
480
- /** Host-owned structural coverage used by the Actions check conclusion. */
481
- coverage: ReviewCoverage,
482
532
  /** Exact path/evidence assignment, distinct from semantic review work. */
483
533
  inputCoverage: ReviewInputCoverage,
484
534
  /** Settlement of scheduled discovery, specialist, and verification work. */
@@ -493,17 +543,26 @@ var ReviewRunOutcome = class extends Schema.Class("@effect-agent/pr-review/Revie
493
543
  usage: Schema.optionalKey(UsageTotals),
494
544
  reviewMode: Schema.optionalKey(Schema.Literals(["incremental", "full"])),
495
545
  reviewReason: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(1e3))),
496
- state: Schema.optionalKey(ReviewState)
546
+ state: Schema.optionalKey(ReviewState),
547
+ /** Maintainer adjudications standing against this run's identities. */
548
+ adjudications: Schema.optionalKey(Schema.Array(StoredAdjudication).check(Schema.isMaxLength(20)))
497
549
  }) {};
498
- /** Build the mission one review run frames from the source's snapshot. */
499
- const buildReviewMission = (metadata, files) => ReviewMission.make({
550
+ /**
551
+ * Build the mission one review run frames from the source's snapshot. The
552
+ * optional continuity context (adjudicated identities, prior-round findings
553
+ * on re-reviewed scope) reaches only RUN missions — fingerprint missions stay
554
+ * plain so an adjudication never invalidates skip-unchanged authority.
555
+ */
556
+ const buildReviewMission = (metadata, files, context) => ReviewMission.make({
500
557
  repository: metadata.repository,
501
558
  number: metadata.number,
502
559
  title: metadata.title,
503
560
  body: metadata.body,
504
561
  baseRef: metadata.baseRef,
505
562
  headRef: metadata.headRef,
506
- changedFileCount: files.length
563
+ changedFileCount: files.length,
564
+ ...context?.adjudicated !== void 0 && context.adjudicated.length > 0 ? { adjudicatedContext: context.adjudicated.slice(0, 20) } : {},
565
+ ...context?.priorFindings !== void 0 && context.priorFindings.length > 0 ? { priorFindingContext: context.priorFindings.slice(0, 20) } : {}
507
566
  });
508
567
  /** Enforce the configured findings bound on an already-validated review. */
509
568
  const enforceFindingsBound = (review, maxFindings) => review.findings.length <= maxFindings ? review : CodeReview.make({
@@ -515,6 +574,24 @@ const enforceFindingsBound = (review, maxFindings) => review.findings.length <=
515
574
  });
516
575
  const findingKey = (finding) => `${finding.path}\u0000${finding.startLine}\u0000${finding.endLine}\u0000${finding.severity}\u0000${finding.title}`;
517
576
  /**
577
+ * Continuity inputs resolved BEFORE any model work: the standing maintainer
578
+ * adjudications (fresh host listing merged later-wins over the prior state's
579
+ * stored set) and the prior-round findings whose paths this run re-reviews.
580
+ * The latter are dropped from the carry (the new round re-decides them) but
581
+ * injected as prompt context so successive rounds do not silently contradict
582
+ * each other — context ONLY, never auto-carried into active findings.
583
+ */
584
+ const resolveReviewContinuityContext = Effect.fn("resolveReviewContinuityContext")(function* () {
585
+ const executionContext = yield* ReviewExecutionContext;
586
+ const priorState = executionContext.mode === "incremental" ? executionContext.priorState : void 0;
587
+ const adjudications = yield* collectReviewAdjudications(priorState?.adjudications ?? []);
588
+ const affectedPaths = new Set(executionContext.affectedPaths);
589
+ return {
590
+ adjudications,
591
+ priorFindingsOnScope: priorState?.unresolvedFindings.filter((finding) => affectedPaths.has(finding.path)) ?? []
592
+ };
593
+ });
594
+ /**
518
595
  * The shared settlement tail: carry unchanged prior scope, decide whether
519
596
  * this run's continuity state can be signed, plan the exact publication, and
520
597
  * (optionally) post it. Continuity requires only that the run COMPLETED with
@@ -523,31 +600,58 @@ const findingKey = (finding) => `${finding.path}\u0000${finding.startLine}\u0000
523
600
  */
524
601
  const settleReviewRun = (core, context, options) => Effect.gen(function* () {
525
602
  const { metadata, files, anchorFiles, fingerprint, usage } = context;
526
- const executionContext = Option.getOrUndefined(yield* Effect.serviceOption(ReviewExecutionContext));
527
- const review = enforceFindingsBound(core.review, clampMaxFindings(options.maxFindings));
603
+ const executionContext = yield* ReviewExecutionContext;
604
+ const adjudications = context.adjudications ?? [];
605
+ const adjudicatedIdentities = new Set(adjudications.map(adjudicationIdentity));
606
+ const isAdjudicatedFinding = (finding) => adjudicatedIdentities.has(findingIdentity(finding));
607
+ const isAdjudicatedConcern = (concern) => adjudicatedIdentities.has(concernIdentity(concern));
608
+ const filteredReview = adjudicatedIdentities.size === 0 ? core.review : CodeReview.make({
609
+ summary: core.review.summary,
610
+ verdict: core.review.verdict,
611
+ findings: core.review.findings.filter((finding) => !isAdjudicatedFinding(finding)),
612
+ ...core.review.concerns === void 0 ? {} : { concerns: core.review.concerns.filter((concern) => !isAdjudicatedConcern(concern)) },
613
+ ...core.review.walkthrough === void 0 ? {} : { walkthrough: core.review.walkthrough }
614
+ });
615
+ const reviewPaths = new Set(files.map((file) => file.path));
616
+ const normalizedReview = CodeReview.make({
617
+ ...filteredReview,
618
+ ...filteredReview.concerns === void 0 ? {} : { concerns: filteredReview.concerns.map((concern) => {
619
+ const evidencePaths = concern.evidencePaths;
620
+ if (evidencePaths === void 0 || evidencePaths.some((path) => !reviewPaths.has(path))) {
621
+ const { evidencePaths: _invalid, ...pathless } = concern;
622
+ return ReviewConcern.make(pathless);
623
+ }
624
+ return ReviewConcern.make({
625
+ ...concern,
626
+ evidencePaths: [...new Set(evidencePaths)].sort()
627
+ });
628
+ }) }
629
+ });
630
+ const review = enforceFindingsBound(normalizedReview, clampMaxFindings(options.maxFindings));
528
631
  const { inputCoverage, assurance } = core;
529
632
  const unreviewedPaths = [...new Set(core.unreviewedPaths)].sort();
530
- const reviewTotalFiles = executionContext?.totalFiles ?? metadata.totalChangedFiles;
531
- const affectedPaths = new Set(executionContext?.affectedPaths ?? files.flatMap((file) => file.previousPath === void 0 ? [file.path] : [file.path, file.previousPath]));
532
- const priorState = executionContext?.mode === "incremental" ? executionContext.priorState : void 0;
533
- const carriedCandidates = priorState?.unresolvedFindings.filter((finding) => !affectedPaths.has(finding.path)).map(fromStoredFinding) ?? [];
534
- const activeFindings = rankAndDedupeFindings([...carriedCandidates, ...review.findings]).slice(0, clampMaxFindings(options.maxFindings));
633
+ const reviewTotalFiles = executionContext.totalFiles;
634
+ const affectedPaths = new Set(executionContext.affectedPaths);
635
+ const priorState = executionContext.mode === "incremental" ? executionContext.priorState : void 0;
636
+ const eligibleCarriedCandidates = (priorState?.unresolvedFindings.filter((finding) => !affectedPaths.has(finding.path)).map(fromStoredFinding) ?? []).filter((finding) => !isAdjudicatedFinding(finding));
637
+ const activeFindings = rankAndDedupeFindings([...eligibleCarriedCandidates, ...review.findings.filter((finding) => !isAdjudicatedFinding(finding))]).slice(0, clampMaxFindings(options.maxFindings));
535
638
  const activeFindingKeys = new Set(activeFindings.map(findingKey));
536
639
  const currentFindingKeys = new Set(review.findings.map(findingKey));
537
- const carriedFindings = carriedCandidates.filter((finding) => activeFindingKeys.has(findingKey(finding)) && !currentFindingKeys.has(findingKey(finding)));
538
- const carriedConcernCandidates = priorState?.unresolvedConcerns.map(fromStoredConcern) ?? [];
539
- const activeConcerns = rankAndDedupeConcerns([...carriedConcernCandidates, ...review.concerns ?? []]);
540
- const currentConcernKeys = new Set((review.concerns ?? []).map((concern) => `${concern.title}\u0000${concern.body}`));
541
- const activeConcernKeys = new Set(activeConcerns.map((concern) => `${concern.title}\u0000${concern.body}`));
542
- const carriedConcerns = carriedConcernCandidates.filter((concern) => {
543
- const key = `${concern.title}\u0000${concern.body}`;
640
+ const carriedFindings = eligibleCarriedCandidates.filter((finding) => activeFindingKeys.has(findingKey(finding)) && !currentFindingKeys.has(findingKey(finding)));
641
+ const eligibleCarriedConcernCandidates = (priorState?.unresolvedConcerns.filter((concern) => concern.evidencePaths !== void 0 && concern.evidencePaths.every((path) => !affectedPaths.has(path))).map(fromStoredConcern) ?? []).filter((concern) => !isAdjudicatedConcern(concern));
642
+ const activeConcerns = rankAndDedupeConcerns([...eligibleCarriedConcernCandidates, ...(review.concerns ?? []).filter((concern) => !isAdjudicatedConcern(concern))]);
643
+ const currentConcernKeys = new Set((review.concerns ?? []).map(reviewConcernKey));
644
+ const activeConcernKeys = new Set(activeConcerns.map(reviewConcernKey));
645
+ const carriedConcerns = eligibleCarriedConcernCandidates.filter((concern) => {
646
+ const key = reviewConcernKey(concern);
544
647
  return activeConcernKeys.has(key) && !currentConcernKeys.has(key);
545
648
  });
546
649
  const settled = inputCoverage.status === "complete" && assurance.status !== "incomplete" && unreviewedPaths.length === 0;
547
- const skipFingerprint = settled ? fingerprint : void 0;
650
+ const concernsHaveEvidencePaths = activeConcerns.every((concern) => concern.evidencePaths !== void 0);
651
+ const skipFingerprint = settled && concernsHaveEvidencePaths ? fingerprint : void 0;
548
652
  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,
653
+ const stateCandidate = fingerprint !== void 0 && executionContext.profileFingerprint !== void 0 && metadata.baseSha !== void 0 && anchorFiles.length >= metadata.totalChangedFiles && carriedScopeFits && concernsHaveEvidencePaths && executionContext.stateAuthenticator?.status === "available" ? ReviewState.make({
654
+ version: 1,
551
655
  repository: metadata.repository,
552
656
  pullRequestNumber: metadata.number,
553
657
  baseRef: metadata.baseRef,
@@ -555,18 +659,20 @@ const settleReviewRun = (core, context, options) => Effect.gen(function* () {
555
659
  headRef: metadata.headRef,
556
660
  reviewedHeadSha: metadata.headSha,
557
661
  profileFingerprint: executionContext.profileFingerprint,
558
- acceptedScopeFingerprint: fingerprint,
662
+ settledScopeFingerprint: fingerprint,
559
663
  reviewedPathCount: anchorFiles.length,
560
664
  unresolvedFindings: activeFindings.map(toStoredFinding),
561
665
  unresolvedConcerns: activeConcerns.map(toStoredConcern),
562
666
  unreviewedPaths,
667
+ unreviewedPasses: (core.unreviewedPasses ?? []).slice(0, 24),
563
668
  settled,
564
- lastReviewMode: executionContext.mode
669
+ lastReviewMode: executionContext.mode,
670
+ ...adjudications.length === 0 ? {} : { adjudications }
565
671
  }) : void 0;
566
- const continuity = stateCandidate === void 0 || executionContext?.stateAuthenticator === void 0 ? {
672
+ const continuity = stateCandidate === void 0 || executionContext.stateAuthenticator === void 0 ? {
567
673
  state: void 0,
568
674
  marker: 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
675
+ notice: !carriedScopeFits ? `carried unreviewed scope (${unreviewedPaths.length} paths) exceeded the 100-path continuity bound` : !concernsHaveEvidencePaths ? "one or more review concerns lacked host-validated affected paths" : executionContext.stateAuthenticator?.status === "unavailable" ? executionContext.stateAuthenticator.unavailableReason ?? "authenticated continuity state is unavailable" : void 0
570
676
  } : yield* executionContext.stateAuthenticator.render(stateCandidate).pipe(Effect.match({
571
677
  onFailure: (error) => ({
572
678
  state: void 0,
@@ -594,30 +700,29 @@ const settleReviewRun = (core, context, options) => Effect.gen(function* () {
594
700
  unreviewedPaths,
595
701
  carriedFindings,
596
702
  carriedConcerns,
597
- reviewMode: executionContext?.mode,
598
- reviewReason: executionContext?.reason,
599
- baselineSha: executionContext?.baselineSha,
703
+ reviewMode: executionContext.mode,
704
+ reviewReason: executionContext.reason,
705
+ baselineSha: executionContext.baselineSha,
600
706
  reviewFilesVisible: files.length,
601
707
  reviewTotalFiles,
602
708
  stateMarker: continuity.marker,
603
- stateNotice: continuity.notice
709
+ stateNotice: continuity.notice,
710
+ ...adjudications.length === 0 ? {} : { adjudications }
604
711
  });
605
712
  const shared = {
606
713
  review,
607
714
  activeFindings,
608
715
  activeConcerns,
609
- coverage: compatibilityCoverage(inputCoverage, assurance),
610
716
  inputCoverage,
611
717
  assurance,
612
718
  unreviewedPaths,
613
719
  plan,
614
720
  turns: core.turns,
615
721
  ...usage === void 0 ? {} : { usage },
616
- ...executionContext === void 0 ? {} : {
617
- reviewMode: executionContext.mode,
618
- reviewReason: executionContext.reason
619
- },
620
- ...continuity.state === void 0 ? {} : { state: continuity.state }
722
+ reviewMode: executionContext.mode,
723
+ reviewReason: executionContext.reason,
724
+ ...continuity.state === void 0 ? {} : { state: continuity.state },
725
+ ...adjudications.length === 0 ? {} : { adjudications }
621
726
  };
622
727
  if (!options.post) return ReviewRunOutcome.make(shared);
623
728
  const published = yield* (yield* ReviewPublisher).publish(plan);
@@ -637,8 +742,12 @@ const executeReview = (binding, options) => Effect.gen(function* () {
637
742
  const metadata = yield* source.metadata;
638
743
  const files = yield* source.changedFiles;
639
744
  const anchorFiles = yield* source.anchorFiles;
640
- const executionContext = Option.getOrUndefined(yield* Effect.serviceOption(ReviewExecutionContext));
641
- const mission = buildReviewMission(metadata, files);
745
+ const executionContext = yield* ReviewExecutionContext;
746
+ const continuity = yield* resolveReviewContinuityContext();
747
+ const mission = buildReviewMission(metadata, files, {
748
+ adjudicated: continuity.adjudications.map(renderAdjudicationContextLine),
749
+ priorFindings: continuity.priorFindingsOnScope.map(renderPriorFindingContextLine)
750
+ });
642
751
  const fullMission = buildReviewMission(metadata, anchorFiles);
643
752
  const fingerprint = options.signature === void 0 ? void 0 : yield* computeChangesetFingerprint(anchorFiles, options.signature(fullMission));
644
753
  const budget = yield* makeUsageBudget(options.limits ?? reviewBudgetLimits);
@@ -651,7 +760,7 @@ const executeReview = (binding, options) => Effect.gen(function* () {
651
760
  const review = yield* Schema.decodeUnknownEffect(CodeReview)(result.output);
652
761
  const assessment = assessFlatReview({
653
762
  files,
654
- totalFiles: executionContext?.totalFiles ?? metadata.totalChangedFiles,
763
+ totalFiles: executionContext.totalFiles,
655
764
  anchorFiles,
656
765
  totalAnchorFiles: metadata.totalChangedFiles,
657
766
  events
@@ -668,7 +777,8 @@ const executeReview = (binding, options) => Effect.gen(function* () {
668
777
  files,
669
778
  anchorFiles,
670
779
  fingerprint,
671
- usage
780
+ usage,
781
+ adjudications: continuity.adjudications
672
782
  }, options);
673
783
  });
674
784
  /**
@@ -682,17 +792,23 @@ const executeFanOutReview = (binding, options) => Effect.gen(function* () {
682
792
  const metadata = yield* source.metadata;
683
793
  const files = yield* source.changedFiles;
684
794
  const anchorFiles = yield* source.anchorFiles;
685
- const executionContext = Option.getOrUndefined(yield* Effect.serviceOption(ReviewExecutionContext));
795
+ const executionContext = yield* ReviewExecutionContext;
686
796
  const fullMission = buildReviewMission(metadata, anchorFiles);
687
797
  const fingerprint = options.signature === void 0 ? void 0 : yield* computeChangesetFingerprint(anchorFiles, options.signature(fullMission));
688
798
  const budget = yield* makeUsageBudget(options.limits ?? fanOutReviewBudgetLimits);
689
- const totalFiles = executionContext?.totalFiles ?? metadata.totalChangedFiles;
799
+ const totalFiles = executionContext.totalFiles;
800
+ const continuity = yield* resolveReviewContinuityContext();
690
801
  const pipeline = yield* runFanOutReview(binding, {
691
802
  files,
692
803
  anchorFiles,
693
804
  totalChangedFiles: totalFiles,
694
805
  maxFindings: options.maxFindings,
695
- budget: toRunBudgetHook(budget)
806
+ budget: toRunBudgetHook(budget),
807
+ ...executionContext.retryPaths.length > 0 ? { retry: {
808
+ paths: executionContext.retryPaths,
809
+ stages: executionContext.retryStages
810
+ } } : {},
811
+ ...continuity.adjudications.length > 0 || continuity.priorFindingsOnScope.length > 0 ? { priorContext: buildPriorReviewContext(continuity.adjudications, continuity.priorFindingsOnScope) } : {}
696
812
  });
697
813
  const inputCoverage = fanOutInputCoverage({
698
814
  plan: pipeline.plan,
@@ -707,13 +823,18 @@ const executeFanOutReview = (binding, options) => Effect.gen(function* () {
707
823
  inputCoverage,
708
824
  assurance: pipeline.assurance,
709
825
  unreviewedPaths: pipeline.unreviewedPaths,
826
+ unreviewedPasses: pipeline.unreviewedPasses.slice(0, 24).map((pass) => StoredUnreviewedPass.make({
827
+ stage: pass.stage,
828
+ paths: pass.paths.slice(0, 12)
829
+ })),
710
830
  turns: pipeline.turns
711
831
  }, {
712
832
  metadata,
713
833
  files,
714
834
  anchorFiles,
715
835
  fingerprint,
716
- usage
836
+ usage,
837
+ adjudications: continuity.adjudications
717
838
  }, options);
718
839
  });
719
840
  //#endregion
@@ -754,8 +875,8 @@ const makeReviewSnapshot = (ignore) => provideIgnore(Effect.gen(function* () {
754
875
  * Build the flat reviewer: one bounded read-only agent over the whole
755
876
  * changeset. Returns the model-agnostic definition, the explicit binding, and
756
877
  * a `run` whose error and requirement channels stay fully inferred — the
757
- * pull-request source, the publisher, extra tool handlers, and the Model
758
- * Layer's requirements all remain visible to the caller.
878
+ * pull-request source, the publisher, `Crypto.Crypto`, extra tool handlers,
879
+ * and the Model Layer's requirements all remain visible to the caller.
759
880
  */
760
881
  const make = (options) => {
761
882
  const extraTools = options.extraTools ?? EMPTY_TOOLS;
@@ -924,7 +1045,7 @@ const settleCallout = (info) => {
924
1045
  if (info.outcome === "failed") return "> ⚠️ **Code review run failed** — nothing was posted.";
925
1046
  switch (info.conclusion) {
926
1047
  case "success": return `> ✅ **Code review posted** — verdict \`${info.verdict}\`, ${info.inlineComments} inline comment(s), nothing blocking.`;
927
- case "blocking": return `> 🛑 **Code review posted** blocking findings; the check fails until they are addressed.`;
1048
+ case "blocking": return `> 🛑 **Code review posted:** blocking review items; the check fails until they are addressed.`;
928
1049
  case "incomplete": return `> ⚠️ **Code review posted** — input coverage or configured review assurance is incomplete, so the check fails.`;
929
1050
  }
930
1051
  };
@@ -1139,7 +1260,8 @@ const gitHubReviewLayers = (target) => Layer.unwrap(Effect.gen(function* () {
1139
1260
  token,
1140
1261
  reviewAuthorLogin
1141
1262
  });
1142
- return Layer.mergeAll(gitHubPullRequestSourceLayer.pipe(Layer.provide(targetLayer)), gitHubReviewPublisherLayer.pipe(Layer.provide(targetLayer)), gitHubPriorReviewsLayer.pipe(Layer.provide(targetLayer)), gitHubReviewRetirementHostLayer.pipe(Layer.provide(targetLayer)), gitHubReviewProgressLayer.pipe(Layer.provide(targetLayer)));
1263
+ const adjudicationHostLayer = gitHubReviewAdjudicationHostLayer.pipe(Layer.provide(targetLayer));
1264
+ return Layer.mergeAll(gitHubPullRequestSourceLayer.pipe(Layer.provide(targetLayer)), gitHubReviewPublisherLayer.pipe(Layer.provide(targetLayer)), gitHubPriorReviewsLayer.pipe(Layer.provide(targetLayer)), gitHubReviewRetirementHostLayer.pipe(Layer.provide(targetLayer)), adjudicationHostLayer, gitHubReviewProgressLayer.pipe(Layer.provide(targetLayer)));
1143
1265
  }));
1144
1266
  //#endregion
1145
1267
  //#region src/internal/providers.ts
@@ -1201,4 +1323,4 @@ const anthropicClientLayer = AnthropicClient.layerConfig({ apiKey: Config.redact
1201
1323
  //#endregion
1202
1324
  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 };
1203
1325
 
1204
- //# sourceMappingURL=providers-NyP-4rS6.mjs.map
1326
+ //# sourceMappingURL=providers-CODZQCmL.mjs.map