@effect-agent/pr-review 0.1.0-beta.6 → 0.1.0-beta.9

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
@@ -415,7 +415,8 @@ const defaultReviewPolicy = AgentPolicy.make({
415
415
  maxDuration: "8 minutes",
416
416
  toolConcurrency: 2,
417
417
  tokenBudget: 3e5,
418
- onExhaustion: "fail"
418
+ contextTokenLimit: 15e4,
419
+ onExhaustion: "final-answer"
419
420
  });
420
421
  const PullRequestReviewer = Agent.define("pr-reviewer", {
421
422
  input: ReviewMission,
@@ -599,6 +600,7 @@ const defaultFileReviewerPolicy = AgentPolicy.make({
599
600
  maxDuration: "4 minutes",
600
601
  toolConcurrency: 2,
601
602
  tokenBudget: 2e5,
603
+ contextTokenLimit: 15e4,
602
604
  onExhaustion: "fail"
603
605
  });
604
606
  /** The model-decoded delegation parameters: which unit to review. */
@@ -643,23 +645,6 @@ const mapFileReviewChildFailure = (failure) => FileReviewUnitFailed.make({
643
645
  childErrorTag: failure._tag,
644
646
  message: (failure.message ?? "").slice(0, 400)
645
647
  });
646
- /** Exactly the failure union `Subagent.define` declares for this delegation. */
647
- const FileReviewDelegationFailure = Schema.Union([
648
- FileReviewUnitFailed,
649
- SubagentPrestartDenied,
650
- SubagentBudgetExhausted,
651
- SubagentProjectionFailure,
652
- SubagentExecutionFailure,
653
- ToolCallWaiting,
654
- SubagentDurabilityError
655
- ]);
656
- const DelegateFileReview = Tool.make("delegate_file_review", {
657
- description: delegationDescription,
658
- parameters: FileReviewRequest,
659
- success: FileReviewUnitResult,
660
- failure: FileReviewDelegationFailure,
661
- failureMode: "return"
662
- }).addDependency(AgentSpawner).addDependency(RunEventSink).addDependency(SubagentDurability).addDependency(IdGenerator).annotate(ToolExecutionClass, "readonly");
663
648
  var ListReviewUnitsQuery = class extends Schema.Class("@effect-agent/pr-review/ListReviewUnitsQuery")({
664
649
  /** Explicit constant keeps the zero-choice operation compatible with strict provider schemas. */
665
650
  scope: Schema.Literal("all") }) {};
@@ -676,7 +661,6 @@ const FanOutCoordinatorToolkitLayer = FanOutCoordinatorToolkit.toLayer({ list_re
676
661
  const source = yield* PullRequestSource;
677
662
  return planReviewUnits(yield* source.changedFiles, { totalChangedFiles: (yield* source.metadata).totalChangedFiles });
678
663
  }) });
679
- const FanOutReviewToolkit = Toolkit.make(ListReviewUnits, DelegateFileReview);
680
664
  /**
681
665
  * Build the coordinator's instructions. The same consumer guidance the
682
666
  * children receive is injected between the mission framing and the procedure
@@ -707,9 +691,10 @@ const defaultFanOutPolicy = AgentPolicy.make({
707
691
  maxToolCalls: 9,
708
692
  maxDuration: "15 minutes",
709
693
  toolConcurrency: 3,
710
- repeatedFailureLimit: 9,
694
+ repeatedFailureLimit: 3,
711
695
  tokenBudget: 3e5,
712
- onExhaustion: "fail"
696
+ contextTokenLimit: 15e4,
697
+ onExhaustion: "final-answer"
713
698
  });
714
699
  const makeFileReviewerDefinition = (options = {}) => Agent.define("pr-file-reviewer", {
715
700
  input: FileReviewBrief,
@@ -723,25 +708,13 @@ const makeFileReviewerDefinition = (options = {}) => Agent.define("pr-file-revie
723
708
  surface: "read-only"
724
709
  }
725
710
  });
726
- const makeFanOutReviewerDefinition = (options = {}) => Agent.define("pr-fanout-reviewer", {
727
- input: ReviewMission,
728
- output: CodeReview,
729
- instructions: makeFanOutReviewInstructions(options),
730
- toolkit: FanOutReviewToolkit,
731
- policy: defaultFanOutPolicy,
732
- 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.",
733
- metadata: {
734
- deploymentClass: "E",
735
- surface: "read-only",
736
- delegation: "S1-attached"
737
- }
738
- });
739
711
  const makeFileReviewDelegation = (child) => Subagent.define("delegate_file_review", {
740
712
  description: delegationDescription,
741
713
  target: child,
742
714
  parameters: FileReviewRequest,
743
715
  success: FileReviewUnitResult,
744
716
  failure: FileReviewUnitFailed,
717
+ failureMode: "return",
745
718
  prepareInput: (request) => Effect.succeed(FileReviewBrief.make({
746
719
  unitId: request.unitId,
747
720
  paths: request.paths,
@@ -754,13 +727,34 @@ const makeFileReviewDelegation = (child) => Subagent.define("delegate_file_revie
754
727
  })),
755
728
  policy: fileReviewPolicy
756
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
+ });
757
750
  /** Build one coherent fan-out suite: child, coordinator, and delegation. */
