@effect-agent/pr-review 0.0.1-beta.0 → 0.1.0-beta.11

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.
@@ -1,5 +1,5 @@
1
- import { Context, Effect, Encoding, Layer, Option, Redacted, Result, Schema } from "effect";
2
- import { Agent, AgentPolicy, AgentSpawner, IdGenerator, RunEventSink, Subagent, SubagentBudgetExhausted, SubagentDurability, SubagentDurabilityError, SubagentExecutionFailure, SubagentPolicy, SubagentPrestartDenied, SubagentProjectionFailure, SubagentRuntime, ToolCallWaiting, ToolExecutionClass } from "effect-agent";
1
+ import { Context, DateTime, Effect, Encoding, Layer, Option, Redacted, Result, Schema } from "effect";
2
+ import { Agent, AgentPolicy, Subagent, SubagentPolicy, SubagentRuntime, ToolExecutionClass } from "effect-agent";
3
3
  import { Tool, Toolkit } from "effect/unstable/ai";
4
4
  import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
5
5
  //#region src/internal/diff.ts
@@ -414,7 +414,9 @@ const defaultReviewPolicy = AgentPolicy.make({
414
414
  maxToolCalls: 24,
415
415
  maxDuration: "8 minutes",
416
416
  toolConcurrency: 2,
417
- tokenBudget: 3e5
417
+ tokenBudget: 3e5,
418
+ contextTokenLimit: 15e4,
419
+ onExhaustion: "final-answer"
418
420
  });
