@jphutchins/code-review 0.1.0-alpha.36-rc.0 → 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) => {
@@ -773,8 +814,7 @@ var DEFAULT_RESERVE = {
773
814
  frac: 0.15,
774
815
  growth: 0.25,
775
816
  flatUsd: 0.02,
776
- flatMs: 12e4,
777
- flatMem: 2 * 1024 * 1024 * 1024
817
+ flatMs: 12e4
778
818
  };
779
819
  var SOFT_MULTIPLE = 2;
780
820
  var growingReserve = (used, limit, flat, r) => {
@@ -801,7 +841,6 @@ var decideBudget = (i) => {
801
841
  const worst = [costAxis(i), timeAxis(i)].filter((a) => a !== null).reduce((max, a) => Math.max(max, axisSeverity(a)), 0);
802
842
  return worst === 2 ? { kind: "hard" } : worst === 1 ? { kind: "soft" } : { kind: "ok" };
803
843
  };
804
- var memoryCritical = (availMemBytes, totalMemBytes, floorBytes) => availMemBytes !== null && totalMemBytes !== null && totalMemBytes > 0 && availMemBytes <= floorBytes;
805
844
  var pct = (n) => `${String(Math.round(n * 100))}%`;
806
845
  var money = (n) => `$${n.toFixed(2)}`;
807
846
  var mins = (ms) => `${(ms / 6e4).toFixed(1)}m`;
@@ -856,7 +895,6 @@ var lastValidPath = (draftPath) => {
856
895
  };
857
896
  var mainHasWrittenDraft = (draftMtimeMs, seedMarkerMtimeMs) => draftMtimeMs !== null && (seedMarkerMtimeMs === null || draftMtimeMs > seedMarkerMtimeMs);
858
897
  var spawnFloorMessage = (draftPath) => `Write your own first-pass findings to ${draftPath} before spawning subagents \u2014 a review must never depend on subagents alone, and a pre-seeded draft does not count until you have revised it yourself this run. Write ${draftPath} from what you have read so far (preliminary findings are fine), run \`code-review validate ${draftPath} --explain\` until it passes, then fan out; your subagents run in the background, so keep refining the draft as their reports arrive.`;
859
- var memoryPressureMessage = (draftPath) => `System memory is critically low right now, so another subagent can't be spawned \u2014 a fresh process is the fastest way to tip the runner into an out-of-memory kill that would lose the whole review. Don't wind down: keep reading code directly and keep folding the reports from subagents already running into ${draftPath}, then try spawning again in a moment \u2014 this clears as soon as running subagents finish and free their memory.`;
860
898
  var forceBackgroundSpawn = (toolInput) => ({
861
899
  hookSpecificOutput: {
862
900
  hookEventName: "PreToolUse",
@@ -902,8 +940,6 @@ var evaluateBudgetHook = (input, params) => {
902
940
  if (phase.kind === "hard" && blockedDuringConvergence(toolName, rec["tool_input"]))
903
941
  return denyPreTool(budgetMessage(inputs, phase, params.draftPath, isSubagent));
904
942
  if (SPAWN_TOOLS.has(toolName)) {
905
- if (memoryCritical(params.availMemBytes, params.totalMemBytes, params.reserve.flatMem))
906
- return denyPreTool(memoryPressureMessage(params.draftPath));
907
943
  if (!isSubagent && !params.mainDraftWritten)
908
944
  return denyPreTool(spawnFloorMessage(params.draftPath));
909
945
  return forceBackgroundSpawn(rec["tool_input"]);
@@ -950,21 +986,6 @@ var parseFraction = (raw, fallback) => {
950
986
  const n = Number.parseFloat(raw);
951
987
  return Number.isFinite(n) && n >= 0 && n <= 1 ? n : fallback;
952
988
  };
953
- var BYTE_UNIT = {
954
- "": 1,
955
- k: 1024,
956
- m: 1024 * 1024,
957
- g: 1024 * 1024 * 1024,
958
- t: 1024 * 1024 * 1024 * 1024
959
- };
960
- var parseByteSize = (raw) => {
961
- const m = /^(\d+(?:\.\d+)?)\s*([kmgt])?(?:i?b)?$/i.exec(raw.trim());
962
- if (m === null) return null;
963
- const [, num = "", unit = ""] = m;
964
- const n = Number.parseFloat(num);
965
- const mult = BYTE_UNIT[unit.toLowerCase()];
966
- return Number.isFinite(n) && mult !== void 0 ? n * mult : null;
967
- };
968
989
  var budgetHookCommand = (draftPath, opts) => [
969
990
  "code-review budget-hook --draft",
970
991
  shellQuote(draftPath),
@@ -974,8 +995,7 @@ var budgetHookCommand = (draftPath, opts) => [
974
995
  ...opts.reserveFrac ? ["--reserve-frac", shellQuote(opts.reserveFrac)] : [],
975
996
  ...opts.reserveGrowth ? ["--reserve-growth", shellQuote(opts.reserveGrowth)] : [],
976
997
  ...opts.reserveUsd ? ["--reserve-usd", shellQuote(opts.reserveUsd)] : [],
977
- ...opts.reserveWall ? ["--reserve-wall", shellQuote(opts.reserveWall)] : [],
978
- ...opts.reserveMem ? ["--reserve-mem", shellQuote(opts.reserveMem)] : []
998
+ ...opts.reserveWall ? ["--reserve-wall", shellQuote(opts.reserveWall)] : []
979
999
  ].join(" ");
980
1000
 
981
1001
  // src/format.ts
@@ -1011,11 +1031,24 @@ var NOTICE_KINDS = [
1011
1031
  "no-output"
1012
1032
  ];
1013
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\`.`;
1014
1047
  var blockquote = (text) => text.replaceAll("\n", "\n> ");
1015
1048
  var reasoned = (lead, noReason, reasons) => typeof reasons === "string" && reasons.trim() !== "" ? `${lead}
1016
1049
 
1017
1050
  > ${blockquote(reasons)}` : noReason;
1018
- var noticeSummary = (kind, reasons) => {
1051
+ var noticeSummary = (kind, reasons, agentAllowlist) => {
1019
1052
  switch (kind) {
1020
1053
  case "security-blocked":
1021
1054
  return reasoned(
@@ -1028,25 +1061,25 @@ var noticeSummary = (kind, reasons) => {
1028
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:",
1029
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).",
1030
1063
  reasons
1031
- );
1064
+ ) + egressNote(agentAllowlist);
1032
1065
  case "setup-failed":
1033
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.";
1034
1067
  case "checkout-failed":
1035
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.";
1036
1069
  case "no-output":
1037
- 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);
1038
1071
  }
1039
1072
  };
1040
1073
  var noticeEnvelope = (summary) => ({
1041
1074
  schema_version: DEFAULT_SCHEMA_VERSION,
1042
- findings: noticeFindings(summary),
1075
+ findings: incompleteFindings(summary),
1043
1076
  models: [],
1044
1077
  turns: 0,
1045
1078
  duration_ms: 0,
1046
1079
  vendor_cost_usd: null,
1047
1080
  incomplete: true
1048
1081
  });
1049
- var buildNoticeEnvelope = (kind, reasons) => noticeEnvelope(noticeSummary(kind, reasons));
1082
+ var buildNoticeEnvelope = (kind, reasons, agentAllowlist = []) => noticeEnvelope(noticeSummary(kind, reasons, agentAllowlist));
1050
1083
  var buildUnknownNoticeEnvelope = (kind) => noticeEnvelope(
1051
1084
  `### \u26A0\uFE0F Review could not be rendered
1052
1085
 
@@ -1056,6 +1089,14 @@ var identity = (decoded) => decoded;
1056
1089
  var findingsTable = [
1057
1090
  {
1058
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",
1059
1100
  defaultVersion: DEFAULT_SCHEMA_VERSION,
1060
1101
  schemaFile: "findings.schema.json",
1061
1102
  codec: FindingsCodec,
@@ -1554,6 +1595,7 @@ var post = async (input, ghApi = runGhApi) => {
1554
1595
  );
1555
1596
  const existingComplete = existingSticky !== null && parseReviewComplete(existingSticky.body);
1556
1597
  const wouldBuryCompleted = (incomplete) => incomplete && existingComplete;
1598
+ const priorRounds = existingSticky !== null ? parseRounds(existingSticky.body) : [];
1557
1599
  const leaveInPlace = () => {
1558
1600
  process.stderr.write(
1559
1601
  `Review did not complete and the sticky already reflects a completed review \u2014 leaving it in place
@@ -1570,7 +1612,7 @@ var post = async (input, ghApi = runGhApi) => {
1570
1612
  const inlineTemplate = readFileSync(input.inlineTemplatePath, "utf-8");
1571
1613
  const renderNotice = (message) => formatMarkdown(
1572
1614
  render({
1573
- findings: noticeFindings(`### \u26A0\uFE0F ${message}`),
1615
+ findings: incompleteFindings(`### \u26A0\uFE0F ${message}`),
1574
1616
  envelope: null,
1575
1617
  incomplete: true,
1576
1618
  prices: decodedPrices.right,
@@ -1579,6 +1621,7 @@ var post = async (input, ghApi = runGhApi) => {
1579
1621
  route: input.route,
1580
1622
  reviewedSha: input.headSha,
1581
1623
  effort: input.effort,
1624
+ rounds: priorRounds,
1582
1625
  runUrl: input.runUrl,
1583
1626
  jsonUrl: input.jsonUrl,
1584
1627
  postedAt: input.postedAt
@@ -1611,16 +1654,20 @@ var post = async (input, ghApi = runGhApi) => {
1611
1654
  const envelope = loadEnvelope(input.envelopePath);
1612
1655
  const testReport = input.testReportPath ? loadTestReport(input.testReportPath) : void 0;
1613
1656
  if (envelope === null) {
1657
+ const envelopelessIncomplete = isIncompleteFindings(findings);
1658
+ if (wouldBuryCompleted(envelopelessIncomplete)) leaveInPlace();
1614
1659
  const body = formatMarkdown(
1615
1660
  render({
1616
1661
  findings,
1617
1662
  envelope: null,
1663
+ incomplete: envelopelessIncomplete,
1618
1664
  prices: decodedPrices.right,
1619
1665
  pricesProvided: input.pricesProvided,
1620
1666
  template,
1621
1667
  route: input.route,
1622
1668
  reviewedSha: input.headSha,
1623
1669
  effort: input.effort,
1670
+ rounds: priorRounds,
1624
1671
  testReport,
1625
1672
  inlineDisposition: { kind: "no-envelope" },
1626
1673
  runUrl: input.runUrl,
@@ -1634,7 +1681,7 @@ var post = async (input, ghApi = runGhApi) => {
1634
1681
  );
1635
1682
  process.exit(0);
1636
1683
  }
1637
- const thisIncomplete = envelope.incomplete === true;
1684
+ const thisIncomplete = envelope.incomplete === true || isIncompleteFindings(findings);
1638
1685
  if (wouldBuryCompleted(thisIncomplete)) leaveInPlace();
1639
1686
  const findingsMarker = findingsPointer(findings, input.jsonUrl);
1640
1687
  const {
@@ -1656,6 +1703,9 @@ var post = async (input, ghApi = runGhApi) => {
1656
1703
  }
1657
1704
  const botReviews = await fetchBotReviews(input.repo, prNumber, input.botLogin, ghApi);
1658
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;
1659
1709
  const commonRenderInput = {
1660
1710
  findings,
1661
1711
  envelope,
@@ -1667,7 +1717,8 @@ var post = async (input, ghApi = runGhApi) => {
1667
1717
  reviewedSha: input.headSha,
1668
1718
  effort: input.effort,
1669
1719
  testReport,
1670
- severityCounts: computeSeverityCounts(findings.findings),
1720
+ severityCounts: currentCounts,
1721
+ rounds,
1671
1722
  strays,
1672
1723
  runUrl: input.runUrl,
1673
1724
  jsonUrl: input.jsonUrl,
@@ -2678,7 +2729,16 @@ var nativeTelemetry = (native, meta) => resolveTelemetry(
2678
2729
  meta
2679
2730
  );
2680
2731
  var absentTelemetry = (meta) => resolveTelemetry({ models: [], turns: 0, durationMs: 0, vendorCostUsd: null }, meta);
2681
- 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
+ };
2682
2742
  const outcome = findingsOutcome(native, agentFilePath, agentFileFallbackPath);
2683
2743
  switch (outcome.kind) {
2684
2744
  case "ok":
@@ -2686,7 +2746,7 @@ var buildEnvelope = (telemetry, native, agentFilePath, agentFileFallbackPath) =>
2686
2746
  case "telemetry-only":
2687
2747
  return {
2688
2748
  schema_version: DEFAULT_SCHEMA_VERSION,
2689
- findings: noticeFindings(`### \u26A0\uFE0F Review did not complete
2749
+ findings: incompleteFindings(`### \u26A0\uFE0F Review did not complete
2690
2750
 
2691
2751
  ${outcome.reason}`),
2692
2752
  incomplete: true,
@@ -2704,7 +2764,8 @@ var adapt = (adapterName, native, agentFilePath, meta = {}) => {
2704
2764
  absentTelemetry(meta),
2705
2765
  void 0,
2706
2766
  agentFilePath,
2707
- meta.agentFileFallbackPath
2767
+ meta.agentFileFallbackPath,
2768
+ meta.seedUnrevised === true
2708
2769
  )
2709
2770
  );
2710
2771
  const decoded = ClaudeCodeEnvelopeCodec.decode(native);
@@ -2715,7 +2776,8 @@ var adapt = (adapterName, native, agentFilePath, meta = {}) => {
2715
2776
  nativeTelemetry(decoded.right, meta),
2716
2777
  native,
2717
2778
  agentFilePath,
2718
- meta.agentFileFallbackPath
2779
+ meta.agentFileFallbackPath,
2780
+ meta.seedUnrevised === true
2719
2781
  )
2720
2782
  );
2721
2783
  }
@@ -2807,6 +2869,19 @@ var readJSONOrAbsent = (path) => {
2807
2869
  return void 0;
2808
2870
  }
2809
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
+ };
2810
2885
  var readStdinJSON = () => {
2811
2886
  if (process.stdin.isTTY) return null;
2812
2887
  const raw = (() => {
@@ -3058,18 +3133,6 @@ var snapshotIfValid = (draftPath) => {
3058
3133
  );
3059
3134
  }
3060
3135
  };
3061
- var readMemInfo = () => {
3062
- try {
3063
- const text = readFileSync("/proc/meminfo", "utf8");
3064
- const kb = (key2) => {
3065
- const m = new RegExp(`^${key2}:\\s+(\\d+)\\s+kB`, "m").exec(text);
3066
- return m?.[1] !== void 0 ? Number(m[1]) * 1024 : null;
3067
- };
3068
- return { availBytes: kb("MemAvailable"), totalBytes: kb("MemTotal") };
3069
- } catch {
3070
- return { availBytes: null, totalBytes: null };
3071
- }
3072
- };
3073
3136
  var budgetHookCmd = defineCommand({
3074
3137
  meta: {
3075
3138
  name: "budget-hook",
@@ -3108,17 +3171,12 @@ var budgetHookCmd = defineCommand({
3108
3171
  "reserve-wall": {
3109
3172
  type: "string",
3110
3173
  description: "Flat wall-clock wind-down floor (e.g. 2m, 120s), whichever is larger with --reserve-frac (default: 2m)"
3111
- },
3112
- "reserve-mem": {
3113
- type: "string",
3114
- description: "Free-RAM floor (e.g. 2g, 1536m) below which new subagent spawns are denied until memory recovers; not a convergence axis (default: 2g)"
3115
3174
  }
3116
3175
  },
3117
3176
  run: async ({ args }) => {
3118
3177
  try {
3119
3178
  const draftPath = resolve$1(args.draft);
3120
3179
  const input = readStdinJSON();
3121
- const mem = readMemInfo();
3122
3180
  const transcriptPath = transcriptPathOf(input);
3123
3181
  const tree = transcriptPath ? readTranscriptTree(resolve$1(transcriptPath)) : void 0;
3124
3182
  const usage = tree ? sumTranscriptUsage(tree.entries) : void 0;
@@ -3135,14 +3193,11 @@ var budgetHookCmd = defineCommand({
3135
3193
  nowMs: Date.now()
3136
3194
  }),
3137
3195
  wallMs,
3138
- availMemBytes: mem.availBytes,
3139
- totalMemBytes: mem.totalBytes,
3140
3196
  reserve: {
3141
3197
  frac: parseFraction(args["reserve-frac"], DEFAULT_RESERVE.frac),
3142
3198
  growth: parseFraction(args["reserve-growth"], DEFAULT_RESERVE.growth),
3143
3199
  flatUsd: parseBudgetUsd(args["reserve-usd"]) ?? DEFAULT_RESERVE.flatUsd,
3144
- flatMs: args["reserve-wall"] ? parseWallMs(args["reserve-wall"]) ?? DEFAULT_RESERVE.flatMs : DEFAULT_RESERVE.flatMs,
3145
- flatMem: args["reserve-mem"] ? parseByteSize(args["reserve-mem"]) ?? DEFAULT_RESERVE.flatMem : DEFAULT_RESERVE.flatMem
3200
+ flatMs: args["reserve-wall"] ? parseWallMs(args["reserve-wall"]) ?? DEFAULT_RESERVE.flatMs : DEFAULT_RESERVE.flatMs
3146
3201
  },
3147
3202
  draftPath,
3148
3203
  mainDraftWritten: mainHasWrittenDraft(
@@ -3219,10 +3274,6 @@ var printSettingsCmd = defineCommand({
3219
3274
  "reserve-wall": {
3220
3275
  type: "string",
3221
3276
  description: "Flat wall-clock wind-down floor (e.g. 2m), whichever is larger with --reserve-frac (default: 2m)"
3222
- },
3223
- "reserve-mem": {
3224
- type: "string",
3225
- description: "Free-RAM floor (e.g. 2g) below which new subagent spawns are denied until memory recovers (default: 2g)"
3226
3277
  }
3227
3278
  },
3228
3279
  run: async ({ args }) => {
@@ -3244,8 +3295,7 @@ var printSettingsCmd = defineCommand({
3244
3295
  reserveFrac: args["reserve-frac"],
3245
3296
  reserveGrowth: args["reserve-growth"],
3246
3297
  reserveUsd: args["reserve-usd"],
3247
- reserveWall: args["reserve-wall"],
3248
- reserveMem: args["reserve-mem"]
3298
+ reserveWall: args["reserve-wall"]
3249
3299
  }
3250
3300
  });
3251
3301
  process.stdout.write(`${JSON.stringify(settings)}
@@ -3387,7 +3437,7 @@ var seedDraftCmd = defineCommand({
3387
3437
  };
3388
3438
  const writeScaffold = () => {
3389
3439
  try {
3390
- writeFileSync(outPath, `${JSON.stringify(noticeFindings(""), null, 2)}
3440
+ writeFileSync(outPath, `${JSON.stringify(emptyFindings(""), null, 2)}
3391
3441
  `);
3392
3442
  writeSeedMarker();
3393
3443
  process.stderr.write(
@@ -3487,11 +3537,14 @@ var adaptCmd = defineCommand({
3487
3537
  }
3488
3538
  },
3489
3539
  run: async ({ args }) => {
3540
+ const agentFile = args["agent-file"];
3541
+ const seedUnrevised = agentFile ? !mainHasWrittenDraft(mtimeMsOf(agentFile), mtimeMsOf(seedMarkerPath(agentFile))) : false;
3490
3542
  const envelope = unwrapAdapt(
3491
- adapt(requireAdapterName(args.adapter), readJSONOrAbsent(args.native), args["agent-file"], {
3543
+ adapt(requireAdapterName(args.adapter), readJSONOrAbsent(args.native), agentFile, {
3492
3544
  route: args.route,
3493
3545
  effort: args.effort,
3494
3546
  agentFileFallbackPath: args["agent-file-fallback"],
3547
+ seedUnrevised,
3495
3548
  ...args.transcript ? {
3496
3549
  transcriptFallback: () => transcriptFallbackFrom(args.transcript)
3497
3550
  } : {}
@@ -3515,6 +3568,10 @@ var noticeCmd = defineCommand({
3515
3568
  reasons: {
3516
3569
  type: "string",
3517
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)."
3518
3575
  }
3519
3576
  },
3520
3577
  // An unrecognized kind degrades to a generic incomplete notice instead of exiting non-zero: a
@@ -3530,8 +3587,10 @@ var noticeCmd = defineCommand({
3530
3587
  `);
3531
3588
  return;
3532
3589
  }
3590
+ const namesAllowlist = args.kind === "no-output" || args.kind === "triage-error";
3591
+ const agentAllowlist = namesAllowlist && args["sandbox-config"] ? parseAgentAllowlist(readSandboxConfigForNotice(args["sandbox-config"])) : [];
3533
3592
  process.stdout.write(
3534
- `${JSON.stringify(buildNoticeEnvelope(args.kind, args.reasons), null, 2)}
3593
+ `${JSON.stringify(buildNoticeEnvelope(args.kind, args.reasons, agentAllowlist), null, 2)}
3535
3594
  `
3536
3595
  );
3537
3596
  }