758
751
  const makeFanOutReviewSuite = (options = {}) => {
759
752
  const child = makeFileReviewerDefinition({ guidance: options.guidance });
753
+ const delegation = makeFileReviewDelegation(child);
760
754
  return {
761
755
  child,
762
- parent: makeFanOutReviewerDefinition(options),
763
- delegation: makeFileReviewDelegation(child)
756
+ parent: makeFanOutReviewerDefinition(options, delegation),
757
+ delegation
764
758
  };
765
759
  };
766
760
  const defaultSuite = makeFanOutReviewSuite();
@@ -770,6 +764,16 @@ const FileReviewer = defaultSuite.child;
770
764
  const FanOutReviewer = defaultSuite.parent;
771
765
  /** The default delegation over the default child. */
772
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;
773
777
  /** Runtime wiring: one delegation plus one explicit child Binding. */
774
778
  const fanOutHandlersLayerFor = (delegation) => (childBinding) => SubagentRuntime.layer(delegation, childBinding, { mapChildFailure: mapFileReviewChildFailure });
775
779
  /** Runtime wiring over the default delegation, mirroring the leaf example. */
@@ -1090,11 +1094,202 @@ const buildProfileMission = (metadata, files) => ReviewMission.make({
1090
1094
  changedFileCount: files.length
1091
1095
  });
1092
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
1093
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");
1094
1286
  /** Which pull request to review and how to reach the API. */
1095
1287
  var GitHubReviewTarget = class GitHubReviewTarget extends Context.Service()("@effect-agent/pr-review/GitHubReviewTarget") {
1096
1288
  static layer(config) {
1097
- return Layer.succeed(this, GitHubReviewTarget.of(config));
1289
+ return Layer.succeed(this, GitHubReviewTarget.of({
1290
+ ...config,
1291
+ graphqlUrl: config.graphqlUrl ?? defaultGraphqlUrl(config.apiUrl)
1292
+ }));
1098
1293
  }
1099
1294
  };
1100
1295
  /** A GitHub API call failed: transport, status, or payload decode. */
@@ -1129,16 +1324,46 @@ const GitHubFileWire = Schema.Struct({
1129
1324
  previous_filename: Schema.optionalKey(Schema.String)
1130
1325
  });
1131
1326
  const GitHubFilesPageWire = Schema.Array(GitHubFileWire);
1327
+ const GitHubActorWire = Schema.Struct({ node_id: Schema.String });
1132
1328
  const GitHubReviewWire = Schema.Struct({
1133
1329
  id: Schema.Int,
1134
- html_url: Schema.String
1330
+ html_url: Schema.String,
1331
+ user: Schema.NullOr(GitHubActorWire),
1332
+ submitted_at: Schema.NullOr(Schema.String)
1333
+ });
1334
+ const GitHubRetirableReviewWire = Schema.Struct({
1335
+ id: Schema.Int,
1336
+ body: Schema.NullOr(Schema.String),
1337
+ commit_id: Schema.String,
1338
+ user: Schema.NullOr(GitHubActorWire),
1339
+ submitted_at: Schema.NullOr(Schema.String)
1340
+ });
1341
+ const GitHubRetirableReviewsPageWire = Schema.Array(GitHubRetirableReviewWire);
1342
+ const GitHubReviewCommentWire = Schema.Struct({
1343
+ node_id: Schema.String,
1344
+ path: Schema.String,
1345
+ body: Schema.String,
1346
+ line: Schema.NullOr(Schema.Int),
1347
+ original_line: Schema.NullOr(Schema.Int),
1348
+ start_line: Schema.optionalKey(Schema.NullOr(Schema.Int)),
1349
+ original_start_line: Schema.optionalKey(Schema.NullOr(Schema.Int))
1135
1350
  });
