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

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.
@@ -169,6 +169,18 @@ const annotatePatch = (patch) => {
169
169
  return output.join("\n");
170
170
  };
171
171
  //#endregion
172
+ //#region src/internal/anchors.ts
173
+ /** Why a finding cannot anchor to the current new-version diff, if any. */
174
+ const anchorViolation = (finding, files) => {
175
+ const file = files.find((candidate) => candidate.path === finding.path);
176
+ if (file === void 0) return "path is not part of the changeset";
177
+ if (file.patch === void 0) return "file has no anchorable textual diff";
178
+ if (finding.endLine < finding.startLine) return "endLine precedes startLine";
179
+ if (finding.endLine - finding.startLine + 1 > 100) return "range is implausibly large";
180
+ const anchors = commentableLines(file.patch);
181
+ for (let line = finding.startLine; line <= finding.endLine; line += 1) if (!anchors.has(line)) return `line ${line} is not part of the diff`;
182
+ };
183
+ //#endregion
172
184
  //#region src/internal/source.ts
173
185
  /** Reading a file head version larger than this is refused, never truncated silently. */
174
186
  const MAX_FILE_CHARS = 2e5;
@@ -235,7 +247,7 @@ var PullRequestSource = class extends Context.Service()("@effect-agent/pr-review
235
247
  const MAX_FINDINGS = 20;
236
248
  /** The hard non-anchored-concerns bound carried by the CodeReview schema. */
237
249
  const MAX_CONCERNS = 10;
238
- /** Annotated patches larger than this are truncated with an explicit marker. */
250
+ /** Maximum characters in one deterministic model-visible evidence chunk. */
239
251
  const MAX_PATCH_CHARS = 6e4;
240
252
  /** The encoded Tool result must retain one complete bounded content fallback. */
241
253
  const REVIEW_TOOL_RESULT_MAX_BYTES = 2 * 1024 * 1024;
@@ -287,6 +299,53 @@ var FileDiffView = class extends Schema.Class("@effect-agent/pr-review/FileDiffV
287
299
  annotatedPatch: Schema.String,
288
300
  truncated: Schema.Boolean
289
301
  }) {};
302
+ /**
303
+ * Split complete model-visible evidence at deterministic line boundaries.
304
+ * A pathological single line is hard-sliced so every character is still
305
+ * assigned and every chunk remains within the provider-independent bound.
306
+ */
307
+ const boundedEvidenceChunks = (evidence) => {
308
+ if (evidence.length <= 6e4) return [evidence];
309
+ const chunks = [];
310
+ let offset = 0;
311
+ while (offset < evidence.length) {
312
+ let end = Math.min(offset + MAX_PATCH_CHARS, evidence.length);
313
+ if (end < evidence.length) {
314
+ const boundary = evidence.lastIndexOf("\n", end - 1);
315
+ if (boundary >= offset) end = boundary + 1;
316
+ }
317
+ if (end === offset) end = Math.min(offset + MAX_PATCH_CHARS, evidence.length);
318
+ chunks.push(evidence.slice(offset, end));
319
+ offset = end;
320
+ }
321
+ return chunks;
322
+ };
323
+ /** Complete bounded evidence chunks used by deterministic fan-out planning. */
324
+ const fileReviewEvidenceChunks = (file) => {
325
+ const contentEvidence = renderReviewContent(file);
326
+ const reviewMode = file.patch !== void 0 ? "diff" : contentEvidence !== void 0 ? "content" : "unavailable";
327
+ const annotated = file.patch === void 0 ? contentEvidence ?? "" : annotatePatch(file.patch);
328
+ return boundedEvidenceChunks(annotated).map((annotatedPatch) => ({
329
+ reviewMode,
330
+ annotatedPatch
331
+ }));
332
+ };
333
+ /** Host-owned rendering of one changed file's bounded review evidence. */
334
+ const fileDiffView = (file) => {
335
+ const chunks = fileReviewEvidenceChunks(file);
336
+ const first = chunks[0] ?? {
337
+ reviewMode: "unavailable",
338
+ annotatedPatch: ""
339
+ };
340
+ const truncated = first.reviewMode === "diff" && chunks.length > 1;
341
+ return FileDiffView.make({
342
+ path: file.path,
343
+ status: file.status,
344
+ reviewMode: first.reviewMode,
345
+ annotatedPatch: truncated ? `${first.annotatedPatch}\n[diff truncated]` : first.annotatedPatch,
346
+ truncated
347
+ });
348
+ };
290
349
  const ReadFileDiff = Tool.make("read_file_diff", {
291
350
  description: "Read one changed file's review evidence. A normal unified diff marks valid anchors as R<number>. When GitHub omitted the diff, bounded base/head content is returned with B/H line labels for review but no valid inline anchors.",
292
351
  parameters: FileDiffQuery,
@@ -352,17 +411,7 @@ const readFileDiffHandler = (query) => Effect.gen(function* () {
352
411
  input: relative,
353
412
  reason: "Path is not part of this pull request's changeset."
354
413
  });
355
- const contentEvidence = renderReviewContent(file);
356
- const reviewMode = file.patch !== void 0 ? "diff" : contentEvidence !== void 0 ? "content" : "unavailable";
357
- const annotated = file.patch === void 0 ? contentEvidence ?? "" : annotatePatch(file.patch);
358
- const truncated = reviewMode === "diff" && annotated.length > MAX_PATCH_CHARS;
359
- return FileDiffView.make({
360
- path: file.path,
361
- status: file.status,
362
- reviewMode,
363
- annotatedPatch: truncated ? `${annotated.slice(0, MAX_PATCH_CHARS)}\n[diff truncated]` : annotated,
364
- truncated
365
- });
414
+ return fileDiffView(file);
366
415
  });
367
416
  /**
368
417
  * The `read_file` handler, shared verbatim by the flat reviewer's toolkit
@@ -434,7 +483,7 @@ var ReviewFinding = class extends Schema.Class("@effect-agent/pr-review/ReviewFi
434
483
  title: Schema.NonEmptyString.check(Schema.isMaxLength(120)),
435
484
  body: Schema.NonEmptyString.check(Schema.isMaxLength(2e3)),
436
485
  /** Replacement for exactly lines startLine..endLine; omit when unsure. */
437
- suggestion: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(2e3)))
486
+ suggestion: Schema.optionalKey(Schema.String.annotate({ description: "Committable replacement source code for exactly lines startLine..endLine: the full replacement for every line in the range and nothing else — never prose describing the change, which belongs in body." }).check(Schema.isMaxLength(2e3)))
438
487
  }) {};
