@jphutchins/code-review 0.1.0-alpha.36 → 0.1.0-alpha.37

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.
package/dist/index.js CHANGED
@@ -4,14 +4,151 @@ import { readFileSync, writeFileSync, statSync, copyFileSync, readdirSync } from
4
4
  import { randomBytes } from 'crypto';
5
5
  import { resolve as resolve$1, join, dirname, basename, extname } from 'path';
6
6
  import { Eta } from 'eta';
7
+ import * as t from 'io-ts';
7
8
  import parseDiff from 'parse-diff';
8
9
  import { Ajv2020 } from 'ajv/dist/2020.js';
9
10
  import _addFormats from 'ajv-formats';
10
- import * as t from 'io-ts';
11
11
  import { execFile } from 'child_process';
12
12
  import { performance } from 'perf_hooks';
13
13
  import { PathReporter } from 'io-ts/lib/PathReporter.js';
14
14
 
15
+ var SeverityCodec = t.union([
16
+ t.literal("critical"),
17
+ t.literal("major"),
18
+ t.literal("minor"),
19
+ t.literal("nit")
20
+ ]);
21
+ var SideCodec = t.union([t.literal("RIGHT"), t.literal("LEFT")]);
22
+ var VerdictCodec = t.union([
23
+ t.literal("approve"),
24
+ t.literal("comment"),
25
+ t.literal("changes"),
26
+ t.literal("error")
27
+ ]);
28
+ var LineNumber = t.refinement(
29
+ t.number,
30
+ (n) => Number.isInteger(n) && n >= 1,
31
+ "LineNumber"
32
+ );
33
+ var Confidence = t.refinement(t.number, (n) => n >= 0 && n <= 1, "Confidence");
34
+ var SCHEMA_VERSION_RE = /^(0|[1-9]\d*)\.(\d+)\.(\d+)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
35
+ var SchemaVersion = t.refinement(
36
+ t.string,
37
+ (s) => SCHEMA_VERSION_RE.test(s),
38
+ "SchemaVersion"
39
+ );
40
+ var FindingShape = t.intersection([
41
+ t.type({
42
+ path: t.string,
43
+ start_line: LineNumber,
44
+ end_line: LineNumber,
45
+ severity: SeverityCodec,
46
+ title: t.string,
47
+ description: t.string,
48
+ reasoning: t.string,
49
+ confidence: Confidence
50
+ }),
51
+ t.partial({
52
+ side: SideCodec,
53
+ code: t.string,
54
+ code_url: t.string,
55
+ recommendation: t.string,
56
+ patch: t.string
57
+ })
58
+ ]);
59
+ var EndGeStart = t.refinement(
60
+ FindingShape,
61
+ (f) => f.end_line >= f.start_line,
62
+ "EndGeStart"
63
+ );
64
+ var FindingCodec = t.exact(EndGeStart);
65
+ var FindingsCodec = t.exact(
66
+ t.type({
67
+ schema_version: SchemaVersion,
68
+ summary: t.string,
69
+ verdict: VerdictCodec,
70
+ findings: t.array(FindingCodec)
71
+ })
72
+ );
73
+ var TriageCodec = t.type({
74
+ safe: t.boolean,
75
+ reasons: t.string
76
+ });
77
+ var TokenCount = t.refinement(
78
+ t.number,
79
+ (n) => Number.isInteger(n) && n >= 0,
80
+ "TokenCount"
81
+ );
82
+ var ModelUsageEntryCodec = t.intersection([
83
+ t.type({
84
+ model: t.string,
85
+ input_tokens: TokenCount,
86
+ output_tokens: TokenCount
87
+ }),
88
+ t.partial({
89
+ cache_read_tokens: TokenCount,
90
+ cache_write_tokens: TokenCount
91
+ })
92
+ ]);
93
+ var ResultEnvelopeCodec = t.intersection([
94
+ t.type({
95
+ schema_version: t.string,
96
+ findings: FindingsCodec,
97
+ models: t.array(ModelUsageEntryCodec),
98
+ turns: TokenCount,
99
+ duration_ms: TokenCount
100
+ }),
101
+ t.partial({
102
+ vendor_cost_usd: t.union([t.number, t.null]),
103
+ route: t.string,
104
+ effort: t.string,
105
+ // The run produced a notice rather than a completed review (security-gate block, agent kill, no
106
+ // recoverable findings). An empty `findings` array alone can't say this — a genuine clean review
107
+ // is also empty — so the render suppresses "clean review" and the sticky precedence guard refuses
108
+ // to bury a completed review under it. Absent ⇒ a completed review.
109
+ incomplete: t.boolean
110
+ })
111
+ ]);
112
+ var ModelPricesCodec = t.type({
113
+ in: t.number,
114
+ out: t.number,
115
+ cache_read: t.number,
116
+ cache_write: t.number
117
+ });
118
+ var PriceMapCodec = t.type({
119
+ _updated: t.string,
120
+ _unit: t.string,
121
+ models: t.record(t.string, ModelPricesCodec)
122
+ });
123
+ var TestFailureCodec = t.intersection([
124
+ t.type({ name: t.string }),
125
+ t.partial({ message: t.string })
126
+ ]);
127
+ var TestSummaryCodec = t.intersection([
128
+ t.type({
129
+ passed: t.number,
130
+ failed: t.number,
131
+ total: t.number
132
+ }),
133
+ t.partial({
134
+ failures: t.array(TestFailureCodec)
135
+ })
136
+ ]);
137
+ var DEFAULT_SCHEMA_VERSION = "0.5.0";
138
+ var emptyFindings = (summary) => ({
139
+ schema_version: DEFAULT_SCHEMA_VERSION,
140
+ summary,
141
+ verdict: "comment",
142
+ findings: []
143
+ });
144
+ var incompleteFindings = (summary) => ({
145
+ schema_version: DEFAULT_SCHEMA_VERSION,
146
+ summary,
147
+ verdict: "error",
148
+ findings: []
149
+ });
150
+ var isIncompleteFindings = (findings) => findings.verdict === "error" && findings.findings.length === 0;
151
+
15
152
  // src/cost.ts