1351
+ const GitHubReviewCommentsPageWire = Schema.Array(GitHubReviewCommentWire);
1352
+ const GitHubMinimizeCommentWire = Schema.Struct({
1353
+ data: Schema.optionalKey(Schema.NullOr(Schema.Struct({ minimizeComment: Schema.NullOr(Schema.Struct({ minimizedComment: Schema.NullOr(Schema.Struct({ isMinimized: Schema.Boolean })) })) }))),
1354
+ errors: Schema.optionalKey(Schema.Array(Schema.Struct({ message: Schema.String })))
1355
+ });
1356
+ /** Decode GitHub's external timestamp before it participates in mutation ordering. */
1357
+ const parseGitHubSubmittedAt = (value) => value === null ? null : Option.getOrNull(DateTime.make(value));
1136
1358
  /** The publication receipt callers report back to the operator. */
1137
1359
  var PublishedReview = class extends Schema.Class("@effect-agent/pr-review/PublishedReview")({
1138
1360
  reviewId: Schema.Int,
1139
1361
  url: Schema.String,
1140
1362
  event: Schema.String,
1141
- inlineComments: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
1363
+ inlineComments: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
1364
+ /** Actor and ordering boundary returned by the create-review response. */
1365
+ authorNodeId: Schema.NullOr(Schema.NonEmptyString.check(Schema.isMaxLength(200))),
1366
+ submittedAt: Schema.NullOr(Schema.DateTimeUtc)
1142
1367
  }) {};
1143
1368
  /** Posts one planned review; the ONLY mutating operation in this package. */
1144
1369
  var ReviewPublisher = class extends Context.Service()("@effect-agent/pr-review/ReviewPublisher") {};
@@ -1262,10 +1487,90 @@ const gitHubReviewPublisherLayer = Layer.effect(ReviewPublisher)(Effect.gen(func
1262
1487
  reviewId: wire.id,
1263
1488
  url: wire.html_url,
1264
1489
  event: plan.event,
1265
- inlineComments: plan.comments.length
1490
+ inlineComments: plan.comments.length,
1491
+ authorNodeId: wire.user?.node_id ?? null,
1492
+ submittedAt: parseGitHubSubmittedAt(wire.submitted_at)
1266
1493
  });
1267
1494
  }) });
1268
1495
  }));