419
421
  const PullRequestReviewer = Agent.define("pr-reviewer", {
420
422
  input: ReviewMission,
@@ -549,6 +551,11 @@ const rankAndDedupeFindings = (findings) => {
549
551
  const MAX_CHILD_FINDINGS = 8;
550
552
  /** One child returns at most this many non-anchored concerns. */
551
553
  const MAX_CHILD_CONCERNS = 3;
554
+ /**
555
+ * One mandatory diff read plus one bounded context read for every path in a
556
+ * maximum-size unit. Keep the child and delegation reservation aligned.
557
+ */
558
+ const MAX_FILE_REVIEW_TOOL_CALLS = 24;
552
559
  const FileReviewToolkit = Toolkit.make(ReadFileDiff, ReadFile);
553
560
  const FileReviewToolkitLayer = FileReviewToolkit.toLayer({
554
561
  read_file_diff: readFileDiffHandler,
@@ -589,10 +596,12 @@ const fileReviewerInstructions = makeFileReviewerInstructions();
589
596
  /** The default per-unit child execution bounds. */
590
597
  const defaultFileReviewerPolicy = AgentPolicy.make({
591
598
  maxTurns: 8,
592
- maxToolCalls: 16,
593
- maxDuration: "4 minutes",
599
+ maxToolCalls: MAX_FILE_REVIEW_TOOL_CALLS,
600
+ maxDuration: "6 minutes",
594
601
  toolConcurrency: 2,
595
- tokenBudget: 2e5
602
+ tokenBudget: 2e5,
603
+ contextTokenLimit: 15e4,
604
+ onExhaustion: "fail"
596
605
  });
597
606
  /** The model-decoded delegation parameters: which unit to review. */
598
607
  var FileReviewRequest = class extends Schema.Class("@effect-agent/pr-review/FileReviewRequest")({
@@ -624,8 +633,8 @@ const fileReviewPolicy = SubagentPolicy.make({
624
633
  maxChildren: 8,
625
634
  maxConcurrency: 3,
626
635
  maxTurns: 8,
627
- maxToolCalls: 16,
628
- maxDuration: "4 minutes"
636
+ maxToolCalls: MAX_FILE_REVIEW_TOOL_CALLS,
637
+ maxDuration: "6 minutes"
629
638
  });
630
639
  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.";
631
640
  /**
@@ -636,23 +645,6 @@ const mapFileReviewChildFailure = (failure) => FileReviewUnitFailed.make({
636
645
  childErrorTag: failure._tag,
637
646
  message: (failure.message ?? "").slice(0, 400)
638
647
  });
639
- /** Exactly the failure union `Subagent.define` declares for this delegation. */
640
- const FileReviewDelegationFailure = Schema.Union([
641
- FileReviewUnitFailed,
642
- SubagentPrestartDenied,
643
- SubagentBudgetExhausted,
644
- SubagentProjectionFailure,
645
- SubagentExecutionFailure,
646
- ToolCallWaiting,
647
- SubagentDurabilityError
648
- ]);
649
- const DelegateFileReview = Tool.make("delegate_file_review", {
650
- description: delegationDescription,
651
- parameters: FileReviewRequest,
652
- success: FileReviewUnitResult,
653
- failure: FileReviewDelegationFailure,
654
- failureMode: "return"
655
- }).addDependency(AgentSpawner).addDependency(RunEventSink).addDependency(SubagentDurability).addDependency(IdGenerator).annotate(ToolExecutionClass, "readonly");
656
648
  var ListReviewUnitsQuery = class extends Schema.Class("@effect-agent/pr-review/ListReviewUnitsQuery")({
657
649
  /** Explicit constant keeps the zero-choice operation compatible with strict provider schemas. */
658
650
  scope: Schema.Literal("all") }) {};
@@ -669,7 +661,6 @@ const FanOutCoordinatorToolkitLayer = FanOutCoordinatorToolkit.toLayer({ list_re
669
661
  const source = yield* PullRequestSource;
670
662
  return planReviewUnits(yield* source.changedFiles, { totalChangedFiles: (yield* source.metadata).totalChangedFiles });
671
663
  }) });
672
- const FanOutReviewToolkit = Toolkit.make(ListReviewUnits, DelegateFileReview);
673
664
  /**
674
665
  * Build the coordinator's instructions. The same consumer guidance the
675
666
  * children receive is injected between the mission framing and the procedure
@@ -700,8 +691,10 @@ const defaultFanOutPolicy = AgentPolicy.make({
700
691
  maxToolCalls: 9,
701
692
  maxDuration: "15 minutes",
702
693
  toolConcurrency: 3,
703
- repeatedFailureLimit: 9,
704
- tokenBudget: 3e5
694
+ repeatedFailureLimit: 3,
695
+ tokenBudget: 3e5,
696
+ contextTokenLimit: 15e4,
697
+ onExhaustion: "final-answer"
705
698
  });
706
699
  const makeFileReviewerDefinition = (options = {}) => Agent.define("pr-file-reviewer", {
707
700
  input: FileReviewBrief,
@@ -715,25 +708,13 @@ const makeFileReviewerDefinition = (options = {}) => Agent.define("pr-file-revie
715
708
  surface: "read-only"
716
709
  }
717
710
  });
718
- const makeFanOutReviewerDefinition = (options = {}) => Agent.define("pr-fanout-reviewer", {
719
- input: ReviewMission,
720
- output: CodeReview,
721
- instructions: makeFanOutReviewInstructions(options),
722
- toolkit: FanOutReviewToolkit,
723
- policy: defaultFanOutPolicy,
724
- 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.",
725
- metadata: {
726
- deploymentClass: "E",
727
- surface: "read-only",
728
- delegation: "S1-attached"
729
- }
730
- });
731
711
  const makeFileReviewDelegation = (child) => Subagent.define("delegate_file_review", {
732
712
  description: delegationDescription,
733
713
  target: child,
734
714
  parameters: FileReviewRequest,
735
715
  success: FileReviewUnitResult,
736
716
  failure: FileReviewUnitFailed,
717
+ failureMode: "return",
737
718
  prepareInput: (request) => Effect.succeed(FileReviewBrief.make({
738
719
  unitId: request.unitId,
739
720
  paths: request.paths,
@@ -746,13 +727,34 @@ const makeFileReviewDelegation = (child) => Subagent.define("delegate_file_revie
746
727
  })),
747
728
  policy: fileReviewPolicy
748
729
  });
730
+ /**
731
+ * The coordinator-facing delegation Tool: the delegation's own first-party
732
+ * contained Tool plus the read-only execution class (the delegated child's
733
+ * whole tool surface is read-only). Effect AI resolves handlers by Tool name,
734
+ * so `SubagentRuntime.layer`'s handler serves this annotated copy unchanged.
735
+ */
736
+ const delegationToolFor = (delegation) => delegation.tool.annotate(ToolExecutionClass, "readonly");
737
+ const makeFanOutReviewerDefinition = (options, delegation) => Agent.define("pr-fanout-reviewer", {
738
+ input: ReviewMission,
739
+ output: CodeReview,
740
+ instructions: makeFanOutReviewInstructions(options),
741
+ toolkit: Toolkit.make(ListReviewUnits, delegationToolFor(delegation)),
742
+ policy: defaultFanOutPolicy,
743
+ 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.",
744
+ metadata: {
745
+ deploymentClass: "E",
746
+ surface: "read-only",
747
+ delegation: "S1-attached"
748
+ }
749
+ });
749
750
  /** Build one coherent fan-out suite: child, coordinator, and delegation. */
750
751
  const makeFanOutReviewSuite = (options = {}) => {
751
752
  const child = makeFileReviewerDefinition({ guidance: options.guidance });
753
+ const delegation = makeFileReviewDelegation(child);
752
754
  return {
753
755
  child,
754
- parent: makeFanOutReviewerDefinition(options),
755
- delegation: makeFileReviewDelegation(child)
756
+ parent: makeFanOutReviewerDefinition(options, delegation),
757
+ delegation
756
758
  };
757
759
  };
758
760
  const defaultSuite = makeFanOutReviewSuite();
@@ -762,6 +764,16 @@ const FileReviewer = defaultSuite.child;
762
764
  const FanOutReviewer = defaultSuite.parent;
763
765
  /** The default delegation over the default child. */
764
766
  const fileReviewDelegation = defaultSuite.delegation;
767
+ /** The default coordinator-facing delegation Tool (first-party contained mode). */
768
+ const DelegateFileReview = delegationToolFor(fileReviewDelegation);
769
+ /** The default coordinator Toolkit. */
770
+ const FanOutReviewToolkit = FanOutReviewer.toolkit;
771
+ /**
772
+ * The contained failure family the delegation can surface as result data
773
+ * (SUB-033), derived from the delegation itself so the coverage decoder can
774
+ * never diverge from what the runtime actually contains.
775
+ */
776
+ const FileReviewDelegationFailure = fileReviewDelegation.containedFailure;
765
777
  /** Runtime wiring: one delegation plus one explicit child Binding. */
766
778
  const fanOutHandlersLayerFor = (delegation) => (childBinding) => SubagentRuntime.layer(delegation, childBinding, { mapChildFailure: mapFileReviewChildFailure });
767
779
  /** Runtime wiring over the default delegation, mirroring the leaf example. */
@@ -1082,11 +1094,204 @@ const buildProfileMission = (metadata, files) => ReviewMission.make({
1082
1094
  changedFileCount: files.length
1083
1095
  });
1084
1096
  //#endregion
1097
+ //#region src/internal/retirement.ts
1098
+ const PositiveLine = Schema.Int.check(Schema.isGreaterThan(0));
1099
+ /** One previously posted review as observed through the retirement host. */
1100
+ var RetirableReview = class extends Schema.Class("@effect-agent/pr-review/RetirableReview")({
1101
+ reviewId: Schema.Int.check(Schema.isGreaterThan(0)),
1102
+ body: Schema.String.check(Schema.isMaxLength(6e4)),
1103
+ commitSha: Schema.NonEmptyString.check(Schema.isMaxLength(64)),
1104
+ authorNodeId: Schema.NullOr(Schema.NonEmptyString.check(Schema.isMaxLength(200))),
1105
+ submittedAt: Schema.NullOr(Schema.DateTimeUtc)
1106
+ }) {};
1107
+ /** One inline comment attached to a previously posted review. */
1108
+ var RetirableReviewComment = class extends Schema.Class("@effect-agent/pr-review/RetirableReviewComment")({
1109
+ nodeId: Schema.NonEmptyString.check(Schema.isMaxLength(200)),
1110
+ path: Schema.NonEmptyString.check(Schema.isMaxLength(500)),
1111
+ startLine: Schema.NullOr(PositiveLine),
1112
+ endLine: Schema.NullOr(PositiveLine),
1113
+ body: Schema.String.check(Schema.isMaxLength(65536))
1114
+ }) {};
1115
+ /** A GitHub retirement read or mutation failed. */
1116
+ var ReviewRetirementFailure = class extends Schema.TaggedError()("ReviewRetirementFailure", {
1117
+ operation: Schema.String,
1118
+ reason: Schema.String
1119
+ }) {
1120
+ get message() {
1121
+ return `Review retirement operation '${this.operation}' failed: ${this.reason}`;
1122
+ }
1123
+ };
1124
+ /**
1125
+ * Host-side GitHub operations used by retirement. Domain code never reaches
1126
+ * into REST or GraphQL directly, and deterministic tests substitute this port.
1127
+ */
1128
+ var ReviewRetirementHost = class extends Context.Service()("@effect-agent/pr-review/ReviewRetirementHost") {};
1129
+ /** Observable cosmetic work completed by one fail-open retirement pass. */
1130
+ var ReviewRetirementReport = class extends Schema.Class("@effect-agent/pr-review/ReviewRetirementReport")({
1131
+ reviewsRetired: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
1132
+ findingsResolved: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
1133
+ commentsMinimized: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
1134
+ failures: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
1135
+ }) {};
1136
+ const findingIdentity = (finding) => `${finding.path}\u0000${finding.startLine}\u0000${finding.endLine}\u0000${finding.title}`;
1137
+ const REVIEW_METADATA_PATTERN = /<!-- effect-agent-pr-review metadata\n[\s\S]*?\n-->/g;
1138
+ const FINGERPRINT_PATTERN = /<!-- effect-agent-pr-review fingerprint=sha256:[0-9a-f]{64} -->/g;
1139
+ const STATE_PATTERN = /<!-- effect-agent-pr-review state-v1:[A-Za-z0-9+/]+={0,2}\.[0-9a-f]{64} -->/g;
1140
+ const RETIRED_ORIGINAL_PATTERN = /<!-- effect-agent-pr-review retired-original:start -->\n([\s\S]*?)\n<!-- effect-agent-pr-review retired-original:end -->/;
1141
+ const MACHINE_COMMENT_PATTERN = new RegExp(`${REVIEW_METADATA_PATTERN.source}|${FINGERPRINT_PATTERN.source}|${STATE_PATTERN.source}`, "g");
1142
+ const VERDICT_CALLOUT_PATTERN = /^(?:> \[!(?:CAUTION|IMPORTANT)\]\n> [^\n]*(?:\n> [^\n]*)*|> (?:ℹ️|✅)[^\n]*)\n*/;
1143
+ const INLINE_FINDING_TITLE_PATTERN = /^\*\*\[(?:🛑 blocking|⚠️ important|💅 nit)\] ([^\n]+)\*\*$/;
1144
+ const MAX_REVIEW_BODY_CHARS = 6e4;
1145
+ /** The host-authored metadata marker is the authority gate for any edit. */
1146
+ const hasReviewMetadataMarker = (body) => /<!-- effect-agent-pr-review metadata\n/.test(body);
1147
+ const machineComments = (body) => Array.from(body.matchAll(MACHINE_COMMENT_PATTERN), (match) => match[0]);
1148
+ const originalVisibleBody = (body) => {
1149
+ const retired = RETIRED_ORIGINAL_PATTERN.exec(body)?.[1];
1150
+ if (retired !== void 0) return retired;
1151
+ return body.replace(MACHINE_COMMENT_PATTERN, "").trim().replace(VERDICT_CALLOUT_PATTERN, "");
1152
+ };
1153
+ const findingLocation = (finding) => `${finding.path}:${finding.startLine}${finding.endLine === finding.startLine ? "" : `-${finding.endLine}`}`;
1154
+ const renderRetiredBody = (input) => {
1155
+ const shortSha = input.currentState.reviewedHeadSha.slice(0, 7);
1156
+ const comments = machineComments(input.priorBody);
1157
+ const original = originalVisibleBody(input.priorBody);
1158
+ const resolved = input.resolvedFindings.length === 0 ? [] : [
1159
+ "### Findings resolved by later review",
1160
+ "",
1161
+ ...input.resolvedFindings.map((finding) => `- \`${findingLocation(finding)}\` ~~${finding.title}~~ · resolved at \`${shortSha}\``),
1162
+ ""
1163
+ ];
1164
+ const prefix = [
1165
+ `> ℹ️ Superseded — ${input.resolvedFindings.length} of ${input.priorState.unresolvedFindings.length} findings resolved at \`${shortSha}\`; see [the latest review](${input.currentReviewUrl}).`,
1166
+ "",
1167
+ "<details>",
1168
+ "<summary>Previous review details</summary>",
1169
+ "",
1170
+ ...resolved,
1171
+ "<!-- effect-agent-pr-review retired-original:start -->"
1172
+ ];
1173
+ const suffix = [
1174
+ "<!-- effect-agent-pr-review retired-original:end -->",
1175
+ "",
1176
+ "</details>",
1177
+ ...comments.length === 0 ? [] : ["", ...comments]
1178
+ ];
1179
+ const render = (visible) => [
1180
+ ...prefix,
1181
+ visible,
1182
+ ...suffix
1183
+ ].join("\n");
1184
+ if (render(original).length <= MAX_REVIEW_BODY_CHARS) return render(original);
1185
+ const truncationNotice = "\n\n_Original review content truncated during retirement._";
1186
+ const budget = Math.max(0, MAX_REVIEW_BODY_CHARS - render(truncationNotice).length);
1187
+ return render(`${original.slice(0, budget)}${truncationNotice}`);
1188
+ };
1189
+ /** Compute one prior review's resolved subset and deterministic retired body. */
1190
+ const decideReviewRetirement = (input) => {
1191
+ const current = new Set(input.currentState.unresolvedFindings.map(findingIdentity));
1192
+ const resolvedFindings = input.priorState.unresolvedFindings.filter((finding) => !current.has(findingIdentity(finding)));
1193
+ return {
1194
+ body: renderRetiredBody({
1195
+ ...input,
1196
+ resolvedFindings
1197
+ }),
1198
+ resolvedFindings,
1199
+ priorFindingCount: input.priorState.unresolvedFindings.length
1200
+ };
1201
+ };
1202
+ const inlineCommentIdentity = (comment) => {
1203
+ if (comment.startLine === null || comment.endLine === null) return void 0;
1204
+ const firstLine = comment.body.split("\n", 1)[0] ?? "";
1205
+ const title = INLINE_FINDING_TITLE_PATTERN.exec(firstLine)?.[1];
1206
+ return title === void 0 ? void 0 : findingIdentity({
1207
+ path: comment.path,
1208
+ startLine: comment.startLine,
1209
+ endLine: comment.endLine,
1210
+ title
1211
+ });
1212
+ };
1213
+ const failOpen = (effect, fallback, message) => effect.pipe(Effect.catch((error) => Effect.logWarning(`${message}: ${String(error)}`).pipe(Effect.as(fallback))));
1214
+ const isStrictlyOlderReview = (review, input) => {
1215
+ if (review.submittedAt === null) return false;
1216
+ const submittedAt = DateTime.toEpochMillis(review.submittedAt);
1217
+ const currentSubmittedAt = DateTime.toEpochMillis(input.currentSubmittedAt);
1218
+ return submittedAt < currentSubmittedAt || submittedAt === currentSubmittedAt && review.reviewId < input.currentReviewId;
1219
+ };
1220
+ /**
1221
+ * Retire every marker-bearing prior review against the newest posted state.
1222
+ * Every lookup, edit, and minimization is isolated: retirement is cosmetic
1223
+ * and can never change the run or check outcome.
1224
+ */
1225
+ const retireStaleReviews = Effect.fn("retireStaleReviews")(function* (input) {
1226
+ const host = yield* ReviewRetirementHost;
1227
+ const authenticator = yield* ReviewStateAuthenticator;
1228
+ if (authenticator.status !== "available") {
1229
+ yield* Effect.logWarning("Skipping stale-review retirement because authenticated review state is unavailable.");
1230
+ return ReviewRetirementReport.make({
1231
+ reviewsRetired: 0,
1232
+ findingsResolved: 0,
1233
+ commentsMinimized: 0,
1234
+ failures: 0
1235
+ });
1236
+ }
1237
+ let failures = 0;
1238
+ let reviewsRetired = 0;
1239
+ let findingsResolved = 0;
1240
+ let commentsMinimized = 0;
1241
+ const reviews = yield* failOpen(host.listReviews, void 0, "Could not list prior reviews");
1242
+ if (reviews === void 0) return ReviewRetirementReport.make({
1243
+ reviewsRetired,
1244
+ findingsResolved,
1245
+ commentsMinimized,
1246
+ failures: 1
1247
+ });
1248
+ for (const review of reviews) {
1249
+ if (review.authorNodeId !== input.currentAuthorNodeId || !isStrictlyOlderReview(review, input) || !hasReviewMetadataMarker(review.body)) continue;
1250
+ const priorState = yield* failOpen(authenticator.extract(review.body), Option.none(), `Could not authenticate prior review ${review.reviewId}`);
1251
+ if (Option.isNone(priorState)) continue;
1252
+ const decision = decideReviewRetirement({
1253
+ priorBody: review.body,
1254
+ priorState: priorState.value,
1255
+ currentState: input.currentState,
1256
+ currentReviewUrl: input.currentReviewUrl
1257
+ });
1258
+ if (yield* failOpen(host.updateBody(review.reviewId, decision.body).pipe(Effect.as(true)), false, `Could not retire prior review ${review.reviewId}`)) {
1259
+ reviewsRetired += 1;
1260
+ findingsResolved += decision.resolvedFindings.length;
1261
+ } else failures += 1;
1262
+ if (decision.resolvedFindings.length === 0) continue;
1263
+ const comments = yield* failOpen(host.listComments(review.reviewId), void 0, `Could not list inline comments for prior review ${review.reviewId}`);
1264
+ if (comments === void 0) {
1265
+ failures += 1;
1266
+ continue;
1267
+ }
1268
+ const resolved = new Set(decision.resolvedFindings.map(findingIdentity));
1269
+ for (const comment of comments) {
1270
+ const identity = inlineCommentIdentity(comment);
1271
+ if (identity === void 0 || !resolved.has(identity)) continue;
1272
+ if (yield* failOpen(host.minimizeComment(comment.nodeId).pipe(Effect.as(true)), false, `Could not minimize resolved inline comment ${comment.nodeId}`)) commentsMinimized += 1;
1273
+ else failures += 1;
1274
+ }
1275
+ }
1276
+ return ReviewRetirementReport.make({
1277
+ reviewsRetired,
1278
+ findingsResolved,
1279
+ commentsMinimized,
1280
+ failures
1281
+ });
1282
+ });
1283
+ //#endregion
1085
1284
  //#region src/internal/github.ts
1285
+ const defaultGraphqlUrl = (apiUrl) => apiUrl === "https://api.github.com" ? "https://api.github.com/graphql" : apiUrl.replace(/\/api\/v3$/, "/api/graphql");
1086
1286
  /** Which pull request to review and how to reach the API. */
1287
+ const DEFAULT_GITHUB_REVIEW_AUTHOR_LOGIN = "github-actions[bot]";
1087
1288
  var GitHubReviewTarget = class GitHubReviewTarget extends Context.Service()("@effect-agent/pr-review/GitHubReviewTarget") {
1088
1289
  static layer(config) {
1089
- return Layer.succeed(this, GitHubReviewTarget.of(config));
1290
+ return Layer.succeed(this, GitHubReviewTarget.of({
1291
+ ...config,
1292
+ graphqlUrl: config.graphqlUrl ?? defaultGraphqlUrl(config.apiUrl),
1293
+ reviewAuthorLogin: config.reviewAuthorLogin ?? "github-actions[bot]"
1294
+ }));
1090
1295
  }
1091
1296
  };
1092
1297
  /** A GitHub API call failed: transport, status, or payload decode. */
@@ -1121,16 +1326,46 @@ const GitHubFileWire = Schema.Struct({
1121
1326
  previous_filename: Schema.optionalKey(Schema.String)
1122
1327
  });
1123
1328
  const GitHubFilesPageWire = Schema.Array(GitHubFileWire);
1329
+ const GitHubActorWire = Schema.Struct({ node_id: Schema.String });
1124
1330
  const GitHubReviewWire = Schema.Struct({
1125
1331
  id: Schema.Int,
1126
- html_url: Schema.String
1332
+ html_url: Schema.String,
1333
+ user: Schema.NullOr(GitHubActorWire),
1334
+ submitted_at: Schema.NullOr(Schema.String)
1335
+ });
1336
+ const GitHubRetirableReviewWire = Schema.Struct({
1337
+ id: Schema.Int,
1338
+ body: Schema.NullOr(Schema.String),
1339
+ commit_id: Schema.String,
1340
+ user: Schema.NullOr(GitHubActorWire),
1341
+ submitted_at: Schema.NullOr(Schema.String)
1342
+ });
1343
+ const GitHubRetirableReviewsPageWire = Schema.Array(GitHubRetirableReviewWire);
1344
+ const GitHubReviewCommentWire = Schema.Struct({
1345
+ node_id: Schema.String,
1346
+ path: Schema.String,
1347
+ body: Schema.String,
1348
+ line: Schema.NullOr(Schema.Int),
1349
+ original_line: Schema.NullOr(Schema.Int),
1350
+ start_line: Schema.optionalKey(Schema.NullOr(Schema.Int)),
1351
+ original_start_line: Schema.optionalKey(Schema.NullOr(Schema.Int))
1127
1352
  });
1353
+ const GitHubReviewCommentsPageWire = Schema.Array(GitHubReviewCommentWire);
1354
+ const GitHubMinimizeCommentWire = Schema.Struct({
1355
+ data: Schema.optionalKey(Schema.NullOr(Schema.Struct({ minimizeComment: Schema.NullOr(Schema.Struct({ minimizedComment: Schema.NullOr(Schema.Struct({ isMinimized: Schema.Boolean })) })) }))),
1356
+ errors: Schema.optionalKey(Schema.Array(Schema.Struct({ message: Schema.String })))
1357
+ });
1358
+ /** Decode GitHub's external timestamp before it participates in mutation ordering. */
1359
+ const parseGitHubSubmittedAt = (value) => value === null ? null : Option.getOrNull(DateTime.make(value));
1128
1360
  /** The publication receipt callers report back to the operator. */
1129
1361
  var PublishedReview = class extends Schema.Class("@effect-agent/pr-review/PublishedReview")({
1130
1362
  reviewId: Schema.Int,
1131
1363
  url: Schema.String,
1132
1364
  event: Schema.String,
1133
- inlineComments: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
1365
+ inlineComments: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
1366
+ /** Actor and ordering boundary returned by the create-review response. */
1367
+ authorNodeId: Schema.NullOr(Schema.NonEmptyString.check(Schema.isMaxLength(200))),
1368
+ submittedAt: Schema.NullOr(Schema.DateTimeUtc)
1134
1369
  }) {};
1135
1370
  /** Posts one planned review; the ONLY mutating operation in this package. */
1136
1371
  var ReviewPublisher = class extends Context.Service()("@effect-agent/pr-review/ReviewPublisher") {};
@@ -1254,10 +1489,90 @@ const gitHubReviewPublisherLayer = Layer.effect(ReviewPublisher)(Effect.gen(func
1254
1489
  reviewId: wire.id,
1255
1490
  url: wire.html_url,
1256
1491
  event: plan.event,
1257
- inlineComments: plan.comments.length
1492
+ inlineComments: plan.comments.length,
1493
+ authorNodeId: wire.user?.node_id ?? null,
1494
+ submittedAt: parseGitHubSubmittedAt(wire.submitted_at)
1258
1495
  });
1259
1496
  }) });
1260
1497
  }));