16
153
  var defaultWarn = (message) => {
17
154
  process.stderr.write(`${message}
@@ -179,6 +316,13 @@ var encodeMarker = (document, jsonUrl, limit) => {
179
316
  return marker ? `${AGENTS_STOP_DIRECTIVE}
180
317
  ${marker}` : "";
181
318
  };
319
+ var decodeBase64Json = (b64) => {
320
+ try {
321
+ return JSON.parse(Buffer.from(b64, "base64").toString("utf-8"));
322
+ } catch {
323
+ return void 0;
324
+ }
325
+ };
182
326
  var findingsPointer = (findings, jsonUrl, limit = EMBED_LIMIT) => encodeMarker(findings, jsonUrl, limit);
183
327
  var findingPointer = (finding, schemaVersion, jsonUrl, limit = EMBED_LIMIT) => encodeMarker({ schema_version: schemaVersion, findings: [finding] }, jsonUrl, limit);
184
328
  var ZERO_SHA = "0000000000000000000000000000000000000000";
@@ -189,21 +333,40 @@ var parseReviewedSha = (body) => {
189
333
  var REVIEW_COMPLETE_MARKER = "<!-- review-complete -->";
190
334
  var parseReviewComplete = (body) => body.includes(REVIEW_COMPLETE_MARKER);
191
335
  var parseFindingsMarker = (body) => {
192
- const match = /<!-- code-review:findings-json;base64 ([A-Za-z0-9+/=]+) -->/.exec(body);
193
- const b64 = match?.[1];
336
+ const b64 = /<!-- code-review:findings-json;base64 ([A-Za-z0-9+/=]+) -->/.exec(body)?.[1];
194
337
  if (b64 === void 0) return null;
195
- try {
196
- return JSON.parse(Buffer.from(b64, "base64").toString("utf-8"));
197
- } catch {
198
- return null;
199
- }
338
+ return decodeBase64Json(b64) ?? null;
339
+ };
340
+ var ROUNDS_RE = /<!-- code-review:rounds;base64 ([A-Za-z0-9+/=]+) -->/;
341
+ var isSeverityCounts = (u) => typeof u === "object" && u !== null && ["critical", "major", "minor", "nit"].every((k) => {
342
+ const v = u[k];
343
+ return typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
344
+ });
345
+ var parseRounds = (body) => {
346
+ const b64 = ROUNDS_RE.exec(body)?.[1];
347
+ if (b64 === void 0) return [];
348
+ const decoded = decodeBase64Json(b64);
349
+ return Array.isArray(decoded) ? decoded.filter(isSeverityCounts) : [];
350
+ };
351
+ var roundsMarker = (rounds) => rounds.length === 0 ? "" : `<!-- code-review:rounds;base64 ${Buffer.from(JSON.stringify(rounds), "utf-8").toString("base64")} -->`;
352
+ var roundChip = (c) => {
353
+ const parts = ["critical", "major", "minor", "nit"].filter((k) => c[k] > 0).map((k) => `${severityEmoji(k)}${String(c[k])}`);
354
+ return parts.length === 0 ? "clean" : parts.join(" ");
355
+ };
356
+ var TRAJECTORY_CHIPS = 8;
357
+ var roundsSummary = (rounds) => {
358
+ if (rounds.length === 0) return "";
359
+ const chips = rounds.slice(-TRAJECTORY_CHIPS).map(roundChip);
360
+ const trajectory = rounds.length > TRAJECTORY_CHIPS ? `\u2026 \u2192 ${chips.join(" \u2192 ")}` : chips.join(" \u2192 ");
361
+ return `**Round ${String(rounds.length)}** \xB7 ${trajectory}`;
200
362
  };
201
363
  var carryForwardMarkers = (body) => {
202
364
  const findings = /<!-- code-review:findings-json[^>]*-->/.exec(body)?.[0];
203
365
  const reviewedSha = /<!-- reviewed-sha: [0-9a-fA-F]{40} -->/.exec(body)?.[0];
366
+ const rounds = ROUNDS_RE.exec(body)?.[0];
204
367
  const findingsBlock = findings ? `${AGENTS_STOP_DIRECTIVE}
205
368
  ${findings}` : void 0;
206
- return [findingsBlock, reviewedSha].filter((m) => m !== void 0).join("\n\n");
369
+ return [findingsBlock, reviewedSha, rounds].filter((m) => m !== void 0).join("\n\n");
207
370
  };
208
371
  var escapeFence = (text) => text.replace(/```/g, "`` ` ``");
209
372
  var projectPatch = (patch) => {
@@ -243,7 +406,7 @@ var render = (input) => {
243
406
  const eta = new Eta({ autoTrim: false });
244
407
  const usageAvailable = input.envelope !== null;
245
408
  const hasUsage = input.envelope !== null && input.envelope.models.length > 0;
246
- const incomplete = input.incomplete ?? input.envelope?.incomplete ?? false;
409
+ const incomplete = (input.incomplete ?? input.envelope?.incomplete ?? false) || isIncompleteFindings(input.findings);
247
410
  const costReport = input.envelope ? computeCost(input.envelope.models, input.prices) : null;
248
411
  const pricesProvided = input.pricesProvided ?? true;
249
412
  const route = input.route ?? input.envelope?.route ?? null;
@@ -270,6 +433,8 @@ var render = (input) => {
270
433
  runUrl: input.runUrl ?? null,
271
434
  jsonUrl: input.jsonUrl ?? null,
272
435
  findingsPointer: input.findingsPointer ?? findingsPointer(input.findings, input.jsonUrl),
436
+ roundsMarker: roundsMarker(input.rounds ?? []),
437
+ roundsSummary: roundsSummary(input.rounds ?? []),
273
438
  reviewUrl: input.reviewUrl ?? null,
274
439
  formatTokens: (n) => Number.isFinite(n) && n >= 0 ? n.toLocaleString("en-US") : "\u2014",
275
440
  // N/A (never a false $0.00) when no real price map was provided — real tokens, no rates to price them.
@@ -287,6 +452,8 @@ var render = (input) => {
287
452
  return "\u{1F4AC} comment";
288
453
  case "changes":
289
454
  return "\u{1F527} changes requested";
455
+ case "error":
456
+ return "\u{1F6E0}\uFE0F no review verdict";
290
457
  default:
291
458
  return `\u2753 ${v}`;
292
459
  }
@@ -525,132 +692,6 @@ var readTranscriptTree = (mainPath) => {
525
692
  missing: mainText === null
526
693
  };
527
694
  };
528
- var SeverityCodec = t.union([
529
- t.literal("critical"),
530
- t.literal("major"),
531
- t.literal("minor"),
532
- t.literal("nit")
533
- ]);
534
- var SideCodec = t.union([t.literal("RIGHT"), t.literal("LEFT")]);
535
- var VerdictCodec = t.union([t.literal("approve"), t.literal("comment"), t.literal("changes")]);
536
- var LineNumber = t.refinement(
537
- t.number,
538
- (n) => Number.isInteger(n) && n >= 1,
539
- "LineNumber"
540
- );
541
- var Confidence = t.refinement(t.number, (n) => n >= 0 && n <= 1, "Confidence");
542
- var SCHEMA_VERSION_RE = /^(0|[1-9]\d*)\.(\d+)\.(\d+)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
543
- var SchemaVersion = t.refinement(
544
- t.string,
545
- (s) => SCHEMA_VERSION_RE.test(s),
546
- "SchemaVersion"
547
- );
548
- var FindingShape = t.intersection([
549
- t.type({
550
- path: t.string,
551
- start_line: LineNumber,
552
- end_line: LineNumber,
553
- severity: SeverityCodec,
554
- title: t.string,
555
- description: t.string,
556
- reasoning: t.string,
557
- confidence: Confidence
558
- }),
559
- t.partial({
560
- side: SideCodec,
561
- code: t.string,
562
- code_url: t.string,
563
- recommendation: t.string,
564
- patch: t.string
565
- })
566
- ]);
567
- var EndGeStart = t.refinement(
568
- FindingShape,
569
- (f) => f.end_line >= f.start_line,
570
- "EndGeStart"
571
- );
572
- var FindingCodec = t.exact(EndGeStart);
573
- var FindingsCodec = t.exact(
574
- t.type({
575
- schema_version: SchemaVersion,
576
- summary: t.string,
577
- verdict: VerdictCodec,
578
- findings: t.array(FindingCodec)
579
- })
580
- );
581
- var TriageCodec = t.type({
582
- safe: t.boolean,
583
- reasons: t.string
584
- });
585
- var TokenCount = t.refinement(
586
- t.number,
587
- (n) => Number.isInteger(n) && n >= 0,
588
- "TokenCount"
589
- );
590
- var ModelUsageEntryCodec = t.intersection([
591
- t.type({
592
- model: t.string,
593
- input_tokens: TokenCount,
594
- output_tokens: TokenCount
595
- }),
596
- t.partial({
597
- cache_read_tokens: TokenCount,
598
- cache_write_tokens: TokenCount
599
- })
600
- ]);
601
- var ResultEnvelopeCodec = t.intersection([
602
- t.type({
603
- schema_version: t.string,
604
- findings: FindingsCodec,
605
- models: t.array(ModelUsageEntryCodec),
606
- turns: TokenCount,
607
- duration_ms: TokenCount
608
- }),
609
- t.partial({
610
- vendor_cost_usd: t.union([t.number, t.null]),
611
- route: t.string,
612
- effort: t.string,
613
- // The run produced a notice rather than a completed review (security-gate block, agent kill, no
614
- // recoverable findings). An empty `findings` array alone can't say this — a genuine clean review
615
- // is also empty — so the render suppresses "clean review" and the sticky precedence guard refuses
616
- // to bury a completed review under it. Absent ⇒ a completed review.
617
- incomplete: t.boolean
618
- })
619
- ]);
620
- var ModelPricesCodec = t.type({
621
- in: t.number,
622
- out: t.number,
623
- cache_read: t.number,
624
- cache_write: t.number
625
- });
626
- var PriceMapCodec = t.type({
627
- _updated: t.string,
628
- _unit: t.string,
629
- models: t.record(t.string, ModelPricesCodec)
630
- });
631
- var TestFailureCodec = t.intersection([
632
- t.type({ name: t.string }),
633
- t.partial({ message: t.string })
634
- ]);
635
- var TestSummaryCodec = t.intersection([
636
- t.type({
637
- passed: t.number,
638
- failed: t.number,
639
- total: t.number
640
- }),
641
- t.partial({
642
- failures: t.array(TestFailureCodec)
643
- })
644
- ]);
645
- var DEFAULT_SCHEMA_VERSION = "0.4.0";
646
- var noticeFindings = (summary) => ({
647
- schema_version: DEFAULT_SCHEMA_VERSION,
648
- summary,
649
- verdict: "comment",
650
- findings: []
651
- });
652
-
653
- // src/validate.ts
654
695
  var addFormats = _addFormats;
655
696
  var validatorCache = /* @__PURE__ */ new Map();
656
697
  var compileSchema = (schemaPath) => {
@@ -990,11 +1031,24 @@ var NOTICE_KINDS = [
990
1031
  "no-output"
991
1032
  ];
992
1033
  var isNoticeKind = (s) => NOTICE_KINDS.some((k) => k === s);
1034
+ var UNSAFE_IN_SUMMARY = /[\n\r`<>|]/;
1035
+ var MAX_NAMED_HOSTS = 20;
1036
+ var parseAgentAllowlist = (sandboxConfig) => {
1037
+ const domains = sandboxConfig?.network?.allowedDomains;
1038
+ return Array.isArray(domains) ? domains.filter(
1039
+ (d) => typeof d === "string" && d.length <= 256 && !UNSAFE_IN_SUMMARY.test(d)
1040
+ ).slice(0, MAX_NAMED_HOSTS) : [];
1041
+ };
1042
+ var egressNote = (agentAllowlist) => agentAllowlist.length === 0 ? "" : `
1043
+
1044
+ If the review failed because the agent could not reach a host it needed, note that its network egress is jailed to only ${agentAllowlist.map((host) => `\`${host}\``).join(
1045
+ ", "
1046
+ )}. Add any missing host to the agent's egress allowlist \u2014 the reusable workflow's \`extra_endpoints\` input, or a single-file workflow's \`--extra\` flag on \`sandbox-config\`.`;
993
1047
  var blockquote = (text) => text.replaceAll("\n", "\n> ");
994
1048
  var reasoned = (lead, noReason, reasons) => typeof reasons === "string" && reasons.trim() !== "" ? `${lead}
995
1049
 
996
1050
  > ${blockquote(reasons)}` : noReason;
997
- var noticeSummary = (kind, reasons) => {
1051
+ var noticeSummary = (kind, reasons, agentAllowlist) => {
998
1052
  switch (kind) {
999
1053
  case "security-blocked":
1000
1054
  return reasoned(
@@ -1007,25 +1061,25 @@ var noticeSummary = (kind, reasons) => {
1007
1061
  "### \u{1F6E0}\uFE0F Security gate could not evaluate\n\nThe security triage could not produce a verdict (operational error), so the review failed closed \u2014 this is an infrastructure failure, not a finding about this diff. Re-run to retry a transient fault; a persistent one is a configuration issue (see the workflow logs). The triage step reported:",
1008
1062
  "### \u{1F6E0}\uFE0F Security gate could not evaluate\n\nThe security triage could not produce a verdict (operational error), so the review failed closed \u2014 this is an infrastructure failure, not a finding about this diff. Re-run to retry a transient fault; a persistent one is a configuration issue (see the workflow logs).",
1009
1063
  reasons
1010
- );
1064
+ ) + egressNote(agentAllowlist);
1011
1065
  case "setup-failed":
1012
1066
  return "### \u{1F6E0}\uFE0F Review did not run\n\nThe review job failed before the security triage could run (e.g. dependency install or environment setup). See the workflow logs \u2014 this is an infrastructure failure, not a security verdict.";
1013
1067
  case "checkout-failed":
1014
1068
  return "### \u26A0\uFE0F Could not check out the PR head\n\nThe PR head commit could not be fetched or checked out (it may have been force-pushed away, or is otherwise unavailable), so the review was skipped rather than run against the wrong tree. See workflow logs.";
1015
1069
  case "no-output":
1016
- return "### \u26A0\uFE0F Review did not complete\n\nThe diff passed triage but the review produced no output. See workflow logs.";
1070
+ return "### \u26A0\uFE0F Review did not complete\n\nThe diff passed triage but the review produced no output. See workflow logs." + egressNote(agentAllowlist);
1017
1071
  }
1018
1072
  };
1019
1073
  var noticeEnvelope = (summary) => ({
1020
1074
  schema_version: DEFAULT_SCHEMA_VERSION,
1021
- findings: noticeFindings(summary),
1075
+ findings: incompleteFindings(summary),
1022
1076
  models: [],
1023
1077
  turns: 0,
1024
1078
  duration_ms: 0,
1025
1079
  vendor_cost_usd: null,
1026
1080
  incomplete: true
1027
1081
  });
1028
- var buildNoticeEnvelope = (kind, reasons) => noticeEnvelope(noticeSummary(kind, reasons));
1082
+ var buildNoticeEnvelope = (kind, reasons, agentAllowlist = []) => noticeEnvelope(noticeSummary(kind, reasons, agentAllowlist));
1029
1083
  var buildUnknownNoticeEnvelope = (kind) => noticeEnvelope(
1030
1084
  `### \u26A0\uFE0F Review could not be rendered
1031
1085
 
@@ -1035,6 +1089,14 @@ var identity = (decoded) => decoded;
1035
1089
  var findingsTable = [
1036
1090
  {
1037
1091
  minor: "0.4",
1092
+ defaultVersion: "0.4.0",
1093
+ schemaFile: "findings.schema.json",
1094
+ codec: FindingsCodec,
1095
+ normalize: identity,
1096
+ latest: false
1097
+ },
1098
+ {
1099
+ minor: "0.5",
1038
1100
  defaultVersion: DEFAULT_SCHEMA_VERSION,
1039
1101
  schemaFile: "findings.schema.json",
1040
1102
  codec: FindingsCodec,
@@ -1533,6 +1595,7 @@ var post = async (input, ghApi = runGhApi) => {
1533
1595
  );
1534
1596
  const existingComplete = existingSticky !== null && parseReviewComplete(existingSticky.body);
1535
1597
  const wouldBuryCompleted = (incomplete) => incomplete && existingComplete;
1598
+ const priorRounds = existingSticky !== null ? parseRounds(existingSticky.body) : [];
1536
1599
  const leaveInPlace = () => {
1537
1600
  process.stderr.write(
1538
1601
  `Review did not complete and the sticky already reflects a completed review \u2014 leaving it in place
@@ -1549,7 +1612,7 @@ var post = async (input, ghApi = runGhApi) => {
1549
1612
  const inlineTemplate = readFileSync(input.inlineTemplatePath, "utf-8");
1550
1613
  const renderNotice = (message) => formatMarkdown(
1551
1614
  render({
1552
- findings: noticeFindings(`### \u26A0\uFE0F ${message}`),
1615
+ findings: incompleteFindings(`### \u26A0\uFE0F ${message}`),
1553
1616
  envelope: null,
1554
1617
  incomplete: true,
1555
1618
  prices: decodedPrices.right,
@@ -1558,6 +1621,7 @@ var post = async (input, ghApi = runGhApi) => {
1558
1621
  route: input.route,
1559
1622
  reviewedSha: input.headSha,
1560
1623
  effort: input.effort,
1624
+ rounds: priorRounds,
1561
1625
  runUrl: input.runUrl,
1562
1626
  jsonUrl: input.jsonUrl,
1563
1627
  postedAt: input.postedAt
@@ -1590,16 +1654,20 @@ var post = async (input, ghApi = runGhApi) => {
1590
1654
  const envelope = loadEnvelope(input.envelopePath);
1591
1655
  const testReport = input.testReportPath ? loadTestReport(input.testReportPath) : void 0;
1592
1656
  if (envelope === null) {
1657
+ const envelopelessIncomplete = isIncompleteFindings(findings);
1658
+ if (wouldBuryCompleted(envelopelessIncomplete)) leaveInPlace();
1593
1659
  const body = formatMarkdown(
1594
1660
  render({
1595
1661
  findings,
1596
1662
  envelope: null,
1663
+ incomplete: envelopelessIncomplete,
1597
1664
  prices: decodedPrices.right,
1598
1665
  pricesProvided: input.pricesProvided,
1599
1666
  template,
1600
1667
  route: input.route,
1601
1668
  reviewedSha: input.headSha,
1602
1669
  effort: input.effort,
1670
+ rounds: priorRounds,
1603
1671
  testReport,
1604
1672
  inlineDisposition: { kind: "no-envelope" },
1605
1673
  runUrl: input.runUrl,
@@ -1613,7 +1681,7 @@ var post = async (input, ghApi = runGhApi) => {
1613
1681
  );
1614
1682
  process.exit(0);
1615
1683
  }
1616
- const thisIncomplete = envelope.incomplete === true;
1684
+ const thisIncomplete = envelope.incomplete === true || isIncompleteFindings(findings);
1617
1685
  if (wouldBuryCompleted(thisIncomplete)) leaveInPlace();
1618
1686
  const findingsMarker = findingsPointer(findings, input.jsonUrl);
1619
1687
  const {
@@ -1635,6 +1703,9 @@ var post = async (input, ghApi = runGhApi) => {
1635
1703
  }
1636
1704
  const botReviews = await fetchBotReviews(input.repo, prNumber, input.botLogin, ghApi);
1637
1705
  const initialDisposition = comments.length === 0 && strays.length > 0 ? { kind: "none-in-diff" } : void 0;
1706
+ const currentCounts = computeSeverityCounts(findings.findings);
1707
+ const effectiveRoute = input.route ?? envelope.route;
1708
+ const rounds = effectiveRoute === "full review" && !thisIncomplete ? [...priorRounds, currentCounts] : priorRounds;
1638
1709
  const commonRenderInput = {
1639
1710
  findings,
1640
1711
  envelope,
@@ -1646,7 +1717,8 @@ var post = async (input, ghApi = runGhApi) => {
1646
1717
  reviewedSha: input.headSha,
1647
1718
  effort: input.effort,
1648
1719
  testReport,
1649
- severityCounts: computeSeverityCounts(findings.findings),
1720
+ severityCounts: currentCounts,
1721
+ rounds,
1650
1722
  strays,
1651
1723
  runUrl: input.runUrl,
1652
1724
  jsonUrl: input.jsonUrl,
@@ -2657,7 +2729,16 @@ var nativeTelemetry = (native, meta) => resolveTelemetry(
2657
2729
  meta
2658
2730
  );
2659
2731
  var absentTelemetry = (meta) => resolveTelemetry({ models: [], turns: 0, durationMs: 0, vendorCostUsd: null }, meta);
2660
- var buildEnvelope = (telemetry, native, agentFilePath, agentFileFallbackPath) => {
2732
+ var buildEnvelope = (telemetry, native, agentFilePath, agentFileFallbackPath, seedUnrevised) => {
2733
+ if (seedUnrevised)
2734
+ return {
2735
+ schema_version: DEFAULT_SCHEMA_VERSION,
2736
+ findings: incompleteFindings(
2737
+ "### \u26A0\uFE0F Review did not complete\n\nThe review agent did not write a review (its draft is the untouched pre-seed). See the workflow logs."
2738
+ ),
2739
+ incomplete: true,
2740
+ ...telemetry
2741
+ };
2661
2742
  const outcome = findingsOutcome(native, agentFilePath, agentFileFallbackPath);
2662
2743
  switch (outcome.kind) {
2663
2744
  case "ok":
@@ -2665,7 +2746,7 @@ var buildEnvelope = (telemetry, native, agentFilePath, agentFileFallbackPath) =>
2665
2746
  case "telemetry-only":
2666
2747
  return {
2667
2748
  schema_version: DEFAULT_SCHEMA_VERSION,
2668
- findings: noticeFindings(`### \u26A0\uFE0F Review did not complete
2749
+ findings: incompleteFindings(`### \u26A0\uFE0F Review did not complete
2669
2750
 
2670
2751
  ${outcome.reason}`),
2671
2752
  incomplete: true,
@@ -2683,7 +2764,8 @@ var adapt = (adapterName, native, agentFilePath, meta = {}) => {
2683
2764
  absentTelemetry(meta),
2684
2765
  void 0,
2685
2766
  agentFilePath,
2686
- meta.agentFileFallbackPath
2767
+ meta.agentFileFallbackPath,
2768
+ meta.seedUnrevised === true
2687
2769
  )
2688
2770
  );
2689
2771
  const decoded = ClaudeCodeEnvelopeCodec.decode(native);
@@ -2694,7 +2776,8 @@ var adapt = (adapterName, native, agentFilePath, meta = {}) => {
2694
2776
  nativeTelemetry(decoded.right, meta),
2695
2777
  native,
2696
2778
  agentFilePath,
2697
- meta.agentFileFallbackPath
2779
+ meta.agentFileFallbackPath,
2780
+ meta.seedUnrevised === true
2698
2781
  )
2699
2782
  );
2700
2783
  }
@@ -2786,6 +2869,19 @@ var readJSONOrAbsent = (path) => {
2786
2869
  return void 0;
2787
2870
  }
2788
2871
  };
2872
+ var readSandboxConfigForNotice = (path) => {
2873
+ const text = readFileOrNull(resolve$1(path));
2874
+ if (text === null) return void 0;
2875
+ const parsed = tryParseJson(text);
2876
+ if (!parsed.ok) {
2877
+ process.stderr.write(
2878
+ `::warning::${annotationSafe(`code-review notice: ${path} is present but not valid JSON \u2014 omitting the agent allowlist from the notice`)}
2879
+ `
2880
+ );
2881
+ return void 0;
2882
+ }
2883
+ return parsed.value;
2884
+ };
2789
2885
  var readStdinJSON = () => {
2790
2886
  if (process.stdin.isTTY) return null;
2791
2887
  const raw = (() => {
@@ -3341,7 +3437,7 @@ var seedDraftCmd = defineCommand({
3341
3437
  };
3342
3438
  const writeScaffold = () => {
3343
3439
  try {
3344
- writeFileSync(outPath, `${JSON.stringify(noticeFindings(""), null, 2)}
3440
+ writeFileSync(outPath, `${JSON.stringify(emptyFindings(""), null, 2)}
3345
3441
  `);
3346
3442
  writeSeedMarker();
3347
3443
  process.stderr.write(
@@ -3441,11 +3537,14 @@ var adaptCmd = defineCommand({
3441
3537
  }
3442
3538
  },
3443
3539
  run: async ({ args }) => {
3540
+ const agentFile = args["agent-file"];
3541
+ const seedUnrevised = agentFile ? !mainHasWrittenDraft(mtimeMsOf(agentFile), mtimeMsOf(seedMarkerPath(agentFile))) : false;
3444
3542
  const envelope = unwrapAdapt(
3445
- adapt(requireAdapterName(args.adapter), readJSONOrAbsent(args.native), args["agent-file"], {
3543
+ adapt(requireAdapterName(args.adapter), readJSONOrAbsent(args.native), agentFile, {
3446
3544
  route: args.route,
3447
3545
  effort: args.effort,
3448
3546
  agentFileFallbackPath: args["agent-file-fallback"],
3547
+ seedUnrevised,
3449
3548
  ...args.transcript ? {
3450
3549
  transcriptFallback: () => transcriptFallbackFrom(args.transcript)
3451
3550
  } : {}
@@ -3469,6 +3568,10 @@ var noticeCmd = defineCommand({
3469
3568
  reasons: {
3470
3569
  type: "string",
3471
3570
  description: "security-blocked / triage-error only: the triage's fail-closed reason string (empty/omitted \u21D2 the no-reason wording)"
3571
+ },
3572
+ "sandbox-config": {
3573
+ type: "string",
3574
+ description: "no-output / triage-error only: path to the agent's sandbox-runtime settings (sandbox.json); its network.allowedDomains is named in the notice so an egress-blocked review self-diagnoses. Missing or unreadable \u21D2 the allowlist is omitted (an early failure runs before the jail is set up)."
3472
3575
  }
3473
3576
  },
3474
3577
  // An unrecognized kind degrades to a generic incomplete notice instead of exiting non-zero: a
@@ -3484,8 +3587,10 @@ var noticeCmd = defineCommand({
3484
3587
  `);
3485
3588
  return;
3486
3589
  }
3590
+ const namesAllowlist = args.kind === "no-output" || args.kind === "triage-error";
3591
+ const agentAllowlist = namesAllowlist && args["sandbox-config"] ? parseAgentAllowlist(readSandboxConfigForNotice(args["sandbox-config"])) : [];
3487
3592
  process.stdout.write(
3488
- `${JSON.stringify(buildNoticeEnvelope(args.kind, args.reasons), null, 2)}
3593
+ `${JSON.stringify(buildNoticeEnvelope(args.kind, args.reasons, agentAllowlist), null, 2)}
3489
3594
  `
3490
3595
  );
3491
3596
  }