1496
+ const MAX_RETIREMENT_PAGES = 5;
1497
+ const MINIMIZE_REVIEW_COMMENT_MUTATION = `mutation MinimizeReviewComment($subjectId: ID!) {
1498
+ minimizeComment(input: { subjectId: $subjectId, classifier: OUTDATED }) {
1499
+ minimizedComment { isMinimized }
1500
+ }
1501
+ }`;
1502
+ /** GitHub-backed host operations for cosmetic retirement after publication. */
1503
+ const gitHubReviewRetirementHostLayer = Layer.effect(ReviewRetirementHost)(Effect.gen(function* () {
1504
+ const target = yield* GitHubReviewTarget;
1505
+ const client = yield* HttpClient.HttpClient;
1506
+ const prefix = `${target.apiUrl}/repos/${target.repository}/pulls/${target.number}`;
1507
+ const asRetirementFailure = (operation) => (error) => ReviewRetirementFailure.make({
1508
+ operation,
1509
+ reason: `${error._tag}: ${error.message ?? "request failed"}`.slice(0, 2048)
1510
+ });
1511
+ const executeRetirement = (operation, request) => HttpClient.execute(request).pipe(Effect.flatMap(HttpClientResponse.filterStatusOk), Effect.mapError(asRetirementFailure(operation)), Effect.provideService(HttpClient.HttpClient, client));
1512
+ const decodeRetirement = (schema, operation) => {
1513
+ const decode = Schema.decodeUnknownEffect(schema);
1514
+ return (response) => response.json.pipe(Effect.mapError(asRetirementFailure(operation)), Effect.flatMap((body) => decode(body).pipe(Effect.mapError(asRetirementFailure(operation)))));
1515
+ };
1516
+ const listPaged = (input) => Effect.gen(function* () {
1517
+ const values = [];
1518
+ const perPage = 100;
1519
+ for (let page = 1; page <= MAX_RETIREMENT_PAGES; page += 1) {
1520
+ const response = yield* executeRetirement(input.operation, withCommonHeaders(HttpClientRequest.get(input.url).pipe(HttpClientRequest.acceptJson, HttpClientRequest.setUrlParams({
1521
+ per_page: String(perPage),
1522
+ page: String(page)
1523
+ })), target.token));
1524
+ const pageValues = yield* input.decode(response);
1525
+ values.push(...pageValues);
1526
+ if (pageValues.length < perPage) return values;
1527
+ }
1528
+ return yield* ReviewRetirementFailure.make({
1529
+ operation: input.operation,
1530
+ reason: `history exceeds the bounded ${MAX_RETIREMENT_PAGES * 100}-item lookup`
1531
+ });
1532
+ });
1533
+ return ReviewRetirementHost.of({
1534
+ listReviews: listPaged({
1535
+ operation: "listReviewsForRetirement",
1536
+ url: `${prefix}/reviews`,
1537
+ decode: decodeRetirement(GitHubRetirableReviewsPageWire, "listReviewsForRetirement")
1538
+ }).pipe(Effect.map((reviews) => reviews.map((review) => RetirableReview.make({
1539
+ reviewId: review.id,
1540
+ body: review.body ?? "",
1541
+ commitSha: review.commit_id,
1542
+ authorNodeId: review.user?.node_id ?? null,
1543
+ submittedAt: parseGitHubSubmittedAt(review.submitted_at)
1544
+ })))),
1545
+ listComments: (reviewId) => listPaged({
1546
+ operation: "listReviewCommentsForRetirement",
1547
+ url: `${prefix}/reviews/${reviewId}/comments`,
1548
+ decode: decodeRetirement(GitHubReviewCommentsPageWire, "listReviewCommentsForRetirement")
1549
+ }).pipe(Effect.map((comments) => comments.map((comment) => {
1550
+ const endLine = comment.line ?? comment.original_line;
1551
+ const startLine = comment.start_line ?? comment.original_start_line ?? endLine;
1552
+ return RetirableReviewComment.make({
1553
+ nodeId: comment.node_id,
1554
+ path: comment.path,
1555
+ startLine,
1556
+ endLine,
1557
+ body: comment.body
1558
+ });
1559
+ }))),
1560
+ updateBody: (reviewId, body) => executeRetirement("updateReview", withCommonHeaders(HttpClientRequest.put(`${prefix}/reviews/${reviewId}`).pipe(HttpClientRequest.acceptJson, HttpClientRequest.bodyJsonUnsafe({ body })), target.token)).pipe(Effect.asVoid),
1561
+ minimizeComment: (nodeId) => Effect.gen(function* () {
1562
+ const response = yield* executeRetirement("minimizeComment", withCommonHeaders(HttpClientRequest.post(target.graphqlUrl).pipe(HttpClientRequest.acceptJson, HttpClientRequest.bodyJsonUnsafe({
1563
+ query: MINIMIZE_REVIEW_COMMENT_MUTATION,
1564
+ variables: { subjectId: nodeId }
1565
+ })), target.token));
1566
+ const wire = yield* decodeRetirement(GitHubMinimizeCommentWire, "minimizeComment")(response);
1567
+ if ((wire.errors?.length ?? 0) > 0 || wire.data?.minimizeComment?.minimizedComment?.isMinimized !== true) return yield* ReviewRetirementFailure.make({
1568
+ operation: "minimizeComment",
1569
+ reason: wire.errors?.map((error) => error.message).join("; ").slice(0, 2048) ?? "GitHub did not confirm comment minimization"
1570
+ });
1571
+ })
1572
+ });
1573
+ }));
1269
1574
  /** Reading the pull request's previously posted reviews failed. */
