@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 +256 -151
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/schema/VERSIONING.md +2 -1
- package/schema/findings.schema.json +2 -2
- package/templates/comment.eta +6 -0
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
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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),
|
|
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
|
}
|