1498
+ const MAX_RETIREMENT_PAGES = 5;
1499
+ const MINIMIZE_REVIEW_COMMENT_MUTATION = `mutation MinimizeReviewComment($subjectId: ID!) {
1500
+ minimizeComment(input: { subjectId: $subjectId, classifier: OUTDATED }) {
1501
+ minimizedComment { isMinimized }
1502
+ }
1503
+ }`;
1504
+ /** GitHub-backed host operations for cosmetic retirement after publication. */
1505
+ const gitHubReviewRetirementHostLayer = Layer.effect(ReviewRetirementHost)(Effect.gen(function* () {
1506
+ const target = yield* GitHubReviewTarget;
1507
+ const client = yield* HttpClient.HttpClient;
1508
+ const prefix = `${target.apiUrl}/repos/${target.repository}/pulls/${target.number}`;
1509
+ const asRetirementFailure = (operation) => (error) => ReviewRetirementFailure.make({
1510
+ operation,
1511
+ reason: `${error._tag}: ${error.message ?? "request failed"}`.slice(0, 2048)
1512
+ });
1513
+ const executeRetirement = (operation, request) => HttpClient.execute(request).pipe(Effect.flatMap(HttpClientResponse.filterStatusOk), Effect.mapError(asRetirementFailure(operation)), Effect.provideService(HttpClient.HttpClient, client));
1514
+ const decodeRetirement = (schema, operation) => {
1515
+ const decode = Schema.decodeUnknownEffect(schema);
1516
+ return (response) => response.json.pipe(Effect.mapError(asRetirementFailure(operation)), Effect.flatMap((body) => decode(body).pipe(Effect.mapError(asRetirementFailure(operation)))));
1517
+ };
1518
+ const listPaged = (input) => Effect.gen(function* () {
1519
+ const values = [];
1520
+ const perPage = 100;
1521
+ for (let page = 1; page <= MAX_RETIREMENT_PAGES; page += 1) {
1522
+ const response = yield* executeRetirement(input.operation, withCommonHeaders(HttpClientRequest.get(input.url).pipe(HttpClientRequest.acceptJson, HttpClientRequest.setUrlParams({
1523
+ per_page: String(perPage),
1524
+ page: String(page)
1525
+ })), target.token));
1526
+ const pageValues = yield* input.decode(response);
1527
+ values.push(...pageValues);
1528
+ if (pageValues.length < perPage) return values;
1529
+ }
1530
+ return yield* ReviewRetirementFailure.make({
1531
+ operation: input.operation,
1532
+ reason: `history exceeds the bounded ${MAX_RETIREMENT_PAGES * 100}-item lookup`
1533
+ });
1534
+ });
1535
+ return ReviewRetirementHost.of({
1536
+ listReviews: listPaged({
1537
+ operation: "listReviewsForRetirement",
1538
+ url: `${prefix}/reviews`,
1539
+ decode: decodeRetirement(GitHubRetirableReviewsPageWire, "listReviewsForRetirement")
1540
+ }).pipe(Effect.map((reviews) => reviews.map((review) => RetirableReview.make({
1541
+ reviewId: review.id,
1542
+ body: review.body ?? "",
1543
+ commitSha: review.commit_id,
1544
+ authorNodeId: review.user?.node_id ?? null,
1545
+ submittedAt: parseGitHubSubmittedAt(review.submitted_at)
1546
+ })))),
1547
+ listComments: (reviewId) => listPaged({
1548
+ operation: "listReviewCommentsForRetirement",
1549
+ url: `${prefix}/reviews/${reviewId}/comments`,
1550
+ decode: decodeRetirement(GitHubReviewCommentsPageWire, "listReviewCommentsForRetirement")
1551
+ }).pipe(Effect.map((comments) => comments.map((comment) => {
1552
+ const endLine = comment.line ?? comment.original_line;
1553
+ const startLine = comment.start_line ?? comment.original_start_line ?? endLine;
1554
+ return RetirableReviewComment.make({
1555
+ nodeId: comment.node_id,
1556
+ path: comment.path,
1557
+ startLine,
1558
+ endLine,
1559
+ body: comment.body
1560
+ });
1561
+ }))),
1562
+ updateBody: (reviewId, body) => executeRetirement("updateReview", withCommonHeaders(HttpClientRequest.put(`${prefix}/reviews/${reviewId}`).pipe(HttpClientRequest.acceptJson, HttpClientRequest.bodyJsonUnsafe({ body })), target.token)).pipe(Effect.asVoid),
1563
+ minimizeComment: (nodeId) => Effect.gen(function* () {
1564
+ const response = yield* executeRetirement("minimizeComment", withCommonHeaders(HttpClientRequest.post(target.graphqlUrl).pipe(HttpClientRequest.acceptJson, HttpClientRequest.bodyJsonUnsafe({
1565
+ query: MINIMIZE_REVIEW_COMMENT_MUTATION,
1566
+ variables: { subjectId: nodeId }
1567
+ })), target.token));
1568
+ const wire = yield* decodeRetirement(GitHubMinimizeCommentWire, "minimizeComment")(response);
1569
+ if ((wire.errors?.length ?? 0) > 0 || wire.data?.minimizeComment?.minimizedComment?.isMinimized !== true) return yield* ReviewRetirementFailure.make({
1570
+ operation: "minimizeComment",
1571
+ reason: wire.errors?.map((error) => error.message).join("; ").slice(0, 2048) ?? "GitHub did not confirm comment minimization"
1572
+ });
1573
+ })
1574
+ });
1575
+ }));
1261
1576
  /** Reading the pull request's previously posted reviews failed. */