1270
1575
  var PriorReviewLookupFailure = class extends Schema.TaggedError()("PriorReviewLookupFailure", { reason: Schema.String }) {
1271
1576
  get message() {
@@ -1363,6 +1668,6 @@ const fingerprintUnchanged = (current) => Effect.gen(function* () {
1363
1668
  return Option.isSome(latest) && latest.value === current;
1364
1669
  });
1365
1670
  //#endregion
1366
- export { ListReviewUnits as $, reviewInstructions as $t, toStoredConcern as A, FileSlice as At, FanOutCoordinatorToolkit as B, ReviewConcern as Bt, StoredReviewFinding as C, planReviewUnits as Ct, fromStoredFinding as D, CodeReview as Dt, fromStoredConcern as E, ChangedFilesView as Et, FINGERPRINT_MARKER_LENGTH as F, MAX_CONCERNS as Ft, FileReviewDelegationFailure as G, ReviewVerdict as Gt, FanOutReviewToolkit as H, ReviewMission as Ht, computeChangesetFingerprint as I, MAX_FINDINGS as It, FileReviewToolkit as J, listChangedFilesHandler as Jt, FileReviewReport as K, clampMaxFindings as Kt, extractFingerprint as L, PullRequestReviewer as Lt, unavailableReviewStateAuthenticatorLayer as M, FindingSeverity as Mt, validateReviewState as N, ListChangedFiles as Nt, selectReviewRange as O, FileDiffQuery as Ot, webCryptoReviewStateAuthenticatorLayer as P, ListChangedFilesQuery as Pt, FileReviewer as Q, resolveGuidance as Qt, renderFingerprintMarker as R, ReadFile as Rt, StoredReviewConcern as S, UNIT_CHANGED_LINE_BUDGET as St, computeProfileFingerprint as T, ChangedFileSummary as Tt, FanOutReviewer as U, ReviewToolkit as Ut, FanOutCoordinatorToolkitLayer as V, ReviewFinding as Vt, FileReviewBrief as W, ReviewToolkitLayer as Wt, FileReviewUnitFailed as X, readFileDiffHandler as Xt, FileReviewToolkitLayer as Y, makeReviewInstructions as Yt, FileReviewUnitResult as Z, readFileHandler as Zt, ReviewState as _, MAX_REVIEW_UNITS as _t, PublishedReview as a, ReviewInputViolation as an, defaultFileReviewerPolicy as at, ReviewStateMarker as b, ReviewUnitId as bt, gitHubPriorReviewsLayer as c, ChangedFileStatus as cn, fanOutReviewInstructions as ct, GitCommitSha as d, commentableLines as dn, fileReviewerInstructions as dt, MAX_CHANGED_FILES as en, ListReviewUnitsQuery as et, MAX_REVIEW_STATE_MARKER_CHARS as f, parsePatch as fn, makeFanOutReviewInstructions as ft, ReviewScopeMode as g, MAX_MERGED_FINDINGS as gt, ReviewMode as h, mapFileReviewChildFailure as ht, PriorReviews as i, PullRequestSourceFailure as in, defaultFanOutPolicy as it, toStoredFinding as j, FileSliceQuery as jt, selectedPullRequestSourceLayer as k, FileDiffView as kt, gitHubPullRequestSourceLayer as l, ChangedPath as ln, fileReviewDelegation as lt, ReviewHeadComparison as m, makeFileReviewerInstructions as mt, GitHubReviewTarget as n, PullRequestMetadata as nn, MAX_CHILD_FINDINGS as nt, ReviewPublisher as o, normalizeRepoRelativePath as on, fanOutHandlersLayer as ot, ReviewExecutionContext as p, makeFanOutReviewSuite as pt, FileReviewRequest as q, defaultReviewPolicy as qt, PriorReviewLookupFailure as r, PullRequestSource as rn, MAX_FILE_REVIEW_TOOL_CALLS as rt, fingerprintUnchanged as s, ChangedFile as sn, fanOutHandlersLayerFor as st, GitHubApiFailure as t, MAX_FILE_CHARS as tn, MAX_CHILD_CONCERNS as tt, gitHubReviewPublisherLayer as u, annotatePatch as un, fileReviewPolicy as ut, ReviewStateAuthenticationFailure as v, MAX_UNIT_FILES as vt, buildProfileMission as w, rankAndDedupeFindings as wt, ReviewStateMarkerTooLarge as x, ReviewUnitPlan as xt, ReviewStateAuthenticator as y, ReviewUnit as yt, DelegateFileReview as z, ReadFileDiff as zt };
1671
+ export { FanOutReviewer as $, ReviewToolkit as $t, ReviewStateMarker as A, ReviewUnitId as At, toStoredConcern as B, FileSlice as Bt, ReviewExecutionContext as C, makeFanOutReviewSuite as Ct, ReviewState as D, MAX_REVIEW_UNITS as Dt, ReviewScopeMode as E, MAX_MERGED_FINDINGS as Et, computeProfileFingerprint as F, ChangedFileSummary as Ft, FINGERPRINT_MARKER_LENGTH as G, MAX_CONCERNS as Gt, unavailableReviewStateAuthenticatorLayer as H, FindingSeverity as Ht, fromStoredConcern as I, ChangedFilesView as It, renderFingerprintMarker as J, ReadFile as Jt, computeChangesetFingerprint as K, MAX_FINDINGS as Kt, fromStoredFinding as L, CodeReview as Lt, StoredReviewConcern as M, UNIT_CHANGED_LINE_BUDGET as Mt, StoredReviewFinding as N, planReviewUnits as Nt, ReviewStateAuthenticationFailure as O, MAX_UNIT_FILES as Ot, buildProfileMission as P, rankAndDedupeFindings as Pt, FanOutReviewToolkit as Q, ReviewMission as Qt, selectReviewRange as R, FileDiffQuery as Rt, MAX_REVIEW_STATE_MARKER_CHARS as S, parsePatch as Sn, makeFanOutReviewInstructions as St, ReviewMode as T, mapFileReviewChildFailure as Tt, validateReviewState as U, ListChangedFiles as Ut, toStoredFinding as V, FileSliceQuery as Vt, webCryptoReviewStateAuthenticatorLayer as W, ListChangedFilesQuery as Wt, FanOutCoordinatorToolkit as X, ReviewConcern as Xt, DelegateFileReview as Y, ReadFileDiff as Yt, FanOutCoordinatorToolkitLayer as Z, ReviewFinding as Zt, ReviewRetirementReport as _, ChangedFile as _n, fanOutHandlersLayerFor as _t, PublishedReview as a, makeReviewInstructions as an, FileReviewToolkitLayer as at, retireStaleReviews as b, annotatePatch as bn, fileReviewPolicy as bt, gitHubPriorReviewsLayer as c, resolveGuidance as cn, FileReviewer as ct, gitHubReviewRetirementHostLayer as d, MAX_FILE_CHARS as dn, MAX_CHILD_CONCERNS as dt, ReviewToolkitLayer as en, FileReviewBrief as et, parseGitHubSubmittedAt as f, PullRequestMetadata as fn, MAX_CHILD_FINDINGS as ft, ReviewRetirementHost as g, normalizeRepoRelativePath as gn, fanOutHandlersLayer as gt, ReviewRetirementFailure as h, ReviewInputViolation as hn, defaultFileReviewerPolicy as ht, PriorReviews as i, listChangedFilesHandler as in, FileReviewToolkit as it, ReviewStateMarkerTooLarge as j, ReviewUnitPlan as jt, ReviewStateAuthenticator as k, ReviewUnit as kt, gitHubPullRequestSourceLayer as l, reviewInstructions as ln, ListReviewUnits as lt, RetirableReviewComment as m, PullRequestSourceFailure as mn, defaultFanOutPolicy as mt, GitHubReviewTarget as n, clampMaxFindings as nn, FileReviewReport as nt, ReviewPublisher as o, readFileDiffHandler as on, FileReviewUnitFailed as ot, RetirableReview as p, PullRequestSource as pn, MAX_FILE_REVIEW_TOOL_CALLS as pt, extractFingerprint as q, PullRequestReviewer as qt, PriorReviewLookupFailure as r, defaultReviewPolicy as rn, FileReviewRequest as rt, fingerprintUnchanged as s, readFileHandler as sn, FileReviewUnitResult as st, GitHubApiFailure as t, ReviewVerdict as tn, FileReviewDelegationFailure as tt, gitHubReviewPublisherLayer as u, MAX_CHANGED_FILES as un, ListReviewUnitsQuery as ut, decideReviewRetirement as v, ChangedFileStatus as vn, fanOutReviewInstructions as vt, ReviewHeadComparison as w, makeFileReviewerInstructions as wt, GitCommitSha as x, commentableLines as xn, fileReviewerInstructions as xt, hasReviewMetadataMarker as y, ChangedPath as yn, fileReviewDelegation as yt, selectedPullRequestSourceLayer as z, FileDiffView as zt };
1367
1672
 
1368
- //# sourceMappingURL=github-CcCtaWZD.mjs.map
1673
+ //# sourceMappingURL=github-Lfa_ox-u.mjs.map