439
488
  const ReviewVerdict = Schema.Literals([
440
489
  "approve",
@@ -489,15 +538,15 @@ const makeReviewInstructions = (options = {}) => (mission) => {
489
538
  mission.body.length > 0 ? `Author description:\n${mission.body}` : "The author provided no description.",
490
539
  ...resolveGuidance(options.guidance, mission),
491
540
  "Work in this order:",
492
- "1. Call list_changed_files once to see the changeset. That list is your COMPLETE review scope: in incremental reviews it is deliberately a subset of the pull request's full diff (totalFiles counts the whole pull request), and everything it omits was already reviewed or excluded.",
493
- "2. Call read_file_diff for every file you review. A normal diff marks new-version anchors as R<number>; only those numbers are valid startLine/endLine values. When GitHub omitted a diff, the tool may return bounded base/head content marked B/H instead. Review that content, but report its defects as non-anchored concerns because B/H lines cannot anchor GitHub comments. Never anchor a finding to a removed (-), B, or H line.",
541
+ "1. Call list_changed_files once to see the selected input scope. In incremental reviews it is deliberately a subset of the pull request's full diff (totalFiles counts the whole pull request); omitted paths belong to settled prior scope or explicit host exclusions, not to this run.",
542
+ "2. Call read_file_diff for every listed file. A normal diff marks new-version anchors as R<number>; only those numbers are valid startLine/endLine values. When GitHub omitted a diff, the tool may return bounded base/head content marked B/H instead. Review that content, but report its defects as non-anchored concerns because B/H lines cannot anchor GitHub comments. Never anchor a finding to a removed (-), B, or H line.",
494
543
  "3. Call read_file when you need surrounding context the diff does not show. ONLY listed files are readable — read_file_diff and read_file both return a failed result for any other path (an import, a neighbor, a file named in the description). Do not request or retry unlisted paths; reason from the visible diffs instead and note the gap honestly in your summary when it matters.",
495
544
  "4. Review for real defects first: correctness, security, concurrency, resource leaks, error handling, API misuse. Style nits are least important. Do not praise; do not restate the diff.",
496
545
  "When the diff adds or changes a test, check that it can actually fail: a test that would still pass with the bug present is theatre, not coverage. The usual tell is a loose assertion standing where an exact one belongs — >= or a truthiness check over an expected value, or a snapshot that absorbs whatever it is handed.",
497
546
  "Go shallow only when the diff has no behavioral surface at all: doc typos, formatting, lockfile or generated-code regeneration, a mechanical rename. Line count is not the signal — a one-line change to auth, money, SQL, a comparison operator, or a config default is not trivial.",
498
547
  "Drop bloat-shaped findings before reporting: defensive checks for cases that cannot happen, abstractions used once, comments restating obvious code, tests asserting tautologies, just-in-case guards. A finding must be sound, correct, and worth acting on; prefer an explicit keep over an invented finding.",
499
548
  "5. After collecting anchored findings, deliberately scan for concerns with NO line to point at: deletion or cleanup plans for code the diff replaces, rollout or migration sequencing, coverage gaps the diff implies but does not add, scope questions only the author can answer. Report each as a \"concern\", never as a finding with an invented anchor; report none when none exist.",
500
- `6. Write a walkthrough: for every file you reviewed, one factual sentence (<= 240 chars) describing what changed in that file — written for a reader scanning the pull request, never restating the diff line by line. Use only paths from list_changed_files; invented paths are dropped.`,
549
+ `6. Write a walkthrough: for every file whose evidence you examined, one factual sentence (<= 240 chars) describing what changed in that file — written for a reader scanning the pull request, never restating the diff line by line. Use only paths from list_changed_files; invented paths are dropped.`,
501
550
  "7. Then return ONLY a JSON object — no Markdown fences, no prose before or after — exactly this shape: {\"summary\": <string, 1-3 paragraphs of overall assessment>, \"verdict\": <\"approve\" | \"comment\" | \"request-changes\">, \"findings\": [{\"path\": <string, a changed file path>, \"startLine\": <integer, an R-marked new-file line>, \"endLine\": <integer, >= startLine, same file, R-marked>, \"severity\": <\"blocking\" | \"important\" | \"nit\">, \"category\": <OPTIONAL: \"correctness\" | \"security\" | \"concurrency\" | \"performance\" | \"resources\" | \"error-handling\" | \"testing\" | \"maintainability\" | \"style\" | \"docs\">, \"title\": <string, <= 120 chars>, \"body\": <string, why it matters and what to do>, \"suggestion\": <string, OPTIONAL: replacement text for exactly lines startLine..endLine, ready to commit>}], \"concerns\": <array, OPTIONAL: [{\"severity\": <\"blocking\" | \"important\" | \"nit\">, \"title\": <string, <= 120 chars>, \"body\": <string>}], only for step-5 concerns with no valid anchor — never duplicate a finding here>, \"walkthrough\": <array, OPTIONAL: [{\"path\": <string, a changed file path>, \"summary\": <string, the step-6 sentence>}], one entry per reviewed file>}.",
502
551
  `Report at most ${maxFindings} findings and at most 10 concerns; prefer the most important ones. An empty findings array with verdict "approve" is a valid review. Include "suggestion" only when you are confident the replacement compiles and preserves intent; its text must contain the full replacement for every line in the range and nothing else.`,
503
552
  "Use verdict \"request-changes\" only when at least one finding or concern is \"blocking\". Line anchors you invent will be discarded, so copy R-numbers from read_file_diff output."
@@ -535,19 +584,70 @@ const PullRequestReviewer = Agent.define("pr-reviewer", {
535
584
  const MAX_REVIEW_UNITS = 8;
536
585
  /** A unit never carries more files than this, regardless of their size. */
537
586
  const MAX_UNIT_FILES = 12;
538
- /** Soft changed-line budget per unit; a single oversized file still gets its own unit. */
587
+ /** Compatibility export; complete evidence chars now own unit packing. */
539
588
  const UNIT_CHANGED_LINE_BUDGET = 800;
540
- /** Flat per-file cost so many tiny files still spread across units. */
541
- const FILE_OVERHEAD_LINES = 20;
589
+ /**
590
+ * Bound the complete model-visible evidence assigned to one child. This is a
591
+ * character bound rather than a token estimate because it is deterministic,
592
+ * provider-independent, and enforced before any model call.
593
+ */
594
+ const UNIT_EVIDENCE_CHAR_BUDGET = 24e4;
595
+ /** Maximum complete evidence shards placed in one child brief. */
596
+ const MAX_UNIT_EVIDENCE_SHARDS = 12;
597
+ /**
598
+ * Keep overflow diagnostics bounded to one plan's total assignment capacity.
599
+ * The plan separately records the exact overflow count and every affected
600
+ * path, so identifiers are a deterministic diagnostic sample rather than the
601
+ * authority for whether input coverage is complete.
602
+ */
603
+ const MAX_REPORTED_UNASSIGNED_EVIDENCE_SHARDS = 96;
604
+ /** @deprecated Use `MAX_PATCH_CHARS`; this is now the per-shard bound. */
605
+ const MAX_FILE_EVIDENCE_CHARS = MAX_PATCH_CHARS;
542
606
  /** The merged review never exceeds the `CodeReview` findings bound. */
543
607
  const MAX_MERGED_FINDINGS = 20;
544
608
  const ReviewUnitId = Schema.NonEmptyString.check(Schema.isMaxLength(32));
609
+ /** High-risk surfaces that receive an explicit specialist focus label. */
610
+ const ReviewRiskCategory = Schema.Literals([
611
+ "authentication-authorization",
612
+ "security-boundary",
613
+ "persistence-durability",
614
+ "concurrency",
615
+ "credential-handling",
616
+ "external-side-effects"
617
+ ]);
618
+ const ReviewDiscoveryPerspective = Schema.Literals(["general", "risk-specialist"]);
619
+ const ReviewPassId = Schema.NonEmptyString.check(Schema.isMaxLength(64));
620
+ const ReviewEvidenceShardId = Schema.NonEmptyString.check(Schema.isMaxLength(32));
621
+ /** One complete bounded slice of a changed path's model-visible evidence. */
622
+ var ReviewEvidenceShard = class extends Schema.Class("@effect-agent/pr-review/ReviewEvidenceShard")({
623
+ shardId: ReviewEvidenceShardId,
624
+ path: ChangedPath,
625
+ ordinal: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),
626
+ total: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),
627
+ evidenceChars: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(MAX_PATCH_CHARS))
628
+ }) {};
629
+ const EvidenceShardIds$1 = Schema.Array(ReviewEvidenceShardId).check(Schema.isMinLength(1)).check(Schema.isMaxLength(12));
630
+ /** One required, independently scoped discovery attempt. */
631
+ var ReviewDiscoveryPass = class extends Schema.Class("@effect-agent/pr-review/ReviewDiscoveryPass")({
632
+ passId: ReviewPassId,
633
+ unitId: ReviewUnitId,
634
+ paths: Schema.Array(ChangedPath).check(Schema.isMinLength(1)).check(Schema.isMaxLength(12)),
635
+ evidenceShardIds: EvidenceShardIds$1,
636
+ perspective: ReviewDiscoveryPerspective,
637
+ /** Empty for the general pass; explicit deterministic focus for specialists. */
638
+ riskCategories: Schema.Array(ReviewRiskCategory).check(Schema.isMaxLength(6))
639
+ }) {};
545
640
  /** One bounded slice of the changeset delegated to one child reviewer. */
546
641
  var ReviewUnit = class extends Schema.Class("@effect-agent/pr-review/ReviewUnit")({
547
642
  unitId: ReviewUnitId,
548
643
  paths: Schema.Array(ChangedPath).check(Schema.isMinLength(1)).check(Schema.isMaxLength(12)),
644
+ evidenceShards: Schema.Array(ReviewEvidenceShard).check(Schema.isMinLength(1)).check(Schema.isMaxLength(12)),
549
645
  /** additions + deletions across the unit's files, for honest sizing. */
550
- changedLines: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
646
+ changedLines: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
647
+ /** Complete model-visible diff/content evidence assigned to each child. */
648
+ evidenceChars: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(UNIT_EVIDENCE_CHAR_BUDGET)),
649
+ /** Host-classified focus labels for the unit's redundant specialist pass. */
650
+ riskCategories: Schema.Array(ReviewRiskCategory).check(Schema.isMaxLength(6))
551
651
  }) {};
552
652
  /** The complete deterministic fan-out plan over one changeset. */
553
653
  var ReviewUnitPlan = class extends Schema.Class("@effect-agent/pr-review/ReviewUnitPlan")({
@@ -555,8 +655,16 @@ var ReviewUnitPlan = class extends Schema.Class("@effect-agent/pr-review/ReviewU
555
655
  /** True when the source returned fewer files than the pull request has. */
556
656
  truncated: Schema.Boolean,
557
657
  units: Schema.Array(ReviewUnit).check(Schema.isMaxLength(8)),
658
+ /** Exact discovery calls the coordinator must make. */
659
+ discoveryPasses: Schema.Array(ReviewDiscoveryPass).check(Schema.isMaxLength(16)),
558
660
  /** Changed files with neither a textual diff nor bounded base/head text. */
559
661
  undiffablePaths: Schema.Array(ChangedPath).check(Schema.isMaxLength(300)),
662
+ /** Assigned paths with one or more evidence shards beyond plan capacity. */
663
+ partialEvidencePaths: Schema.Array(ChangedPath).check(Schema.isMaxLength(300)),
664
+ /** Exact number of shards beyond the bounded unit capacity. */
665
+ unassignedEvidenceShardCount: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
666
+ /** Bounded deterministic prefix of the unassigned shard identifiers. */
667
+ unassignedEvidenceShardIds: Schema.Array(ReviewEvidenceShardId).check(Schema.isMaxLength(96)),
560
668
  /**
561
669
  * Diffable files beyond the fan-out capacity (MAX_REVIEW_UNITS units of
562
670
  * MAX_UNIT_FILES files). Never silently dropped: the coordinator must name
@@ -564,62 +672,232 @@ var ReviewUnitPlan = class extends Schema.Class("@effect-agent/pr-review/ReviewU
564
672
  */
565
673
  unassignedPaths: Schema.Array(ChangedPath).check(Schema.isMaxLength(300))
566
674
  }) {};
567
- const fileCost = (file) => {
568
- const contentChars = (file.reviewBaseContent?.length ?? 0) + (file.reviewHeadContent?.length ?? 0);
569
- const contentWeight = Math.ceil(contentChars / 200);
570
- return file.additions + file.deletions + contentWeight + FILE_OVERHEAD_LINES;
675
+ const riskRules = [
676
+ {
677
+ category: "authentication-authorization",
678
+ patterns: [
679
+ /auth/,
680
+ /authoriz/,
681
+ /permission/,
682
+ /principal/,
683
+ /access[-_ ]?control/,
684
+ /role\b/
685
+ ]
686
+ },
687
+ {
688
+ category: "security-boundary",
689
+ patterns: [
690
+ /security/,
691
+ /sandbox/,
692
+ /untrusted/,
693
+ /schema\.decode/,
694
+ /validation/,
695
+ /injection/,
696
+ /csrf/,
697
+ /xss/,
698
+ /path traversal/
699
+ ]
700
+ },
701
+ {
702
+ category: "persistence-durability",
703
+ patterns: [
704
+ /durab/,
705
+ /persist/,
706
+ /storage/,
707
+ /database/,
708
+ /\bsql\b/,
709
+ /journal/,
710
+ /ledger/,
711
+ /checkpoint/,
712
+ /migration/,
713
+ /transaction/
714
+ ]
715
+ },
716
+ {
717
+ category: "concurrency",
718
+ patterns: [
719
+ /concurr/,
720
+ /semaphore/,
721
+ /\bfiber/,
722
+ /race/,
723
+ /mutex/,
724
+ /\block\b/,
725
+ /queue/,
726
+ /parallel/,
727
+ /interrupt/
728
+ ]
729
+ },
730
+ {
731
+ category: "credential-handling",
732
+ patterns: [
733
+ /credential/,
734
+ /secret/,
735
+ /password/,
736
+ /api[-_ ]?key/,
737
+ /bearer/,
738
+ /hmac/,
739
+ /signature/
740
+ ]
741
+ },
742
+ {
743
+ category: "external-side-effects",
744
+ patterns: [
745
+ /publish/,
746
+ /webhook/,
747
+ /github/,
748
+ /fetch\(/,
749
+ /http/,
750
+ /send[-_ ]?(email|message)/,
751
+ /write[-_ ]?(file|record)/,
752
+ /delete/,
753
+ /mutation/,
754
+ /side[-_ ]?effect/,
755
+ /spawn/,
756
+ /exec/
757
+ ]
758
+ }
759
+ ];
760
+ /**
761
+ * Deterministic host policy for specialist assignment. It intentionally
762
+ * favors false positives: an extra bounded pass costs work, while a missed
763
+ * high-risk classification removes redundancy. This is not a claim that the
764
+ * keyword policy recognizes every semantically risky change.
765
+ */
766
+ const classifyReviewRisks = (file) => {
767
+ const text = [
768
+ file.path,
769
+ file.previousPath ?? "",
770
+ file.patch ?? "",
771
+ file.reviewBaseContent ?? "",
772
+ file.reviewHeadContent ?? ""
773
+ ].join("\n").toLowerCase();
774
+ return riskRules.filter((rule) => rule.patterns.some((pattern) => pattern.test(text))).map((rule) => rule.category);
775
+ };
776
+ /**
777
+ * Whether every claimed finding anchor was present in the exact bounded
778
+ * evidence shards assigned to one unit. This is stricter than checking the
779
+ * full pull-request diff when an oversized path spans multiple units.
780
+ */
781
+ const findingAnchorInUnitEvidence = (finding, unit, files) => {
782
+ const file = files.find((candidate) => candidate.path === finding.path);
783
+ if (file?.patch === void 0 || finding.endLine < finding.startLine) return false;
784
+ const assignedOrdinals = new Set(unit.evidenceShards.filter((shard) => shard.path === finding.path).map((shard) => shard.ordinal));
785
+ const visibleLines = /* @__PURE__ */ new Set();
786
+ const chunks = fileReviewEvidenceChunks(file);
787
+ for (let index = 0; index < chunks.length; index += 1) {
788
+ if (!assignedOrdinals.has(index + 1)) continue;
789
+ for (const line of chunks[index]?.annotatedPatch.split("\n") ?? []) {
790
+ const match = /^R(\d+) /.exec(line);
791
+ if (match?.[1] !== void 0) visibleLines.add(Number(match[1]));
792
+ }
793
+ }
794
+ for (let line = finding.startLine; line <= finding.endLine; line += 1) if (!visibleLines.has(line)) return false;
795
+ return true;
571
796
  };
572
- const unitOf = (index, files) => ReviewUnit.make({
797
+ const uniquePaths = (shards) => [...new Set(shards.map(({ shard }) => shard.path))];
798
+ const plannedEvidenceShards = (files) => {
799
+ const planned = [];
800
+ let shardIndex = 0;
801
+ for (const file of files) {
802
+ const chunks = fileReviewEvidenceChunks(file);
803
+ for (let index = 0; index < chunks.length; index += 1) {
804
+ const chunk = chunks[index];
805
+ if (chunk === void 0) continue;
806
+ shardIndex += 1;
807
+ planned.push({
808
+ shard: ReviewEvidenceShard.make({
809
+ shardId: `shard-${String(shardIndex).padStart(4, "0")}`,
810
+ path: file.path,
811
+ ordinal: index + 1,
812
+ total: chunks.length,
813
+ evidenceChars: chunk.annotatedPatch.length
814
+ }),
815
+ file,
816
+ changedLines: index === 0 ? file.additions + file.deletions : 0
817
+ });
818
+ }
819
+ }
820
+ return planned;
821
+ };
822
+ const unitOf = (index, shards) => ReviewUnit.make({
573
823
  unitId: `unit-${String(index + 1).padStart(3, "0")}`,
574
- paths: files.map((file) => file.path),
575
- changedLines: files.reduce((total, file) => total + file.additions + file.deletions, 0)
824
+ paths: uniquePaths(shards),
825
+ evidenceShards: shards.map(({ shard }) => shard),
826
+ changedLines: shards.reduce((total, shard) => total + shard.changedLines, 0),
827
+ evidenceChars: shards.reduce((total, { shard }) => total + shard.evidenceChars, 0),
828
+ riskCategories: [...new Set(shards.flatMap(({ file }) => classifyReviewRisks(file)))]
576
829
  });
830
+ const discoveryPassesFor = (units) => units.flatMap((unit) => [ReviewDiscoveryPass.make({
831
+ passId: `${unit.unitId}-general`,
832
+ unitId: unit.unitId,
833
+ paths: unit.paths,
834
+ evidenceShardIds: unit.evidenceShards.map((shard) => shard.shardId),
835
+ perspective: "general",
836
+ riskCategories: []
837
+ }), ReviewDiscoveryPass.make({
838
+ passId: `${unit.unitId}-specialist`,
839
+ unitId: unit.unitId,
840
+ paths: unit.paths,
841
+ evidenceShardIds: unit.evidenceShards.map((shard) => shard.shardId),
842
+ perspective: "risk-specialist",
843
+ riskCategories: unit.riskCategories
844
+ })]);
577
845
  /**
578
846
  * Group the changeset into at most `MAX_REVIEW_UNITS` review units.
579
847
  *
580
848
  * Deterministic by construction: files are ordered by path (so files sharing
581
849
  * a directory become neighbors — directory affinity without a heuristic),
582
- * then packed greedily in that order under the soft changed-line budget and
583
- * the hard per-unit file bound. Capacity is finite and explicit:
850
+ * then split into complete line-bounded evidence shards and packed greedily
851
+ * under the hard evidence and per-unit shard bounds. Capacity is finite and
852
+ * explicit:
584
853
  *
585
854
  * - files without a textual diff are still delegated when the source
586
855
  * recovered complete bounded UTF-8 base/head content. Findings from that
587
856
  * evidence cannot anchor inline and are reported as concerns;
588
857
  * - files with neither form of textual evidence surface in
589
858
  * `undiffablePaths` instead of laundering missing coverage;
590
- * - reviewable files beyond `MAX_REVIEW_UNITS` full units surface in
591
- * `unassignedPaths` so the review can report them as unreviewed, never
592
- * silently truncated.
859
+ * - an oversized path spans as many deterministic shards and units as needed;
860
+ * - shards beyond `MAX_REVIEW_UNITS` full units surface explicitly, so a path
861
+ * is partial only when finite plan capacity is genuinely exhausted.
593
862
  */
594
863
  const planReviewUnits = (files, options) => {
595
864
  const ordered = [...files].sort((left, right) => left.path < right.path ? -1 : 1);
596
865
  const reviewable = ordered.filter(isReviewableFile);
597
866
  const undiffable = ordered.filter((file) => !isReviewableFile(file));
867
+ const shards = plannedEvidenceShards(reviewable);
598
868
  const groups = [];
599
869
  const unassigned = [];
600
870
  let current = [];
601
- let currentCost = 0;
602
- for (const file of reviewable) {
603
- const cost = fileCost(file);
604
- if (current.length >= 12 || current.length > 0 && currentCost + cost > 800) {
871
+ let currentEvidenceChars = 0;
872
+ for (const shard of shards) {
873
+ const nextPaths = /* @__PURE__ */ new Set([...uniquePaths(current), shard.shard.path]);
874
+ if (current.length >= 12 || nextPaths.size > 12 || current.length > 0 && currentEvidenceChars + shard.shard.evidenceChars > 24e4) {
605
875
  groups.push(current);
606
876
  current = [];
607
- currentCost = 0;
877
+ currentEvidenceChars = 0;
608
878
  }
609
879
  if (groups.length >= 8) {
610
- unassigned.push(file);
880
+ unassigned.push(shard);
611
881
  continue;
612
882
  }
613
- current.push(file);
614
- currentCost += cost;
883
+ current.push(shard);
884
+ currentEvidenceChars += shard.shard.evidenceChars;
615
885
  }
616
886
  if (current.length > 0 && groups.length < 8) groups.push(current);
887
+ const units = groups.map((group, index) => unitOf(index, group));
888
+ const assignedShardIds = new Set(units.flatMap((unit) => unit.evidenceShards.map((shard) => shard.shardId)));
889
+ const assignedPaths = new Set(shards.filter(({ shard }) => assignedShardIds.has(shard.shardId)).map(({ shard }) => shard.path));
890
+ const unassignedPathsWithEvidence = new Set(unassigned.map(({ shard }) => shard.path));
617
891
  return ReviewUnitPlan.make({
618
892
  totalFiles: files.length,
619
893
  truncated: files.length < options.totalChangedFiles,
620
- units: groups.map((group, index) => unitOf(index, group)),
894
+ units,
895
+ discoveryPasses: discoveryPassesFor(units),
621
896
  undiffablePaths: undiffable.map((file) => file.path),
622
- unassignedPaths: unassigned.map((file) => file.path)
897
+ partialEvidencePaths: [...unassignedPathsWithEvidence].filter((path) => assignedPaths.has(path)),
898
+ unassignedEvidenceShardCount: unassigned.length,
899
+ unassignedEvidenceShardIds: unassigned.slice(0, 96).map(({ shard }) => shard.shardId),
900
+ unassignedPaths: [...unassignedPathsWithEvidence].filter((path) => !assignedPaths.has(path))
623
901
  });
624
902
  };
625
903
  const severityRank = {
@@ -652,116 +930,340 @@ const rankAndDedupeFindings = (findings) => {
652
930
  };
653
931
  //#endregion
654
932
  //#region src/internal/fan-out.ts
655
- /** One child returns at most this many findings; the merge caps the total. */
656
- const MAX_CHILD_FINDINGS = 8;
657
- /** One child returns at most this many non-anchored concerns. */
933
+ /** One discovery pass returns at most this many anchored candidates. */
934
+ const MAX_CHILD_FINDINGS = 6;
935
+ /** One discovery pass returns at most this many non-anchored candidates. */
658
936
  const MAX_CHILD_CONCERNS = 3;
937
+ /** Every unit receives independent general and specialist discovery passes. */
938
+ const MAX_UNIT_CANDIDATES = 18;
939
+ /** General + specialist discovery for every unit, then one verifier per unit. */
940
+ const MAX_REVIEW_CHILDREN = 24;
941
+ /** Structural minimum for a child that exposes no tools. */
942
+ const MAX_FILE_REVIEW_TOOL_CALLS = 1;
943
+ const ReviewWorkPhase = Schema.Literals(["discovery", "verification"]);
944
+ const ReviewWorkPerspective = Schema.Literals([
945
+ "general",
946
+ "risk-specialist",
947
+ "candidate-verification"
948
+ ]);
949
+ const ReviewCandidateId = Schema.NonEmptyString.check(Schema.isMaxLength(96));
950
+ var FindingCandidate = class extends Schema.TaggedClass()("FindingCandidate", {
951
+ candidateId: ReviewCandidateId,
952
+ workId: ReviewPassId,
953
+ unitId: ReviewUnitId,
954
+ finding: ReviewFinding,
955
+ evidencePaths: Schema.Array(ChangedPath).check(Schema.isMinLength(1)).check(Schema.isMaxLength(1))
956
+ }) {};
957
+ var ConcernCandidate = class extends Schema.TaggedClass()("ConcernCandidate", {
958
+ candidateId: ReviewCandidateId,
959
+ workId: ReviewPassId,
960
+ unitId: ReviewUnitId,
961
+ concern: ReviewConcern,
962
+ evidencePaths: Schema.Array(ChangedPath).check(Schema.isMinLength(1)).check(Schema.isMaxLength(3))
963
+ }) {};
964
+ const ReviewCandidate = Schema.Union([FindingCandidate, ConcernCandidate]);
965
+ /** Deterministic host equivalence for claims repeated across discovery passes. */
966
+ const reviewCandidateSubjectKey = (candidate) => candidate._tag === "FindingCandidate" ? `finding:${JSON.stringify(Schema.encodeSync(ReviewFinding)(candidate.finding))}` : `concern:${JSON.stringify(Schema.encodeSync(ReviewConcern)(candidate.concern))}`;
967
+ var CandidateAssessment = class extends Schema.Class("@effect-agent/pr-review/CandidateAssessment")({
968
+ candidateId: ReviewCandidateId,
969
+ disposition: Schema.Literals(["confirmed", "rejected"]),
970
+ /**
971
+ * Exact suggestion settlement: required when the candidate finding carries
972
+ * a suggestion, forbidden otherwise. Untrusted child output cannot publish
973
+ * a GitHub replacement block by prompt compliance alone — the host keeps a
974
+ * confirmed finding's suggestion only on an exact "committable" settlement.
975
+ */
976
+ suggestion: Schema.optionalKey(Schema.Literals(["committable", "not-committable"]).annotate({ description: "Required exactly when the candidate finding carries a suggestion: \"committable\" only when its text is the full replacement source for exactly lines startLine..endLine and nothing else. Forbidden for candidates without a suggestion." })),
977
+ rationale: Schema.NonEmptyString.check(Schema.isMaxLength(600))
978
+ }) {};
659
979
  /**
660
- * One mandatory diff read plus one bounded context read for every path in a
661
- * maximum-size unit. Keep the child and delegation reservation aligned.
980
+ * Exact suggestion settlement shape: a carried suggestion must be settled and
981
+ * nothing else may be. Enforced identically by the live delegation projection
982
+ * and the independent host coverage fold.
662
983
  */
663
- const MAX_FILE_REVIEW_TOOL_CALLS = 24;
664
- const FileReviewToolkit = Toolkit.make(ReadFileDiff, ReadFile);
665
- const FileReviewToolkitLayer = FileReviewToolkit.toLayer({
666
- read_file_diff: readFileDiffHandler,
667
- read_file: readFileHandler
668
- });
984
+ const assessmentSettlesSuggestionExactly = (assessment, candidate) => candidate._tag === "FindingCandidate" && candidate.finding.suggestion !== void 0 ? assessment.suggestion !== void 0 : assessment.suggestion === void 0;
985
+ /**
986
+ * Fail-closed publication of a confirmed finding: only an exact "committable"
987
+ * settlement keeps the suggestion; anything else publishes the finding with
988
+ * the suggestion stripped so unverified text can never become a one-click
989
+ * GitHub replacement block.
990
+ */
991
+ const confirmedFindingForPublication = (assessment, candidate) => {
992
+ if (candidate.finding.suggestion === void 0 || assessment.suggestion === "committable") return candidate.finding;
993
+ const { suggestion: _stripped, ...finding } = candidate.finding;
994
+ return ReviewFinding.make(finding);
995
+ };
996
+ /**
997
+ * Concern candidates need explicit paths internally to bind the claim to
998
+ * scheduled evidence. The verifier receives the complete bounded unit so it
999
+ * can use neighboring evidence to falsify the claim. The public ReviewConcern
1000
+ * remains path-free after the host confirms and projects it.
1001
+ */
1002
+ var DiscoveredConcern = class extends Schema.Class("@effect-agent/pr-review/DiscoveredConcern")({
1003
+ concern: ReviewConcern,
1004
+ evidencePaths: Schema.Array(ChangedPath).check(Schema.isMinLength(1)).check(Schema.isMaxLength(3))
1005
+ }) {};
669
1006
  const UnitPaths = Schema.Array(ChangedPath).check(Schema.isMinLength(1)).check(Schema.isMaxLength(12));
670
- /** The child Agent input: one briefed unit of the changeset. */
1007
+ const RiskCategories = Schema.Array(ReviewRiskCategory).check(Schema.isMaxLength(6));
1008
+ const Candidates = Schema.Array(ReviewCandidate).check(Schema.isMaxLength(18));
1009
+ const EvidenceShardIds = Schema.Array(ReviewEvidenceShardId).check(Schema.isMinLength(1)).check(Schema.isMaxLength(12));
1010
+ /** Strict-object coordinator request for either discovery or verification. */
1011
+ var FileReviewRequest = class extends Schema.Class("@effect-agent/pr-review/FileReviewRequest")({
1012
+ phase: ReviewWorkPhase,
1013
+ workId: ReviewPassId,
1014
+ unitId: ReviewUnitId,
1015
+ paths: UnitPaths,
1016
+ evidenceShardIds: EvidenceShardIds,
1017
+ perspective: ReviewWorkPerspective,
1018
+ riskCategories: RiskCategories,
1019
+ /** Empty for discovery; the exact discovered set for unit verification. */
1020
+ candidates: Candidates
1021
+ }) {};
1022
+ /** One complete host-selected evidence shard supplied to a review child. */
1023
+ var FileReviewEvidence = class extends Schema.Class("@effect-agent/pr-review/FileReviewEvidence")({
1024
+ shardId: ReviewEvidenceShardId,
1025
+ path: ChangedPath,
1026
+ status: ChangedFileStatus,
1027
+ reviewMode: Schema.Literals([
1028
+ "diff",
1029
+ "content",
1030
+ "unavailable"
1031
+ ]),
1032
+ ordinal: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),
1033
+ total: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),
1034
+ annotatedPatch: Schema.String.check(Schema.isMaxLength(MAX_PATCH_CHARS))
1035
+ }) {};
1036
+ /** Host-prepared child input with complete bounded diff/content evidence. */
671
1037
  var FileReviewBrief = class extends Schema.Class("@effect-agent/pr-review/FileReviewBrief")({
1038
+ phase: ReviewWorkPhase,
1039
+ workId: ReviewPassId,
672
1040
  unitId: ReviewUnitId,
673
1041
  paths: UnitPaths,
674
- focus: Schema.NonEmptyString.check(Schema.isMaxLength(200))
1042
+ evidenceShardIds: EvidenceShardIds,
1043
+ perspective: ReviewWorkPerspective,
1044
+ riskCategories: RiskCategories,
1045
+ candidates: Candidates,
1046
+ evidence: Schema.Array(FileReviewEvidence).check(Schema.isMinLength(1)).check(Schema.isMaxLength(12))
675
1047
  }) {};
676
- /** The child Agent output: the briefed unit's bounded findings and concerns. */
1048
+ /** Child output; phase-inapplicable collections must be empty. */
677
1049
  var FileReviewReport = class extends Schema.Class("@effect-agent/pr-review/FileReviewReport")({
1050
+ phase: ReviewWorkPhase,
1051
+ workId: ReviewPassId,
678
1052
  unitId: ReviewUnitId,
679
- findings: Schema.Array(ReviewFinding).check(Schema.isMaxLength(8)),
680
- /** Unit-scoped concerns with no diff line to anchor to. */
681
- concerns: Schema.optionalKey(Schema.Array(ReviewConcern).check(Schema.isMaxLength(3))),
682
- /** One-sentence per-file change summaries for the merged walkthrough. */
683
- fileSummaries: Schema.optionalKey(Schema.Array(WalkthroughEntry).check(Schema.isMaxLength(12)))
1053
+ findings: Schema.Array(ReviewFinding).check(Schema.isMaxLength(6)),
1054
+ concerns: Schema.Array(DiscoveredConcern).check(Schema.isMaxLength(3)),
1055
+ fileSummaries: Schema.Array(WalkthroughEntry).check(Schema.isMaxLength(12)),
1056
+ assessments: Schema.Array(CandidateAssessment).check(Schema.isMaxLength(18))
684
1057
  }) {};
1058
+ /** Bounded coordinator-visible result with host-assigned candidate IDs. */
1059
+ var FileReviewUnitResult = class extends Schema.Class("@effect-agent/pr-review/FileReviewUnitResult")({
1060
+ phase: ReviewWorkPhase,
1061
+ workId: ReviewPassId,
1062
+ unitId: ReviewUnitId,
1063
+ candidates: Candidates,
1064
+ fileSummaries: Schema.Array(WalkthroughEntry).check(Schema.isMaxLength(12)),
1065
+ assessments: Schema.Array(CandidateAssessment).check(Schema.isMaxLength(18))
1066
+ }) {};
1067
+ var FileReviewUnitFailed = class extends Schema.TaggedError()("FileReviewUnitFailed", {
1068
+ childErrorTag: Schema.NonEmptyString.check(Schema.isMaxLength(256)),
1069
+ message: Schema.String.check(Schema.isMaxLength(400))
1070
+ }) {};
1071
+ var FileReviewWorkRejected = class extends Schema.TaggedError()("FileReviewWorkRejected", {
1072
+ workId: ReviewPassId,
1073
+ reason: Schema.NonEmptyString.check(Schema.isMaxLength(600))
1074
+ }) {};
1075
+ const FileReviewFailure = Schema.Union([FileReviewUnitFailed, FileReviewWorkRejected]);
685
1076
  const staticGuidanceLines = (guidance) => {
686
1077
  if (guidance === void 0) return [];
687
1078
  return (typeof guidance === "string" ? [guidance] : guidance).filter((line) => line.length > 0);
688
1079
  };
689
- /** Build the child file-reviewer instructions with optional static guidance. */
690
- const makeFileReviewerInstructions = (options = {}) => (brief) => [
691
- `You are a code reviewer for one unit of a pull request: unit ${brief.unitId}, covering exactly these changed files: ${brief.paths.join(", ")}. Focus: ${brief.focus}.`,
692
- ...staticGuidanceLines(options.guidance),
693
- "Work in this order:",
694
- "1. Call read_file_diff for every file in your unit. A normal diff marks new-version anchors as R<number>; only those numbers are valid startLine/endLine values. When GitHub omitted a diff, the tool may return bounded base/head content marked B/H instead. Review that content, but report its defects as non-anchored concerns because B/H lines cannot anchor GitHub comments. Never anchor a finding to a removed (-), B, or H line.",
695
- "2. Call read_file when you need surrounding context the diff does not show. ONLY files in the changeset are readable: a request for any other path (an import, a neighbor, a config) returns a failed result — do not retry it; reason from the diff instead and note the gap in your report when it matters.",
696
- "3. Review for real defects first: correctness, security, concurrency, resource leaks, error handling, API misuse. Style nits are least important. Do not praise; do not restate the diff.",
697
- "When the diff adds or changes a test, check that it can actually fail: a test that would still pass with the bug present is theatre, not coverage. The usual tell is a loose assertion standing where an exact one belongs — >= or a truthiness check over an expected value, or a snapshot that absorbs whatever it is handed.",
698
- "Drop bloat-shaped findings before reporting: defensive checks for cases that cannot happen, abstractions used once, comments restating obvious code, tests asserting tautologies, just-in-case guards. A finding must be sound, correct, and worth acting on; prefer an explicit keep over an invented finding.",
699
- `4. For every file in your unit, write one factual sentence (<= 240 chars) describing what changed in that file — for a reader scanning the pull request, never a line-by-line restatement.`,
700
- `5. Then return ONLY a JSON object — no Markdown fences, no prose before or after — exactly this shape: {"unitId": ${JSON.stringify(brief.unitId)}, "findings": [{"path": <string, a file in your unit>, "startLine": <integer, an R-marked new-file line>, "endLine": <integer, >= startLine, same file, R-marked>, "severity": <"blocking" | "important" | "nit">, "category": <OPTIONAL: "correctness" | "security" | "concurrency" | "performance" | "resources" | "error-handling" | "testing" | "maintainability" | "style" | "docs">, "title": <string, <= 120 chars>, "body": <string, why it matters and what to do>, "suggestion": <string, OPTIONAL: replacement text for exactly lines startLine..endLine, ready to commit>}], "concerns": <array, OPTIONAL: [{"severity": <"blocking" | "important" | "nit">, "title": <string, <= 120 chars>, "body": <string>}], only for concerns about YOUR unit's files with no valid line anchor (a missing cleanup, a coverage gap the diff implies, sequencing the diff leaves open) — never duplicate a finding here>, "fileSummaries": <array, OPTIONAL: [{"path": <string, a file in your unit>, "summary": <string, the step-4 sentence>}], one entry per file in your unit>}.`,
701
- `Report at most 8 findings and at most 3 concerns; prefer the most important ones. An empty findings array is a valid report. Never report on files outside your unit. Line anchors you invent will be discarded, so copy R-numbers from read_file_diff output.`
702
- ].join("\n");
1080
+ const evidenceInstructions = [
1081
+ "The host placed complete bounded review evidence shards in the input evidence array. Treat every shard as required input; ordinal/total identifies multi-shard paths.",
1082
+ "You have no tools and cannot roam outside this evidence. If it is insufficient for a candidate, reject or omit that candidate rather than guessing.",
1083
+ "A diff marks new-version anchors as R<number>; only those lines may anchor findings. B/H content evidence is non-anchorable."
1084
+ ];
1085
+ /** Discovery and verification instructions share one child definition. */
1086
+ const makeFileReviewerInstructions = (options = {}) => (brief) => {
1087
+ const common = [
1088
+ `You are an attached review worker for ${brief.workId} in host-planned unit ${brief.unitId}: ${brief.paths.join(", ")}.`,
1089
+ ...staticGuidanceLines(options.guidance),
1090
+ ...evidenceInstructions
1091
+ ];
1092
+ if (brief.phase === "verification") return [
1093
+ ...common,
1094
+ "Independently verify every candidate in the input. You did not receive another reviewer's transcript or reasoning; use only the candidate claim and bounded evidence.",
1095
+ "The evidence array contains the complete bounded unit, including neighboring changed code that may confirm or falsify a locally plausible claim.",
1096
+ "For each candidate, try to falsify it first. Confirm only when the cited behavior is supported and actionable. Reject unsupported, speculative, duplicate, or non-actionable candidates.",
1097
+ "Return ONLY JSON with phase \"verification\", the exact workId/unitId, empty findings/concerns/fileSummaries arrays, and exactly one assessment per candidateId. Each assessment is {\"candidateId\": <exact id>, \"disposition\": <\"confirmed\" | \"rejected\">, \"suggestion\": <\"committable\" | \"not-committable\", present exactly when the candidate finding carries a suggestion>, \"rationale\": <bounded evidence-based reason>}. Never add or omit an id.",
1098
+ "Settle every carried suggestion independently of the claim: answer \"committable\" only when its text is the full replacement source for exactly lines startLine..endLine and nothing else — it compiles in context and preserves the finding's intent, never prose describing a change. Otherwise answer \"not-committable\"; the host then publishes the confirmed finding without its suggestion. Omit the assessment \"suggestion\" field for candidates without one."
1099
+ ].join("\n");
1100
+ const focus = brief.perspective === "risk-specialist" ? brief.riskCategories.length > 0 ? `This is a fresh specialist discovery pass. Concentrate on these host-classified risks without relying on another pass: ${brief.riskCategories.join(", ")}.` : "This is a fresh specialist discovery pass. The host found no keyword-classified category, so independently scrutinize authentication/authorization, security boundaries, durability, concurrency, credentials, and external side effects rather than treating classification silence as low risk." : "This is the general discovery pass. Review broadly for correctness, security, concurrency, resource, API, and error-handling defects.";
1101
+ return [
1102
+ ...common,
1103
+ focus,
1104
+ "The discovery evidence array contains every complete shard in the unit. Review every entry and every shard of a multi-shard path. A later independent verifier, not you, decides which candidates publish.",
1105
+ "When a non-anchored concern depends on one or more unit files, list 1-3 exact evidencePaths to bind the claim to scheduled evidence.",
1106
+ `Return ONLY JSON with phase "discovery", the exact workId/unitId, up to 6 findings, up to 3 concerns shaped as {concern, evidencePaths}, one factual file summary per path (<= 240 chars), and an empty assessments array. Empty candidate arrays are valid; do not invent defects.`,
1107
+ "Each finding is {\"path\": <a unit file path>, \"startLine\": <integer, an R-marked new-file line>, \"endLine\": <integer, >= startLine, same file, R-marked>, \"severity\": <\"blocking\" | \"important\" | \"nit\">, \"category\": <OPTIONAL problem-kind label>, \"title\": <string, <= 120 chars>, \"body\": <string, why it matters and what to do>, \"suggestion\": <string, OPTIONAL: replacement source code for exactly lines startLine..endLine, ready to commit>}.",
1108
+ "Include \"suggestion\" only when you are confident the replacement compiles and preserves intent; its text must contain the full replacement source for every line in the range and nothing else — never prose describing the change, which belongs in \"body\"."
1109
+ ].join("\n");
1110
+ };
703
1111
  const fileReviewerInstructions = makeFileReviewerInstructions();
704
- /** The default per-unit child execution bounds. */
1112
+ const FileReviewToolkit = Toolkit.empty;
1113
+ /** Compatibility export: the evidence-only child has no handler requirements. */
1114
+ const FileReviewToolkitLayer = Layer.empty;
705
1115
  const defaultFileReviewerPolicy = AgentPolicy.make({
706
- maxTurns: 8,
707
- maxToolCalls: MAX_FILE_REVIEW_TOOL_CALLS,
1116
+ maxTurns: 6,
1117
+ maxToolCalls: 1,
708
1118
  maxDuration: "6 minutes",
709
1119
  toolConcurrency: 2,
710
- repeatedFailureLimit: 12,
1120
+ repeatedFailureLimit: 6,
711
1121
  tokenBudget: 2e5,
712
1122
  contextTokenLimit: 15e4,
713
1123
  toolResultBounds: ToolResultBounds.make({ maxBytes: REVIEW_TOOL_RESULT_MAX_BYTES }),
714
1124
  onExhaustion: "fail"
715
1125
  });
716
- /** The model-decoded delegation parameters: which unit to review. */
717
- var FileReviewRequest = class extends Schema.Class("@effect-agent/pr-review/FileReviewRequest")({
718
- unitId: ReviewUnitId,
719
- paths: UnitPaths
720
- }) {};
721
- /** The bounded parent-visible result of one delegated unit review. */
722
- var FileReviewUnitResult = class extends Schema.Class("@effect-agent/pr-review/FileReviewUnitResult")({
723
- unitId: ReviewUnitId,
724
- findings: Schema.Array(ReviewFinding).check(Schema.isMaxLength(8)),
725
- /** Unit-scoped concerns with no diff line to anchor to. */
726
- concerns: Schema.optionalKey(Schema.Array(ReviewConcern).check(Schema.isMaxLength(3))),
727
- /** One-sentence per-file change summaries for the merged walkthrough. */
728
- fileSummaries: Schema.optionalKey(Schema.Array(WalkthroughEntry).check(Schema.isMaxLength(12)))
729
- }) {};
730
- /**
731
- * One unit's review failed: the child Run ended in a typed failure (policy
732
- * bound, output violation, model fault). The marker is bounded and carries no
733
- * child transcript content beyond the failure tag and message.
734
- */
735
- var FileReviewUnitFailed = class extends Schema.TaggedError()("FileReviewUnitFailed", {
736
- childErrorTag: Schema.NonEmptyString.check(Schema.isMaxLength(256)),
737
- message: Schema.String.check(Schema.isMaxLength(400))
738
- }) {};
739
- /**
740
- * Finite per-invocation bounds (SUB-009), aligned with the child's own
741
- * AgentPolicy: the child's policy is the limit that trips typed; the
742
- * reservation mirrors it so parent-side accounting stays honest.
743
- */
744
1126
  const fileReviewPolicy = SubagentPolicy.make({
745
- maxChildren: 8,
746
- maxConcurrency: 3,
747
- maxTurns: 8,
748
- maxToolCalls: MAX_FILE_REVIEW_TOOL_CALLS,
749
- maxDuration: "6 minutes"
1127
+ maxChildren: MAX_REVIEW_CHILDREN,
1128
+ maxConcurrency: 4,
1129
+ maxTurns: 6,
1130
+ maxToolCalls: 1,
1131
+ maxDuration: "6 minutes",
1132
+ maxResultBytes: 256 * 1024
750
1133
  });
751
- const delegationDescription = "Delegate the review of one planned unit to a bounded file-reviewer child and return its line-anchored findings. Call it exactly once per unit from list_review_units; never retry a failed unit.";
752
- /**
753
- * Total mapping from every expected child Run failure to the declared unit
754
- * failure (SUB-028): the tag plus a bounded message, nothing else crosses.
755
- */
756
1134
  const mapFileReviewChildFailure = (failure) => FileReviewUnitFailed.make({
757
1135
  childErrorTag: failure._tag,
758
1136
  message: (failure.message ?? "").slice(0, 400)
759
1137
  });
760
- var ListReviewUnitsQuery = class extends Schema.Class("@effect-agent/pr-review/ListReviewUnitsQuery")({
761
- /** Explicit constant keeps the zero-choice operation compatible with strict provider schemas. */
762
- scope: Schema.Literal("all") }) {};
1138
+ const sameStrings = (left, right) => left.length === right.length && left.every((value, index) => value === right[index]);
1139
+ const rejectWork = (workId, reason) => FileReviewWorkRejected.make({
1140
+ workId,
1141
+ reason
1142
+ });
1143
+ /** Validate coordinator scheduling against the current deterministic plan. */
1144
+ const prepareReviewBrief = (request) => Effect.gen(function* () {
1145
+ const source = yield* PullRequestSource;
1146
+ const mapSourceFailure = (failure) => rejectWork(request.workId, `pull-request source ${failure.operation} failed: ${failure.reason}`.slice(0, 600));
1147
+ const files = yield* source.changedFiles.pipe(Effect.mapError(mapSourceFailure));
1148
+ const plan = planReviewUnits(files, { totalChangedFiles: (yield* source.metadata.pipe(Effect.mapError(mapSourceFailure))).totalChangedFiles });
1149
+ const unit = plan.units.find((candidate) => candidate.unitId === request.unitId);
1150
+ if (unit === void 0 || !sameStrings(request.paths, unit.paths) || !sameStrings(request.evidenceShardIds, unit.evidenceShards.map((shard) => shard.shardId))) return yield* rejectWork(request.workId, "request does not match a host-planned unit");
1151
+ if (request.phase === "discovery") {
1152
+ const pass = plan.discoveryPasses.find((candidate) => candidate.passId === request.workId);
1153
+ if (pass === void 0 || pass.unitId !== request.unitId || !sameStrings(pass.paths, request.paths) || !sameStrings(pass.evidenceShardIds, request.evidenceShardIds) || pass.perspective !== request.perspective || !sameStrings(pass.riskCategories, request.riskCategories) || request.candidates.length !== 0) return yield* rejectWork(request.workId, "discovery request does not match the host plan");
1154
+ } else {
1155
+ if (request.workId !== `${request.unitId}-verification` || request.perspective !== "candidate-verification" || !sameStrings(request.riskCategories, unit.riskCategories) || request.candidates.length === 0) return yield* rejectWork(request.workId, "verification request does not match the host-planned unit");
1156
+ const candidateIds = /* @__PURE__ */ new Set();
1157
+ const candidateSubjects = /* @__PURE__ */ new Set();
1158
+ const allowed = new Set(unit.paths);
1159
+ for (const candidate of request.candidates) {
1160
+ const subjectKey = reviewCandidateSubjectKey(candidate);
1161
+ if (candidateIds.has(candidate.candidateId) || candidateSubjects.has(subjectKey) || candidate.unitId !== unit.unitId || candidate.evidencePaths.some((path) => !allowed.has(path)) || candidate._tag === "FindingCandidate" && !allowed.has(candidate.finding.path)) return yield* rejectWork(request.workId, "verification candidates are duplicated or outside the planned unit");
1162
+ candidateIds.add(candidate.candidateId);
1163
+ candidateSubjects.add(subjectKey);
1164
+ }
1165
+ }
1166
+ const byPath = new Map(files.map((file) => [file.path, file]));
1167
+ const evidence = [];
1168
+ for (const shard of unit.evidenceShards) {
1169
+ const file = byPath.get(shard.path);
1170
+ if (file === void 0) return yield* rejectWork(request.workId, `planned evidence path is unavailable: ${shard.path}`);
1171
+ const chunks = fileReviewEvidenceChunks(file);
1172
+ const chunk = chunks[shard.ordinal - 1];
1173
+ if (chunk === void 0 || chunks.length !== shard.total || chunk.annotatedPatch.length !== shard.evidenceChars) return yield* rejectWork(request.workId, `planned evidence shard no longer matches source: ${shard.shardId}`);
1174
+ evidence.push(FileReviewEvidence.make({
1175
+ shardId: shard.shardId,
1176
+ path: shard.path,
1177
+ status: file.status,
1178
+ reviewMode: chunk.reviewMode,
1179
+ ordinal: shard.ordinal,
1180
+ total: shard.total,
1181
+ annotatedPatch: chunk.annotatedPatch
1182
+ }));
1183
+ }
1184
+ return FileReviewBrief.make({
1185
+ ...request,
1186
+ evidence
1187
+ });
1188
+ });
1189
+ const candidateOrdinal = (index) => String(index + 1).padStart(3, "0");
1190
+ const projectReviewResult = (report, context, request) => {
1191
+ if (context.budgetExhausted) return Effect.fail(rejectWork(report.workId, "review work exhausted its budget before exact settlement"));
1192
+ if (report.phase !== request.phase || report.workId !== request.workId || report.unitId !== request.unitId) return Effect.fail(rejectWork(request.workId, "review output identity does not match the scheduled request"));
1193
+ if (report.phase === "verification") {
1194
+ if (report.findings.length > 0 || report.concerns.length > 0 || report.fileSummaries.length > 0) return Effect.fail(rejectWork(report.workId, "verification output contained discovery-only fields"));
1195
+ const expectedById = new Map(request.candidates.map((candidate) => [candidate.candidateId, candidate]));
1196
+ const assessedIds = /* @__PURE__ */ new Set();
1197
+ for (const assessment of report.assessments) {
1198
+ const candidate = expectedById.get(assessment.candidateId);
1199
+ if (candidate === void 0 || assessedIds.has(assessment.candidateId)) return Effect.fail(rejectWork(report.workId, "verification output did not assess the exact candidate set"));
1200
+ if (!assessmentSettlesSuggestionExactly(assessment, candidate)) return Effect.fail(rejectWork(report.workId, "verification output did not settle suggestion publication exactly"));
1201
+ assessedIds.add(assessment.candidateId);
1202
+ }
1203
+ if (assessedIds.size !== expectedById.size) return Effect.fail(rejectWork(report.workId, "verification output did not assess the exact candidate set"));
1204
+ return Effect.succeed(FileReviewUnitResult.make({
1205
+ phase: report.phase,
1206
+ workId: report.workId,
1207
+ unitId: report.unitId,
1208
+ candidates: [],
1209
+ fileSummaries: [],
1210
+ assessments: report.assessments
1211
+ }));
1212
+ }
1213
+ if (report.assessments.length > 0) return Effect.fail(rejectWork(report.workId, "discovery output contained verification-only assessments"));
1214
+ const allowed = new Set(request.paths);
1215
+ if (report.findings.some((finding) => !allowed.has(finding.path)) || report.concerns.some((candidate) => candidate.evidencePaths.some((path) => !allowed.has(path))) || report.fileSummaries.some((entry) => !allowed.has(entry.path))) return Effect.fail(rejectWork(report.workId, "discovery output referenced evidence outside the scheduled unit"));
1216
+ return Effect.gen(function* () {
1217
+ const source = yield* PullRequestSource;
1218
+ const mapSourceFailure = (failure) => rejectWork(request.workId, `pull-request source ${failure.operation} failed: ${failure.reason}`.slice(0, 600));
1219
+ const files = yield* source.changedFiles.pipe(Effect.mapError(mapSourceFailure));
1220
+ const metadata = yield* source.metadata.pipe(Effect.mapError(mapSourceFailure));
1221
+ const anchorFiles = yield* source.anchorFiles.pipe(Effect.mapError(mapSourceFailure));
1222
+ const unit = planReviewUnits(files, { totalChangedFiles: metadata.totalChangedFiles }).units.find((candidate) => candidate.unitId === request.unitId);
1223
+ if (unit === void 0) return yield* rejectWork(request.workId, "scheduled review unit is no longer available");
1224
+ for (const finding of report.findings) {
1225
+ const violation = anchorViolation(finding, anchorFiles);
1226
+ if (violation !== void 0 || !findingAnchorInUnitEvidence(finding, unit, files)) return yield* rejectWork(request.workId, `discovery finding has no valid anchor in its assigned evidence: ${violation ?? finding.path}`);
1227
+ }
1228
+ const findingCandidates = report.findings.map((finding, index) => FindingCandidate.make({
1229
+ candidateId: `${request.workId}:finding:${candidateOrdinal(index)}`,
1230
+ workId: request.workId,
1231
+ unitId: request.unitId,
1232
+ finding,
1233
+ evidencePaths: [finding.path]
1234
+ }));
1235
+ const concernCandidates = report.concerns.map((candidate, index) => ConcernCandidate.make({
1236
+ candidateId: `${request.workId}:concern:${candidateOrdinal(index)}`,
1237
+ workId: request.workId,
1238
+ unitId: request.unitId,
1239
+ concern: candidate.concern,
1240
+ evidencePaths: candidate.evidencePaths
1241
+ }));
1242
+ return FileReviewUnitResult.make({
1243
+ phase: report.phase,
1244
+ workId: report.workId,
1245
+ unitId: report.unitId,
1246
+ candidates: [...findingCandidates, ...concernCandidates],
1247
+ fileSummaries: report.fileSummaries,
1248
+ assessments: []
1249
+ });
1250
+ });
1251
+ };
1252
+ const delegationDescription = "Run exactly one host-planned discovery or candidate-verification child. Copy every plan field and candidate verbatim; never retry failed work.";
1253
+ const makeFileReviewDelegation = (child) => Subagent.define("delegate_file_review", {
1254
+ description: delegationDescription,
1255
+ target: child,
1256
+ parameters: FileReviewRequest,
1257
+ success: FileReviewUnitResult,
1258
+ failure: FileReviewFailure,
1259
+ failureMode: "return",
1260
+ prepareInput: prepareReviewBrief,
1261
+ projectResult: projectReviewResult,
1262
+ policy: fileReviewPolicy
1263
+ });
1264
+ var ListReviewUnitsQuery = class extends Schema.Class("@effect-agent/pr-review/ListReviewUnitsQuery")({ scope: Schema.Literal("all") }) {};
763
1265
  const ListReviewUnits = Tool.make("list_review_units", {
764
- description: "List this pull request's changeset grouped into bounded review units (size-budgeted, directory-affine), plus the files no unit can cover.",
1266
+ description: "List deterministic bounded review units, explicit risk categories, every required discovery pass, and paths the pipeline cannot cover.",
765
1267
  parameters: ListReviewUnitsQuery,
766
1268
  success: ReviewUnitPlan,
767
1269
  failure: PullRequestSourceFailure,
@@ -773,80 +1275,44 @@ const FanOutCoordinatorToolkitLayer = FanOutCoordinatorToolkit.toLayer({ list_re
773
1275
  const source = yield* PullRequestSource;
774
1276
  return planReviewUnits(yield* source.changedFiles, { totalChangedFiles: (yield* source.metadata).totalChangedFiles });
775
1277
  }) });
776
- /**
777
- * Build the coordinator's instructions. The same consumer guidance the
778
- * children receive is injected between the mission framing and the procedure
779
- * so the merged summary and verdict are shaped by the same review profile,
780
- * and the configured findings bound reaches the merge step instead of only
781
- * the host-side trim.
782
- */
783
1278
  const makeFanOutReviewInstructions = (options = {}) => (mission) => {
784
1279
  const maxFindings = clampMaxFindings(options.maxFindings);
785
1280
  return [
786
- `You coordinate the review of pull request #${mission.number} ("${mission.title}") in ${mission.repository}, merging ${mission.headRef} into ${mission.baseRef}. It changes ${mission.changedFileCount} file(s).`,
787
- mission.body.length > 0 ? `Author description:\n${mission.body}` : "The author provided no description.",
1281
+ `You coordinate the bounded multi-pass review of pull request #${mission.number} ("${mission.title}") in ${mission.repository}.`,
1282
+ mission.body.length > 0 ? `Author description:\n${mission.body}` : "No author description.",
788
1283
  ...staticGuidanceLines(options.guidance),
789
- "Work in this order:",
790
- "1. Call list_review_units once to get the planned review units.",
791
- "2. Call delegate_file_review EXACTLY once per unit, passing each unit's unitId and paths verbatim. Prefer declaring all delegation calls in one batch. Never review files yourself and never invent units.",
792
- "3. A delegation result with \"_tag\" is a FAILED unit. Never retry it; instead your summary MUST name it honestly, e.g. \"unit-002 unreviewed: AgentPolicyError\". The plan's undiffablePaths and unassignedPaths must also be named as not reviewed when present.",
793
- `4. Merge the successful units' findings: drop duplicates sharing the same path and line range keeping the most severe, rank blocking > important > nit, and keep at most ${maxFindings} findings. Drop bloat-shaped findings during the merge defensive checks for cases that cannot happen, abstractions used once, comments restating obvious code, tests asserting tautologies; children bias toward recommending changes, and a finding must be sound, correct, and worth acting on to survive.`,
794
- `5. Merge the units' concerns the same way: drop duplicates keeping the most severe, and keep at most 10.`,
795
- "6. Merge the units' fileSummaries into one walkthrough: copy each entry verbatim, one entry per file, dropping duplicate paths.",
796
- "7. Then return ONLY a JSON object — no Markdown fences, no prose before or after — exactly this shape: {\"summary\": <string, 1-3 paragraphs of overall assessment, including every unreviewed unit or file>, \"verdict\": <\"approve\" | \"comment\" | \"request-changes\">, \"findings\": [{\"path\": <string>, \"startLine\": <integer>, \"endLine\": <integer>, \"severity\": <\"blocking\" | \"important\" | \"nit\">, \"category\": <string, OPTIONAL>, \"title\": <string, <= 120 chars>, \"body\": <string>, \"suggestion\": <string, OPTIONAL>}], \"concerns\": <array, OPTIONAL: [{\"severity\": <\"blocking\" | \"important\" | \"nit\">, \"title\": <string, <= 120 chars>, \"body\": <string>}], the merged unit concerns>, \"walkthrough\": <array, OPTIONAL: [{\"path\": <string>, \"summary\": <string>}], the merged fileSummaries>}. Copy findings (including \"category\" and \"suggestion\" when present), concerns, and walkthrough entries verbatim from the delegation results; never invent or edit anchors.",
797
- "Use verdict \"request-changes\" only when at least one finding or concern is \"blocking\". An empty findings array with verdict \"approve\" is a valid review when every unit succeeded and found nothing."
1284
+ "1. Call list_review_units exactly once.",
1285
+ "2. For EVERY discoveryPass, call delegate_file_review exactly once with phase \"discovery\", workId=passId, and the pass unitId/paths/evidenceShardIds/perspective/riskCategories verbatim; candidates must be []. Prefer one bounded parallel batch. Never retry.",
1286
+ "3. Group candidates returned by all successful discovery passes by unit. Deterministically deduplicate byte-identical finding or concern payloads, retaining the first candidate in discoveryPass plan order. For every unit with at least one retained candidate, call delegate_file_review exactly once with phase \"verification\", workId \"<unitId>-verification\", perspective \"candidate-verification\", the unit paths/evidenceShardIds/riskCategories, and EVERY retained candidate copied byte-for-byte. Prefer one bounded parallel batch. Never retry.",
1287
+ "4. Verification is authoritative: rejected candidates must not be reported. The host independently reconstructs publishable findings from exact confirmed assessments, so do not select, rewrite, downgrade, or invent findings.",
1288
+ `5. Return ONLY CodeReview JSON. Write a concise summary of completed and failed stages. Set findings=[] and concerns=[]; the host injects exact confirmed candidates. Copy factual fileSummaries into walkthrough without invention. The host publication cap is ${maxFindings}.`,
1289
+ "No configured pipeline can prove absence of defects. Describe settled work, never an exhaustive or defect-free review."
798
1290
  ].join("\n");
799
1291
  };
800
1292
  const fanOutReviewInstructions = makeFanOutReviewInstructions();
801
- /** The default fan-out coordinator execution bounds. */
802
1293
  const defaultFanOutPolicy = AgentPolicy.make({
803
- maxTurns: 6,
804
- maxToolCalls: 9,
805
- maxDuration: "15 minutes",
806
- toolConcurrency: 3,
1294
+ maxTurns: 7,
1295
+ maxToolCalls: 25,
1296
+ maxDuration: "20 minutes",
1297
+ toolConcurrency: 4,
807
1298
  repeatedFailureLimit: 3,
808
- tokenBudget: 3e5,
1299
+ tokenBudget: 4e5,
809
1300
  contextTokenLimit: 15e4,
810
1301
  onExhaustion: "final-answer"
811
1302
  });
812
- const makeFileReviewerDefinition = (options = {}) => Agent.define("pr-file-reviewer", {
1303
+ const makeFileReviewerDefinition = (options = {}) => Agent.define("pr-review-worker", {
813
1304
  input: FileReviewBrief,
814
1305
  output: FileReviewReport,
815
1306
  instructions: makeFileReviewerInstructions(options),
816
1307
  toolkit: FileReviewToolkit,
817
1308
  policy: defaultFileReviewerPolicy,
818
- description: "Review one bounded unit of a pull request's changeset read-only and return line-anchored findings for exactly those files.",
1309
+ description: "Perform one bounded discovery or independent candidate-verification pass over host-supplied pull-request evidence.",
819
1310
  metadata: {
820
1311
  deploymentClass: "E",
821
- surface: "read-only"
1312
+ surface: "read-only",
1313
+ stage: "discovery-verification"
822
1314
  }
823
1315
  });
824
- const makeFileReviewDelegation = (child) => Subagent.define("delegate_file_review", {
825
- description: delegationDescription,
826
- target: child,
827
- parameters: FileReviewRequest,
828
- success: FileReviewUnitResult,
829
- failure: FileReviewUnitFailed,
830
- failureMode: "return",
831
- prepareInput: (request) => Effect.succeed(FileReviewBrief.make({
832
- unitId: request.unitId,
833
- paths: request.paths,
834
- focus: "defects-first: correctness, security, concurrency, resources, error handling"
835
- })),
836
- projectResult: (report) => Effect.succeed(FileReviewUnitResult.make({
837
- unitId: report.unitId,
838
- findings: report.findings,
839
- ...report.concerns !== void 0 ? { concerns: report.concerns } : {},
840
- ...report.fileSummaries !== void 0 ? { fileSummaries: report.fileSummaries } : {}
841
- })),
842
- policy: fileReviewPolicy
843
- });
844
- /**
845
- * The coordinator-facing delegation Tool: the delegation's own first-party
846
- * contained Tool plus the read-only execution class (the delegated child's
847
- * whole tool surface is read-only). Effect AI resolves handlers by Tool name,
848
- * so `SubagentRuntime.layer`'s handler serves this annotated copy unchanged.
849
- */
850
1316
  const delegationToolFor = (delegation) => delegation.tool.annotate(ToolExecutionClass, "readonly");
851
1317
  const makeFanOutReviewerDefinition = (options, delegation) => Agent.define("pr-fanout-reviewer", {
852
1318
  input: ReviewMission,
@@ -854,14 +1320,14 @@ const makeFanOutReviewerDefinition = (options, delegation) => Agent.define("pr-f
854
1320
  instructions: makeFanOutReviewInstructions(options),
855
1321
  toolkit: Toolkit.make(ListReviewUnits, delegationToolFor(delegation)),
856
1322
  policy: defaultFanOutPolicy,
857
- description: "Coordinate one pull-request review by fanning bounded per-unit file reviews out to delegated children and merging their findings into one structured review.",
1323
+ description: "Coordinate deterministic general/specialist discovery and independent candidate verification over bounded review units.",
858
1324
  metadata: {
859
1325
  deploymentClass: "E",
860
1326
  surface: "read-only",
861
- delegation: "S1-attached"
1327
+ delegation: "S1-attached",
1328
+ assurance: "multi-pass"
862
1329
  }
863
1330
  });
864
- /** Build one coherent fan-out suite: child, coordinator, and delegation. */
865
1331
  const makeFanOutReviewSuite = (options = {}) => {
866
1332
  const child = makeFileReviewerDefinition({ guidance: options.guidance });
867
1333
  const delegation = makeFileReviewDelegation(child);
@@ -872,25 +1338,13 @@ const makeFanOutReviewSuite = (options = {}) => {
872
1338
  };
873
1339
  };
874
1340
  const defaultSuite = makeFanOutReviewSuite();
875
- /** The default child Agent Definition. */
876
1341
  const FileReviewer = defaultSuite.child;
877
- /** The default coordinator Agent Definition. */
878
1342
  const FanOutReviewer = defaultSuite.parent;
879
- /** The default delegation over the default child. */
880
1343
  const fileReviewDelegation = defaultSuite.delegation;
881
- /** The default coordinator-facing delegation Tool (first-party contained mode). */
882
1344
  const DelegateFileReview = delegationToolFor(fileReviewDelegation);
883
- /** The default coordinator Toolkit. */
884
1345
  const FanOutReviewToolkit = FanOutReviewer.toolkit;
885
- /**
886
- * The contained failure family the delegation can surface as result data
887
- * (SUB-033), derived from the delegation itself so the coverage decoder can
888
- * never diverge from what the runtime actually contains.
889
- */
890
1346
  const FileReviewDelegationFailure = fileReviewDelegation.containedFailure;
891
- /** Runtime wiring: one delegation plus one explicit child Binding. */
892
1347
  const fanOutHandlersLayerFor = (delegation) => (childBinding) => SubagentRuntime.layer(delegation, childBinding, { mapChildFailure: mapFileReviewChildFailure });
893
- /** Runtime wiring over the default delegation, mirroring the leaf example. */
894
1348
  const fanOutHandlersLayer = fanOutHandlersLayerFor(fileReviewDelegation);
895
1349
  //#endregion
896
1350
  //#region src/internal/fingerprint.ts
@@ -957,10 +1411,12 @@ var StoredReviewConcern = class extends Schema.Class("@effect-agent/pr-review/St
957
1411
  body: StoredText
958
1412
  }) {};
959
1413
  /**
960
- * Versioned state embedded in one successfully covered review. The reviewed
961
- * head plus the full-scope fingerprint means every path not represented by an
962
- * unresolved item is accepted at that head; storing hundreds of path strings
963
- * separately would not fit GitHub's bounded review body in the worst case.
1414
+ * Versioned state embedded only after complete input assignment and settled
1415
+ * configured review assurance. The head plus full-scope fingerprint forms an
1416
+ * incremental baseline; an absent unresolved item never means the path is
1417
+ * defect-free. The `acceptedScopeFingerprint` name is retained for wire
1418
+ * compatibility. Storing hundreds of path strings separately would not fit
1419
+ * GitHub's bounded review body in the worst case.
964
1420
  */
965
1421
  var ReviewState = class extends Schema.Class("@effect-agent/pr-review/ReviewState")({
966
1422
  version: Schema.Literal(1),
@@ -1170,7 +1626,7 @@ const selectReviewRange = (input) => {
1170
1626
  const selectedFiles = [...selectedByPath.values()].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
1171
1627
  return {
1172
1628
  mode: "incremental",
1173
- reason: `changes since successfully reviewed head ${input.priorState.reviewedHeadSha.slice(0, 7)}${baseReason}`,
1629
+ reason: `changes since settled review head ${input.priorState.reviewedHeadSha.slice(0, 7)}${baseReason}`,
1174
1630
  files: selectedFiles,
1175
1631
  affectedPaths: [...affectedPaths].sort(),
1176
1632
  totalFiles: selectedFiles.length,
@@ -1842,6 +2298,6 @@ const fingerprintUnchanged = (current) => Effect.gen(function* () {
1842
2298
  return Option.isSome(latest) && latest.value === current;
1843
2299
  });
1844
2300
  //#endregion
1845
- export { FanOutReviewToolkit as $, ReadFile as $t, ReviewStateAuthenticator as A, isReviewableFile as An, ReviewUnit as At, selectedPullRequestSourceLayer as B, FileDiffView as Bt, MAX_REVIEW_STATE_MARKER_CHARS as C, ChangedFile as Cn, makeFanOutReviewInstructions as Ct, ReviewScopeMode as D, annotatePatch as Dn, MAX_MERGED_FINDINGS as Dt, ReviewMode as E, MAX_REVIEW_CONTENT_CHARS as En, mapFileReviewChildFailure as Et, buildProfileMission as F, rankAndDedupeFindings as Ft, webCryptoReviewStateAuthenticatorLayer as G, ListChangedFiles as Gt, toStoredFinding as H, FileSliceQuery as Ht, computeProfileFingerprint as I, ChangedFileSummary as It, extractFingerprint as J, MAX_FINDINGS as Jt, FINGERPRINT_MARKER_LENGTH as K, ListChangedFilesQuery as Kt, fromStoredConcern as L, ChangedFilesView as Lt, ReviewStateMarkerTooLarge as M, renderReviewContent as Mn, ReviewUnitPlan as Mt, StoredReviewConcern as N, UNIT_CHANGED_LINE_BUDGET as Nt, ReviewState as O, commentableLines as On, MAX_REVIEW_UNITS as Ot, StoredReviewFinding as P, planReviewUnits as Pt, FanOutCoordinatorToolkitLayer as Q, REVIEW_TOOL_RESULT_MAX_BYTES as Qt, fromStoredFinding as R, CodeReview as Rt, GitCommitSha as S, normalizeRepoRelativePath as Sn, fileReviewerInstructions as St, ReviewHeadComparison as T, ChangedPath as Tn, makeFileReviewerInstructions as Tt, unavailableReviewStateAuthenticatorLayer as U, FindingCategory as Ut, toStoredConcern as V, FileSlice as Vt, validateReviewState as W, FindingSeverity as Wt, DelegateFileReview as X, MAX_WALKTHROUGH_SUMMARY_CHARS as Xt, renderFingerprintMarker as Y, MAX_WALKTHROUGH_ENTRIES as Yt, FanOutCoordinatorToolkit as Z, PullRequestReviewer as Zt, ReviewRetirementHost as _, MAX_FILE_CHARS as _n, fanOutHandlersLayer as _t, PriorReviews as a, ReviewToolkitLayer as an, FileReviewToolkit as at, hasReviewMetadataMarker as b, PullRequestSourceFailure as bn, fileReviewDelegation as bt, fingerprintUnchanged as c, clampMaxFindings as cn, FileReviewUnitResult as ct, gitHubReviewPublisherLayer as d, makeReviewInstructions as dn, ListReviewUnitsQuery as dt, ReadFileDiff as en, FanOutReviewer as et, gitHubReviewRetirementHostLayer as f, readFileDiffHandler as fn, MAX_CHILD_CONCERNS as ft, ReviewRetirementFailure as g, MAX_CHANGED_FILES as gn, defaultFileReviewerPolicy as gt, RetirableReviewComment as h, reviewInstructions as hn, defaultFanOutPolicy as ht, PriorReviewLookupFailure as i, ReviewToolkit as in, FileReviewRequest as it, ReviewStateMarker as j, parsePatch as jn, ReviewUnitId as jt, ReviewStateAuthenticationFailure as k, hasReviewableContent as kn, MAX_UNIT_FILES as kt, gitHubPriorReviewsLayer as l, defaultReviewPolicy as ln, FileReviewer as lt, RetirableReview as m, resolveGuidance as mn, MAX_FILE_REVIEW_TOOL_CALLS as mt, GitHubApiFailure as n, ReviewFinding as nn, FileReviewDelegationFailure as nt, PublishedReview as o, ReviewVerdict as on, FileReviewToolkitLayer as ot, parseGitHubSubmittedAt as p, readFileHandler as pn, MAX_CHILD_FINDINGS as pt, computeChangesetFingerprint as q, MAX_CONCERNS as qt, GitHubReviewTarget as r, ReviewMission as rn, FileReviewReport as rt, ReviewPublisher as s, WalkthroughEntry as sn, FileReviewUnitFailed as st, DEFAULT_GITHUB_REVIEW_AUTHOR_LOGIN as t, ReviewConcern as tn, FileReviewBrief as tt, gitHubPullRequestSourceLayer as u, listChangedFilesHandler as un, ListReviewUnits as ut, ReviewRetirementReport as v, PullRequestMetadata as vn, fanOutHandlersLayerFor as vt, ReviewExecutionContext as w, ChangedFileStatus as wn, makeFanOutReviewSuite as wt, retireStaleReviews as x, ReviewInputViolation as xn, fileReviewPolicy as xt, decideReviewRetirement as y, PullRequestSource as yn, fanOutReviewInstructions as yt, selectReviewRange as z, FileDiffQuery as zt };
2301
+ export { DiscoveredConcern as $, normalizeRepoRelativePath as $n, ReviewPassId as $t, ReviewStateAuthenticator as A, ReviewConcern as An, defaultFanOutPolicy as At, selectedPullRequestSourceLayer as B, fileReviewEvidenceChunks as Bn, makeFileReviewerInstructions as Bt, MAX_REVIEW_STATE_MARKER_CHARS as C, MAX_PATCH_CHARS as Cn, MAX_UNIT_CANDIDATES as Ct, ReviewScopeMode as D, REVIEW_TOOL_RESULT_MAX_BYTES as Dn, ReviewWorkPhase as Dt, ReviewMode as E, PullRequestReviewer as En, ReviewWorkPerspective as Et, buildProfileMission as F, ReviewVerdict as Fn, fileReviewDelegation as Ft, webCryptoReviewStateAuthenticatorLayer as G, resolveGuidance as Gn, MAX_REPORTED_UNASSIGNED_EVIDENCE_SHARDS as Gt, toStoredFinding as H, makeReviewInstructions as Hn, reviewCandidateSubjectKey as Ht, computeProfileFingerprint as I, WalkthroughEntry as In, fileReviewPolicy as It, extractFingerprint as J, MAX_FILE_CHARS as Jn, MAX_UNIT_FILES as Jt, FINGERPRINT_MARKER_LENGTH as K, reviewInstructions as Kn, MAX_REVIEW_UNITS as Kt, fromStoredConcern as L, clampMaxFindings as Ln, fileReviewerInstructions as Lt, ReviewStateMarkerTooLarge as M, ReviewMission as Mn, fanOutHandlersLayer as Mt, StoredReviewConcern as N, ReviewToolkit as Nn, fanOutHandlersLayerFor as Nt, ReviewState as O, ReadFile as On, assessmentSettlesSuggestionExactly as Ot, StoredReviewFinding as P, ReviewToolkitLayer as Pn, fanOutReviewInstructions as Pt, DelegateFileReview as Q, ReviewInputViolation as Qn, ReviewEvidenceShardId as Qt, fromStoredFinding as R, defaultReviewPolicy as Rn, makeFanOutReviewInstructions as Rt, GitCommitSha as S, MAX_FINDINGS as Sn, MAX_REVIEW_CHILDREN as St, ReviewHeadComparison as T, MAX_WALKTHROUGH_SUMMARY_CHARS as Tn, ReviewCandidateId as Tt, unavailableReviewStateAuthenticatorLayer as U, readFileDiffHandler as Un, MAX_FILE_EVIDENCE_CHARS as Ut, toStoredConcern as V, listChangedFilesHandler as Vn, mapFileReviewChildFailure as Vt, validateReviewState as W, readFileHandler as Wn, MAX_MERGED_FINDINGS as Wt, CandidateAssessment as X, PullRequestSource as Xn, ReviewDiscoveryPerspective as Xt, renderFingerprintMarker as Y, PullRequestMetadata as Yn, ReviewDiscoveryPass as Yt, ConcernCandidate as Z, PullRequestSourceFailure as Zn, ReviewEvidenceShard as Zt, ReviewRetirementHost as _, FindingCategory as _n, ListReviewUnits as _t, PriorReviews as a, UNIT_EVIDENCE_CHAR_BUDGET as an, annotatePatch as ar, FileReviewDelegationFailure as at, hasReviewMetadataMarker as b, ListChangedFilesQuery as bn, MAX_CHILD_FINDINGS as bt, fingerprintUnchanged as c, planReviewUnits as cn, isReviewableFile as cr, FileReviewReport as ct, gitHubReviewPublisherLayer as d, ChangedFilesView as dn, FileReviewToolkitLayer as dt, ReviewRiskCategory as en, anchorViolation as er, FanOutCoordinatorToolkit as et, gitHubReviewRetirementHostLayer as f, CodeReview as fn, FileReviewUnitFailed as ft, ReviewRetirementFailure as g, FileSliceQuery as gn, FindingCandidate as gt, RetirableReviewComment as h, FileSlice as hn, FileReviewer as ht, PriorReviewLookupFailure as i, UNIT_CHANGED_LINE_BUDGET as in, MAX_REVIEW_CONTENT_CHARS as ir, FileReviewBrief as it, ReviewStateMarker as j, ReviewFinding as jn, defaultFileReviewerPolicy as jt, ReviewStateAuthenticationFailure as k, ReadFileDiff as kn, confirmedFindingForPublication as kt, gitHubPriorReviewsLayer as l, rankAndDedupeFindings as ln, parsePatch as lr, FileReviewRequest as lt, RetirableReview as m, FileDiffView as mn, FileReviewWorkRejected as mt, GitHubApiFailure as n, ReviewUnitId as nn, ChangedFileStatus as nr, FanOutReviewToolkit as nt, PublishedReview as o, classifyReviewRisks as on, commentableLines as or, FileReviewEvidence as ot, parseGitHubSubmittedAt as p, FileDiffQuery as pn, FileReviewUnitResult as pt, computeChangesetFingerprint as q, MAX_CHANGED_FILES as qn, MAX_UNIT_EVIDENCE_SHARDS as qt, GitHubReviewTarget as r, ReviewUnitPlan as rn, ChangedPath as rr, FanOutReviewer as rt, ReviewPublisher as s, findingAnchorInUnitEvidence as sn, hasReviewableContent as sr, FileReviewFailure as st, DEFAULT_GITHUB_REVIEW_AUTHOR_LOGIN as t, ReviewUnit as tn, ChangedFile as tr, FanOutCoordinatorToolkitLayer as tt, gitHubPullRequestSourceLayer as u, ChangedFileSummary as un, renderReviewContent as ur, FileReviewToolkit as ut, ReviewRetirementReport as v, FindingSeverity as vn, ListReviewUnitsQuery as vt, ReviewExecutionContext as w, MAX_WALKTHROUGH_ENTRIES as wn, ReviewCandidate as wt, retireStaleReviews as x, MAX_CONCERNS as xn, MAX_FILE_REVIEW_TOOL_CALLS as xt, decideReviewRetirement as y, ListChangedFiles as yn, MAX_CHILD_CONCERNS as yt, selectReviewRange as z, fileDiffView as zn, makeFanOutReviewSuite as zt };
1846
2302
 
1847
- //# sourceMappingURL=github-DnenG3be.mjs.map
2303
+ //# sourceMappingURL=github-BgtP7Rdv.mjs.map