1262
1577
  var PriorReviewLookupFailure = class extends Schema.TaggedError()("PriorReviewLookupFailure", { reason: Schema.String }) {
1263
1578
  get message() {
@@ -1296,6 +1611,7 @@ const gitHubPriorReviewsLayer = Layer.effect(PriorReviews)(Effect.gen(function*
1296
1611
  const target = yield* GitHubReviewTarget;
1297
1612
  const client = yield* HttpClient.HttpClient;
1298
1613
  const prefix = `${target.apiUrl}/repos/${target.repository}/pulls/${target.number}`;
1614
+ const reviewAuthorLogin = target.reviewAuthorLogin ?? "github-actions[bot]";
1299
1615
  const decodePage = Schema.decodeUnknownEffect(GitHubPriorReviewsPageWire);
1300
1616
  const asLookupFailure = (error) => PriorReviewLookupFailure.make({ reason: `${error._tag}: ${error.message ?? "request failed"}`.slice(0, 2048) });
1301
1617
  const readMarkers = (authenticator) => Effect.gen(function* () {
@@ -1308,7 +1624,7 @@ const gitHubPriorReviewsLayer = Layer.effect(PriorReviews)(Effect.gen(function*
1308
1624
  page: String(page)
1309
1625
  })), target.token)).pipe(Effect.flatMap(HttpClientResponse.filterStatusOk), Effect.mapError(asLookupFailure))).json.pipe(Effect.mapError(asLookupFailure), Effect.flatMap((body) => decodePage(body).pipe(Effect.mapError(asLookupFailure))));
1310
1626
  for (const wire of wires) {
1311
- if (wire.user?.login !== "github-actions[bot]" || wire.user.type !== "Bot") continue;
1627
+ if (wire.user?.login.toLowerCase() !== reviewAuthorLogin.toLowerCase() || wire.user.type !== "Bot") continue;
1312
1628
  const fingerprint = extractFingerprint(wire.body ?? "");
1313
1629
  if (fingerprint !== void 0) latest = Option.some(fingerprint);
1314
1630
  if (Option.isSome(authenticator)) {
@@ -1355,6 +1671,6 @@ const fingerprintUnchanged = (current) => Effect.gen(function* () {
1355
1671
  return Option.isSome(latest) && latest.value === current;
1356
1672
  });
1357
1673
  //#endregion
1358
- export { ListReviewUnits as $, MAX_CHANGED_FILES as $t, toStoredConcern as A, FileSliceQuery as At, FanOutCoordinatorToolkit as B, ReviewFinding as Bt, StoredReviewFinding as C, rankAndDedupeFindings as Ct, fromStoredFinding as D, FileDiffQuery as Dt, fromStoredConcern as E, CodeReview as Et, FINGERPRINT_MARKER_LENGTH as F, MAX_FINDINGS as Ft, FileReviewDelegationFailure as G, clampMaxFindings as Gt, FanOutReviewToolkit as H, ReviewToolkit as Ht, computeChangesetFingerprint as I, PullRequestReviewer as It, FileReviewToolkit as J, makeReviewInstructions as Jt, FileReviewReport as K, defaultReviewPolicy as Kt, extractFingerprint as L, ReadFile as Lt, unavailableReviewStateAuthenticatorLayer as M, ListChangedFiles as Mt, validateReviewState as N, ListChangedFilesQuery as Nt, selectReviewRange as O, FileDiffView as Ot, webCryptoReviewStateAuthenticatorLayer as P, MAX_CONCERNS as Pt, FileReviewer as Q, reviewInstructions as Qt, renderFingerprintMarker as R, ReadFileDiff as Rt, StoredReviewConcern as S, planReviewUnits as St, computeProfileFingerprint as T, ChangedFilesView as Tt, FanOutReviewer as U, ReviewToolkitLayer as Ut, FanOutCoordinatorToolkitLayer as V, ReviewMission as Vt, FileReviewBrief as W, ReviewVerdict as Wt, FileReviewUnitFailed as X, readFileHandler as Xt, FileReviewToolkitLayer as Y, readFileDiffHandler as Yt, FileReviewUnitResult as Z, resolveGuidance as Zt, ReviewState as _, MAX_UNIT_FILES as _t, PublishedReview as a, normalizeRepoRelativePath as an, fanOutHandlersLayer as at, ReviewStateMarker as b, ReviewUnitPlan as bt, gitHubPriorReviewsLayer as c, ChangedPath as cn, fileReviewDelegation as ct, GitCommitSha as d, parsePatch as dn, makeFanOutReviewInstructions as dt, MAX_FILE_CHARS as en, ListReviewUnitsQuery as et, MAX_REVIEW_STATE_MARKER_CHARS as f, makeFanOutReviewSuite as ft, ReviewScopeMode as g, MAX_REVIEW_UNITS as gt, ReviewMode as h, MAX_MERGED_FINDINGS as ht, PriorReviews as i, ReviewInputViolation as in, defaultFileReviewerPolicy as it, toStoredFinding as j, FindingSeverity as jt, selectedPullRequestSourceLayer as k, FileSlice as kt, gitHubPullRequestSourceLayer as l, annotatePatch as ln, fileReviewPolicy as lt, ReviewHeadComparison as m, mapFileReviewChildFailure as mt, GitHubReviewTarget as n, PullRequestSource as nn, MAX_CHILD_FINDINGS as nt, ReviewPublisher as o, ChangedFile as on, fanOutHandlersLayerFor as ot, ReviewExecutionContext as p, makeFileReviewerInstructions as pt, FileReviewRequest as q, listChangedFilesHandler as qt, PriorReviewLookupFailure as r, PullRequestSourceFailure as rn, defaultFanOutPolicy as rt, fingerprintUnchanged as s, ChangedFileStatus as sn, fanOutReviewInstructions as st, GitHubApiFailure as t, PullRequestMetadata as tn, MAX_CHILD_CONCERNS as tt, gitHubReviewPublisherLayer as u, commentableLines as un, fileReviewerInstructions as ut, ReviewStateAuthenticationFailure as v, ReviewUnit as vt, buildProfileMission as w, ChangedFileSummary as wt, ReviewStateMarkerTooLarge as x, UNIT_CHANGED_LINE_BUDGET as xt, ReviewStateAuthenticator as y, ReviewUnitId as yt, DelegateFileReview as z, ReviewConcern as zt };
1674
+ export { FanOutReviewToolkit as $, ReviewMission as $t, ReviewStateAuthenticator as A, ReviewUnit as At, selectedPullRequestSourceLayer as B, FileDiffView as Bt, MAX_REVIEW_STATE_MARKER_CHARS as C, parsePatch as Cn, makeFanOutReviewInstructions as Ct, ReviewScopeMode as D, MAX_MERGED_FINDINGS as Dt, ReviewMode as E, mapFileReviewChildFailure as Et, buildProfileMission as F, rankAndDedupeFindings as Ft, webCryptoReviewStateAuthenticatorLayer as G, ListChangedFilesQuery as Gt, toStoredFinding as H, FileSliceQuery as Ht, computeProfileFingerprint as I, ChangedFileSummary as It, extractFingerprint as J, PullRequestReviewer as Jt, FINGERPRINT_MARKER_LENGTH as K, MAX_CONCERNS as Kt, fromStoredConcern as L, ChangedFilesView as Lt, ReviewStateMarkerTooLarge as M, ReviewUnitPlan as Mt, StoredReviewConcern as N, UNIT_CHANGED_LINE_BUDGET as Nt, ReviewState as O, MAX_REVIEW_UNITS as Ot, StoredReviewFinding as P, planReviewUnits as Pt, FanOutCoordinatorToolkitLayer as Q, ReviewFinding as Qt, fromStoredFinding as R, CodeReview as Rt, GitCommitSha as S, commentableLines as Sn, fileReviewerInstructions as St, ReviewHeadComparison as T, makeFileReviewerInstructions as Tt, unavailableReviewStateAuthenticatorLayer as U, FindingSeverity as Ut, toStoredConcern as V, FileSlice as Vt, validateReviewState as W, ListChangedFiles as Wt, DelegateFileReview as X, ReadFileDiff as Xt, renderFingerprintMarker as Y, ReadFile as Yt, FanOutCoordinatorToolkit as Z, ReviewConcern as Zt, ReviewRetirementHost as _, normalizeRepoRelativePath as _n, fanOutHandlersLayer as _t, PriorReviews as a, listChangedFilesHandler as an, FileReviewToolkit as at, hasReviewMetadataMarker as b, ChangedPath as bn, fileReviewDelegation as bt, fingerprintUnchanged as c, readFileHandler as cn, FileReviewUnitResult as ct, gitHubReviewPublisherLayer as d, MAX_CHANGED_FILES as dn, ListReviewUnitsQuery as dt, ReviewToolkit as en, FanOutReviewer as et, gitHubReviewRetirementHostLayer as f, MAX_FILE_CHARS as fn, MAX_CHILD_CONCERNS as ft, ReviewRetirementFailure as g, ReviewInputViolation as gn, defaultFileReviewerPolicy as gt, RetirableReviewComment as h, PullRequestSourceFailure as hn, defaultFanOutPolicy as ht, PriorReviewLookupFailure as i, defaultReviewPolicy as in, FileReviewRequest as it, ReviewStateMarker as j, ReviewUnitId as jt, ReviewStateAuthenticationFailure as k, MAX_UNIT_FILES as kt, gitHubPriorReviewsLayer as l, resolveGuidance as ln, FileReviewer as lt, RetirableReview as m, PullRequestSource as mn, MAX_FILE_REVIEW_TOOL_CALLS as mt, GitHubApiFailure as n, ReviewVerdict as nn, FileReviewDelegationFailure as nt, PublishedReview as o, makeReviewInstructions as on, FileReviewToolkitLayer as ot, parseGitHubSubmittedAt as p, PullRequestMetadata as pn, MAX_CHILD_FINDINGS as pt, computeChangesetFingerprint as q, MAX_FINDINGS as qt, GitHubReviewTarget as r, clampMaxFindings as rn, FileReviewReport as rt, ReviewPublisher as s, readFileDiffHandler as sn, FileReviewUnitFailed as st, DEFAULT_GITHUB_REVIEW_AUTHOR_LOGIN as t, ReviewToolkitLayer as tn, FileReviewBrief as tt, gitHubPullRequestSourceLayer as u, reviewInstructions as un, ListReviewUnits as ut, ReviewRetirementReport as v, ChangedFile as vn, fanOutHandlersLayerFor as vt, ReviewExecutionContext as w, makeFanOutReviewSuite as wt, retireStaleReviews as x, annotatePatch as xn, fileReviewPolicy as xt, decideReviewRetirement as y, ChangedFileStatus as yn, fanOutReviewInstructions as yt, selectReviewRange as z, FileDiffQuery as zt };
1359
1675
 
1360
- //# sourceMappingURL=github-bwQ2V-wb.mjs.map
1676
+ //# sourceMappingURL=github-5TCFrxfX.mjs.map