@jphutchins/code-review 0.1.0-alpha.40 → 0.1.0-alpha.41
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/README.md +21 -10
- package/dist/index.js +575 -193
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/schema/VERSIONING.md +3 -2
- package/schema/findings.schema.json +39 -0
- package/templates/comment.eta +12 -3
- package/templates/inline.eta +4 -0
package/dist/index.js
CHANGED
|
@@ -93,6 +93,36 @@ var SystemicProblemStrict = t.refinement(
|
|
|
93
93
|
"SystemicProblemStrict"
|
|
94
94
|
);
|
|
95
95
|
var SystemicProblemCodec = t.exact(SystemicProblemStrict);
|
|
96
|
+
var RecurringShape = t.type({
|
|
97
|
+
code: t.string,
|
|
98
|
+
consecutive_rounds: t.refinement(
|
|
99
|
+
t.number,
|
|
100
|
+
(n) => Number.isSafeInteger(n) && n >= 1,
|
|
101
|
+
"ConsecutiveRounds"
|
|
102
|
+
),
|
|
103
|
+
start_round: t.refinement(
|
|
104
|
+
t.number,
|
|
105
|
+
(n) => Number.isSafeInteger(n) && n >= 1,
|
|
106
|
+
"StartRound"
|
|
107
|
+
)
|
|
108
|
+
});
|
|
109
|
+
var RECURRING_KEYS = new Set(Object.keys(RecurringShape.props));
|
|
110
|
+
var RecurringCodec = t.refinement(
|
|
111
|
+
RecurringShape,
|
|
112
|
+
(r) => Object.keys(r).every((k) => RECURRING_KEYS.has(k)),
|
|
113
|
+
"RecurringStrict"
|
|
114
|
+
);
|
|
115
|
+
var ScopeMetastasisShape = t.type({
|
|
116
|
+
decision_prompt: t.string,
|
|
117
|
+
recurring: t.array(RecurringCodec)
|
|
118
|
+
});
|
|
119
|
+
var SCOPE_METASTASIS_KEYS = new Set(Object.keys(ScopeMetastasisShape.props));
|
|
120
|
+
var ScopeMetastasisStrict = t.refinement(
|
|
121
|
+
ScopeMetastasisShape,
|
|
122
|
+
(s) => Object.keys(s).every((k) => SCOPE_METASTASIS_KEYS.has(k)),
|
|
123
|
+
"ScopeMetastasisStrict"
|
|
124
|
+
);
|
|
125
|
+
var ScopeMetastasisCodec = t.exact(ScopeMetastasisStrict);
|
|
96
126
|
var FindingsCodec = t.exact(
|
|
97
127
|
t.intersection([
|
|
98
128
|
t.type({
|
|
@@ -102,7 +132,9 @@ var FindingsCodec = t.exact(
|
|
|
102
132
|
findings: t.array(FindingCodec)
|
|
103
133
|
}),
|
|
104
134
|
t.partial({
|
|
105
|
-
systemic_problems: t.array(SystemicProblemCodec)
|
|
135
|
+
systemic_problems: t.array(SystemicProblemCodec),
|
|
136
|
+
// Pipeline-stamped only (issue #150); see ScopeMetastasisCodec.
|
|
137
|
+
scope_metastasis: ScopeMetastasisCodec
|
|
106
138
|
})
|
|
107
139
|
])
|
|
108
140
|
);
|
|
@@ -338,7 +370,7 @@ var severityEmoji = (s) => {
|
|
|
338
370
|
return "\u2753";
|
|
339
371
|
}
|
|
340
372
|
};
|
|
341
|
-
var EMBED_LIMIT =
|
|
373
|
+
var EMBED_LIMIT = 42700;
|
|
342
374
|
var AGENTS_STOP_DIRECTIVE = "<!-- AGENTS: STOP \u2014 do not parse the prose below; decode this findings JSON and read schema_version first. -->";
|
|
343
375
|
var b64LengthOf = (document) => Buffer.from(JSON.stringify(document), "utf-8").toString("base64").length;
|
|
344
376
|
var encodeMarker = (document, jsonUrl, limit) => {
|
|
@@ -417,17 +449,21 @@ var parseRounds = (body) => {
|
|
|
417
449
|
if (b64 === void 0) return [];
|
|
418
450
|
const decoded = decodeBase64Json(b64);
|
|
419
451
|
if (!Array.isArray(decoded)) return [];
|
|
420
|
-
|
|
452
|
+
const kept = [];
|
|
453
|
+
let priorCodes;
|
|
454
|
+
for (const u of decoded.filter(isSeverityCounts)) {
|
|
421
455
|
const rec = u;
|
|
422
|
-
const codes = normalizeCodeCounts(rec["codes"]);
|
|
456
|
+
const codes = normalizeCodeCounts(rec["codes"], priorCodes);
|
|
457
|
+
priorCodes = codes;
|
|
423
458
|
const sha = rec["sha"];
|
|
424
459
|
const shaStr = typeof sha === "string" && sha !== "" ? sha : void 0;
|
|
425
460
|
const round = rec["round"];
|
|
426
461
|
const roundNum = typeof round === "number" && Number.isSafeInteger(round) && round >= 1 ? round : void 0;
|
|
427
462
|
const base = codes === void 0 ? { critical: u.critical, major: u.major, minor: u.minor, nit: u.nit } : { critical: u.critical, major: u.major, minor: u.minor, nit: u.nit, codes };
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
}
|
|
463
|
+
const record3 = shaStr === void 0 ? base : { ...base, sha: shaStr };
|
|
464
|
+
kept.push(roundNum === void 0 ? record3 : { ...record3, round: roundNum });
|
|
465
|
+
}
|
|
466
|
+
return kept;
|
|
431
467
|
};
|
|
432
468
|
var ROUNDS_MARKER_LIMIT = 8e3;
|
|
433
469
|
var roundsMarker = (rounds) => {
|
|
@@ -499,11 +535,13 @@ var consecutiveCodeStreaks = (rounds) => {
|
|
|
499
535
|
return Object.fromEntries(entries);
|
|
500
536
|
};
|
|
501
537
|
var DEFAULT_METASTASIS_STREAK = 3;
|
|
538
|
+
var SCOPE_METASTASIS_DECISION_PROMPT = "Findings keep recurring in the same mechanism across consecutive rounds \u2014 each fix keeps enabling the next finding in that machinery. This is a decision, not a directive: state in your summary whether you are committing to the expanding scope (plan the remaining facets of the recurring mechanism(s) above as one unit) or narrowing the scope so the recurrence stops.";
|
|
539
|
+
var flaggedCodeStreaks = (rounds, minStreak) => Object.entries(consecutiveCodeStreaks(rounds)).filter(([, s]) => s.streak >= minStreak).sort((a, b) => b[1].streak - a[1].streak).map(([code, streak]) => ({ code, streak }));
|
|
502
540
|
var metastasisNote = (rounds, minStreak = DEFAULT_METASTASIS_STREAK) => {
|
|
503
|
-
const flagged =
|
|
541
|
+
const flagged = flaggedCodeStreaks(rounds, minStreak);
|
|
504
542
|
if (flagged.length === 0) return "";
|
|
505
543
|
const lines = flagged.map(
|
|
506
|
-
(
|
|
544
|
+
({ code, streak }) => `> **\`${escapeCodeBackticks(code)}\`** \u2014 findings in ${String(streak.streak)} consecutive rounds.`
|
|
507
545
|
);
|
|
508
546
|
return [
|
|
509
547
|
"> [!WARNING]",
|
|
@@ -511,6 +549,18 @@ var metastasisNote = (rounds, minStreak = DEFAULT_METASTASIS_STREAK) => {
|
|
|
511
549
|
...lines
|
|
512
550
|
].join("\n");
|
|
513
551
|
};
|
|
552
|
+
var computeScopeMetastasis = (rounds, minStreak = DEFAULT_METASTASIS_STREAK) => {
|
|
553
|
+
const flagged = flaggedCodeStreaks(rounds, minStreak);
|
|
554
|
+
if (flagged.length === 0) return null;
|
|
555
|
+
return {
|
|
556
|
+
decision_prompt: SCOPE_METASTASIS_DECISION_PROMPT,
|
|
557
|
+
recurring: flagged.map(({ code, streak }) => ({
|
|
558
|
+
code,
|
|
559
|
+
consecutive_rounds: streak.streak,
|
|
560
|
+
start_round: streak.startRound
|
|
561
|
+
}))
|
|
562
|
+
};
|
|
563
|
+
};
|
|
514
564
|
var computeSameRootNotes = (priorRounds, findings, currentSha) => {
|
|
515
565
|
const codes = findings.map((f) => f.code).filter((c) => c !== void 0 && c !== "");
|
|
516
566
|
const entries = [];
|
|
@@ -542,28 +592,31 @@ var convergenceSummary = (counts, threshold = DEFAULT_CONVERGENCE_THRESHOLD) =>
|
|
|
542
592
|
const { score, converged } = convergenceSignal(counts, threshold);
|
|
543
593
|
return converged ? `**Convergence** \u{1F3C1} ${String(score)} \u2264 ${String(threshold)} \u2014 converged` : `**Convergence** \u{1F504} ${String(score)} > ${String(threshold)} \u2014 iterating`;
|
|
544
594
|
};
|
|
545
|
-
var SURFACE_SCHEMA_VERSION = "0.
|
|
595
|
+
var SURFACE_SCHEMA_VERSION = "0.8.0";
|
|
546
596
|
var convergenceSignal = (counts, threshold = DEFAULT_CONVERGENCE_THRESHOLD) => {
|
|
547
597
|
const score = convergenceScore(counts);
|
|
548
598
|
return { score, threshold, converged: score <= threshold };
|
|
549
599
|
};
|
|
550
600
|
var signalForRound = (round, counts, threshold = DEFAULT_CONVERGENCE_THRESHOLD) => ({ round, convergence: convergenceSignal(counts, threshold) });
|
|
551
|
-
var surfaceFindings = (findings, signal) => {
|
|
601
|
+
var surfaceFindings = (findings, signal, scopeMetastasis = null) => {
|
|
552
602
|
const agentDoc = Object.fromEntries(
|
|
553
|
-
Object.entries(findings).filter(
|
|
603
|
+
Object.entries(findings).filter(
|
|
604
|
+
([key2]) => key2 !== "round" && key2 !== "convergence" && key2 !== "scope_metastasis"
|
|
605
|
+
)
|
|
554
606
|
);
|
|
555
607
|
return {
|
|
556
608
|
...agentDoc,
|
|
557
609
|
schema_version: SURFACE_SCHEMA_VERSION,
|
|
558
|
-
...signal === null ? {} : signal
|
|
610
|
+
...signal === null ? {} : signal,
|
|
611
|
+
...scopeMetastasis === null ? {} : { scope_metastasis: scopeMetastasis }
|
|
559
612
|
};
|
|
560
613
|
};
|
|
561
614
|
var signalMarker = (signal) => `<!-- code-review:signal;base64 ${Buffer.from(
|
|
562
615
|
JSON.stringify({ schema_version: SURFACE_SCHEMA_VERSION, ...signal }),
|
|
563
616
|
"utf-8"
|
|
564
617
|
).toString("base64")} -->`;
|
|
565
|
-
var surfacedFindingsPointer = (findings, signal, jsonUrl) => {
|
|
566
|
-
const marker = findingsPointer(surfaceFindings(findings, signal), jsonUrl);
|
|
618
|
+
var surfacedFindingsPointer = (findings, signal, jsonUrl, scopeMetastasis = null) => {
|
|
619
|
+
const marker = findingsPointer(surfaceFindings(findings, signal, scopeMetastasis), jsonUrl);
|
|
567
620
|
if (signal === null || marker.includes("<!-- code-review:findings-json;base64 ")) return marker;
|
|
568
621
|
return marker === "" ? signalMarker(signal) : `${marker}
|
|
569
622
|
${signalMarker(signal)}`;
|
|
@@ -592,7 +645,7 @@ var parseSurfaceSignal = (doc) => {
|
|
|
592
645
|
convergence: { score: c["score"], threshold: c["threshold"], converged: c["converged"] }
|
|
593
646
|
};
|
|
594
647
|
};
|
|
595
|
-
var SURFACE_SCHEMA_VERSIONS = [SURFACE_SCHEMA_VERSION];
|
|
648
|
+
var SURFACE_SCHEMA_VERSIONS = ["0.7.0", SURFACE_SCHEMA_VERSION];
|
|
596
649
|
var stripSurfaceFields = (doc) => {
|
|
597
650
|
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) return doc;
|
|
598
651
|
const o = doc;
|
|
@@ -628,15 +681,346 @@ var reviewBodyPointer = (headSha, stickyUrl, marker) => {
|
|
|
628
681
|
|
|
629
682
|
${linkLine}` : linkLine;
|
|
630
683
|
};
|
|
684
|
+
var asRecord = (u) => typeof u === "object" && u !== null && !Array.isArray(u) ? u : null;
|
|
685
|
+
var errMsg = (e) => e instanceof Error ? e.message : String(e);
|
|
686
|
+
var annotationSafe = (msg) => msg.replaceAll(/[\r\n]+/g, " ");
|
|
687
|
+
var clipText = (body, max) => {
|
|
688
|
+
if (body.length <= max) return body;
|
|
689
|
+
const cut = body.slice(0, max);
|
|
690
|
+
const safe = /[\uD800-\uDBFF]$/.test(cut) ? cut.slice(0, -1) : cut;
|
|
691
|
+
return `${safe}
|
|
692
|
+
\u2026 [truncated]`;
|
|
693
|
+
};
|
|
694
|
+
var tryParseJson = (text) => {
|
|
695
|
+
try {
|
|
696
|
+
return { ok: true, value: JSON.parse(text) };
|
|
697
|
+
} catch {
|
|
698
|
+
return { ok: false };
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
var readFileOrNull = (path) => {
|
|
702
|
+
try {
|
|
703
|
+
return readFileSync(path, "utf-8");
|
|
704
|
+
} catch {
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
// src/transcript.ts
|
|
710
|
+
var numField = (rec, key2) => {
|
|
711
|
+
const v = rec[key2];
|
|
712
|
+
return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0;
|
|
713
|
+
};
|
|
714
|
+
var messageUsage = (entry) => {
|
|
715
|
+
const rec = asRecord(entry);
|
|
716
|
+
if (rec === null || rec["type"] !== "assistant") return null;
|
|
717
|
+
const msg = asRecord(rec["message"]);
|
|
718
|
+
if (msg === null) return null;
|
|
719
|
+
const model = msg["model"];
|
|
720
|
+
const usage = asRecord(msg["usage"]);
|
|
721
|
+
if (typeof model !== "string" || usage === null) return null;
|
|
722
|
+
const id = msg["id"];
|
|
723
|
+
return {
|
|
724
|
+
id: typeof id === "string" ? id : null,
|
|
725
|
+
model,
|
|
726
|
+
input: numField(usage, "input_tokens"),
|
|
727
|
+
output: numField(usage, "output_tokens"),
|
|
728
|
+
cacheRead: numField(usage, "cache_read_input_tokens"),
|
|
729
|
+
cacheWrite: numField(usage, "cache_creation_input_tokens")
|
|
730
|
+
};
|
|
731
|
+
};
|
|
732
|
+
var tsMsOf = (entry) => {
|
|
733
|
+
const rec = asRecord(entry);
|
|
734
|
+
const ts = rec?.["timestamp"];
|
|
735
|
+
if (typeof ts !== "string") return null;
|
|
736
|
+
const ms = Date.parse(ts);
|
|
737
|
+
return Number.isNaN(ms) ? null : ms;
|
|
738
|
+
};
|
|
739
|
+
var EMPTY_TOTALS = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
740
|
+
var parseJsonl = (text) => text.split("\n").filter((line) => line.trim() !== "").flatMap((line) => {
|
|
741
|
+
try {
|
|
742
|
+
return [JSON.parse(line)];
|
|
743
|
+
} catch {
|
|
744
|
+
return [];
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
var sumTranscriptUsage = (entries) => {
|
|
748
|
+
const summed = entries.reduce(
|
|
749
|
+
(acc, entry) => {
|
|
750
|
+
const u = messageUsage(entry);
|
|
751
|
+
if (u === null) return acc;
|
|
752
|
+
if (u.id !== null && acc.seen.has(u.id)) return acc;
|
|
753
|
+
if (u.id !== null) acc.seen.add(u.id);
|
|
754
|
+
const prev = acc.totals.get(u.model) ?? EMPTY_TOTALS;
|
|
755
|
+
acc.totals.set(u.model, {
|
|
756
|
+
input: prev.input + u.input,
|
|
757
|
+
output: prev.output + u.output,
|
|
758
|
+
cacheRead: prev.cacheRead + u.cacheRead,
|
|
759
|
+
cacheWrite: prev.cacheWrite + u.cacheWrite
|
|
760
|
+
});
|
|
761
|
+
return { totals: acc.totals, turns: acc.turns + 1, seen: acc.seen };
|
|
762
|
+
},
|
|
763
|
+
{ totals: /* @__PURE__ */ new Map(), turns: 0, seen: /* @__PURE__ */ new Set() }
|
|
764
|
+
);
|
|
765
|
+
const models = [...summed.totals].map(([model, t8]) => ({
|
|
766
|
+
model,
|
|
767
|
+
input_tokens: t8.input,
|
|
768
|
+
output_tokens: t8.output,
|
|
769
|
+
cache_read_tokens: t8.cacheRead,
|
|
770
|
+
cache_write_tokens: t8.cacheWrite
|
|
771
|
+
}));
|
|
772
|
+
const bounds = entries.reduce(
|
|
773
|
+
(acc, entry) => {
|
|
774
|
+
const ms = tsMsOf(entry);
|
|
775
|
+
if (ms === null) return acc;
|
|
776
|
+
return {
|
|
777
|
+
min: acc.min === null || ms < acc.min ? ms : acc.min,
|
|
778
|
+
max: acc.max === null || ms > acc.max ? ms : acc.max
|
|
779
|
+
};
|
|
780
|
+
},
|
|
781
|
+
{ min: null, max: null }
|
|
782
|
+
);
|
|
783
|
+
return {
|
|
784
|
+
models,
|
|
785
|
+
turns: summed.turns,
|
|
786
|
+
durationMs: bounds.min !== null && bounds.max !== null ? bounds.max - bounds.min : 0,
|
|
787
|
+
firstTsMs: bounds.min,
|
|
788
|
+
lastTsMs: bounds.max
|
|
789
|
+
};
|
|
790
|
+
};
|
|
791
|
+
var subagentFiles = (mainPath) => {
|
|
792
|
+
const dir = join(dirname(mainPath), basename(mainPath, ".jsonl"), "subagents");
|
|
793
|
+
try {
|
|
794
|
+
return readdirSync(dir).filter((name) => name.endsWith(".jsonl")).map((name) => join(dir, name));
|
|
795
|
+
} catch {
|
|
796
|
+
return [];
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
var readTranscriptTree = (mainPath) => {
|
|
800
|
+
const mainText = readFileOrNull(mainPath);
|
|
801
|
+
const mainEntries = mainText === null ? [] : parseJsonl(mainText);
|
|
802
|
+
const inlineSidechains = mainEntries.some((e) => asRecord(e)?.["isSidechain"] === true);
|
|
803
|
+
const siblings = inlineSidechains ? [] : subagentFiles(mainPath);
|
|
804
|
+
const siblingReads = siblings.flatMap((path) => {
|
|
805
|
+
const text = readFileOrNull(path);
|
|
806
|
+
return text === null ? [] : [{ path, entries: parseJsonl(text) }];
|
|
807
|
+
});
|
|
808
|
+
return {
|
|
809
|
+
entries: [...mainEntries, ...siblingReads.flatMap((r) => r.entries)],
|
|
810
|
+
files: [...mainText === null ? [] : [mainPath], ...siblingReads.map((r) => r.path)],
|
|
811
|
+
missing: mainText === null
|
|
812
|
+
};
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
// src/answered.ts
|
|
816
|
+
var THREAD_COMMENT_JQ = ".[] | {id, in_reply_to_id, user: {login: .user.login}, user_login: .user.login, user_type: .user.type, body, html_url, path, line, created_at, author_association}";
|
|
817
|
+
var ThreadCommentCodec = t.type({
|
|
818
|
+
id: t.number,
|
|
819
|
+
in_reply_to_id: t.union([t.number, t.null]),
|
|
820
|
+
user_login: t.string,
|
|
821
|
+
user_type: t.union([t.string, t.null]),
|
|
822
|
+
body: t.union([t.string, t.null]),
|
|
823
|
+
html_url: t.string,
|
|
824
|
+
path: t.union([t.string, t.null]),
|
|
825
|
+
line: t.union([t.number, t.null]),
|
|
826
|
+
created_at: t.union([t.string, t.null])
|
|
827
|
+
});
|
|
828
|
+
var AnsweredEntryCodec = t.type({
|
|
829
|
+
code: t.string,
|
|
830
|
+
title: t.string,
|
|
831
|
+
description: t.string,
|
|
832
|
+
reasoning: t.string,
|
|
833
|
+
severity: t.union([
|
|
834
|
+
t.literal("critical"),
|
|
835
|
+
t.literal("major"),
|
|
836
|
+
t.literal("minor"),
|
|
837
|
+
t.literal("nit")
|
|
838
|
+
]),
|
|
839
|
+
path: t.string,
|
|
840
|
+
patch: t.union([t.string, t.null]),
|
|
841
|
+
replied_at: t.union([t.string, t.null]),
|
|
842
|
+
reply_id: t.number,
|
|
843
|
+
thread_url: t.string,
|
|
844
|
+
reply_url: t.string,
|
|
845
|
+
reply_author: t.string,
|
|
846
|
+
reply_excerpt: t.string
|
|
847
|
+
});
|
|
848
|
+
var encodeAnsweredEntry = (e) => ({
|
|
849
|
+
code: e.code,
|
|
850
|
+
title: e.title,
|
|
851
|
+
description: e.description,
|
|
852
|
+
reasoning: e.reasoning,
|
|
853
|
+
severity: e.severity,
|
|
854
|
+
path: e.path,
|
|
855
|
+
patch: e.patch,
|
|
856
|
+
replied_at: e.repliedAt,
|
|
857
|
+
reply_id: e.replyId,
|
|
858
|
+
thread_url: e.threadUrl,
|
|
859
|
+
reply_url: e.replyUrl,
|
|
860
|
+
reply_author: e.replyAuthor,
|
|
861
|
+
reply_excerpt: e.replyExcerpt
|
|
862
|
+
});
|
|
863
|
+
var decodeAnsweredEntry = (raw) => {
|
|
864
|
+
const decoded = AnsweredEntryCodec.decode(raw);
|
|
865
|
+
return decoded._tag === "Right" ? decoded.right : null;
|
|
866
|
+
};
|
|
867
|
+
var isHuman = (login, type8, botLogin) => login !== botLogin && type8 === "User";
|
|
868
|
+
var EXCERPT_LIMIT = 400;
|
|
869
|
+
var answeredRegistryFrom = (comments, botLogin) => {
|
|
870
|
+
const ordered = [...comments].sort(
|
|
871
|
+
(a, b) => (a.created_at ?? "").localeCompare(b.created_at ?? "") || (a.id > b.id ? 1 : a.id < b.id ? -1 : 0)
|
|
872
|
+
);
|
|
873
|
+
const byId = /* @__PURE__ */ new Map();
|
|
874
|
+
for (const c of ordered) byId.set(c.id, c);
|
|
875
|
+
const rootOf = (c) => {
|
|
876
|
+
let current = c;
|
|
877
|
+
const seen = /* @__PURE__ */ new Set();
|
|
878
|
+
while (current.in_reply_to_id !== null && !seen.has(current.id)) {
|
|
879
|
+
seen.add(current.id);
|
|
880
|
+
const parent = byId.get(current.in_reply_to_id);
|
|
881
|
+
if (parent === void 0) return null;
|
|
882
|
+
current = parent;
|
|
883
|
+
}
|
|
884
|
+
return current.in_reply_to_id === null ? current : null;
|
|
885
|
+
};
|
|
886
|
+
const findingOf = (root) => {
|
|
887
|
+
const decoded = parseFindingsMarker(root.body ?? "");
|
|
888
|
+
const doc = typeof decoded === "object" && decoded !== null ? decoded.findings : void 0;
|
|
889
|
+
const first = Array.isArray(doc) ? doc[0] : void 0;
|
|
890
|
+
if (first === void 0) return null;
|
|
891
|
+
const title = first["title"];
|
|
892
|
+
const description = first["description"];
|
|
893
|
+
const reasoning = first["reasoning"];
|
|
894
|
+
const code = first["code"];
|
|
895
|
+
const severity = first["severity"];
|
|
896
|
+
const path = first["path"];
|
|
897
|
+
const patch = first["patch"];
|
|
898
|
+
return typeof title === "string" && typeof description === "string" && typeof reasoning === "string" && typeof path === "string" && (severity === "critical" || severity === "major" || severity === "minor" || severity === "nit") ? {
|
|
899
|
+
code: typeof code === "string" ? code : "",
|
|
900
|
+
title,
|
|
901
|
+
description,
|
|
902
|
+
reasoning,
|
|
903
|
+
severity,
|
|
904
|
+
path,
|
|
905
|
+
patch: typeof patch === "string" ? patch : null
|
|
906
|
+
} : null;
|
|
907
|
+
};
|
|
908
|
+
const threads = /* @__PURE__ */ new Map();
|
|
909
|
+
for (const c of ordered) {
|
|
910
|
+
const root = rootOf(c);
|
|
911
|
+
if (root === null) continue;
|
|
912
|
+
const group = threads.get(root.id);
|
|
913
|
+
if (group === void 0) threads.set(root.id, [c]);
|
|
914
|
+
else group.push(c);
|
|
915
|
+
}
|
|
916
|
+
const entries = [];
|
|
917
|
+
for (const [rootId, group] of threads) {
|
|
918
|
+
const root = byId.get(rootId);
|
|
919
|
+
if (root === void 0 || root.user_login !== botLogin) continue;
|
|
920
|
+
const finding = findingOf(root);
|
|
921
|
+
if (finding === null) continue;
|
|
922
|
+
const humanReplies = group.filter(
|
|
923
|
+
(c) => c.id !== root.id && isHuman(c.user_login, c.user_type, botLogin)
|
|
924
|
+
);
|
|
925
|
+
const reply = humanReplies[humanReplies.length - 1];
|
|
926
|
+
if (reply === void 0) continue;
|
|
927
|
+
entries.push({
|
|
928
|
+
...finding,
|
|
929
|
+
repliedAt: reply.created_at,
|
|
930
|
+
replyId: reply.id,
|
|
931
|
+
threadUrl: root.html_url,
|
|
932
|
+
replyUrl: reply.html_url,
|
|
933
|
+
replyAuthor: reply.user_login,
|
|
934
|
+
replyExcerpt: clipText(reply.body ?? "", EXCERPT_LIMIT)
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
938
|
+
for (const entry of [...entries].sort(
|
|
939
|
+
(a, b) => (b.repliedAt ?? "").localeCompare(a.repliedAt ?? "") || b.replyId - a.replyId
|
|
940
|
+
)) {
|
|
941
|
+
const key2 = answeredNoteKey(entry);
|
|
942
|
+
if (!byKey.has(key2)) byKey.set(key2, entry);
|
|
943
|
+
}
|
|
944
|
+
return [...byKey.values()];
|
|
945
|
+
};
|
|
946
|
+
var matches = (f, e) => f.code !== void 0 && f.code !== "" ? e.code === f.code : e.code === "" && e.title === f.title;
|
|
947
|
+
var answeredNoteKey = (f) => f.code !== void 0 && f.code !== "" ? f.code : `title:${f.title}`;
|
|
948
|
+
var answeredNote = (e) => `Re-raised; prior answer at ${e.replyUrl} by ${e.replyAuthor} \u2014 cite the new evidence that invalidates it.`;
|
|
949
|
+
var applyAnswered = (findings, registry) => {
|
|
950
|
+
const kept = [];
|
|
951
|
+
const noteEntries = [];
|
|
952
|
+
const droppedByKey = /* @__PURE__ */ new Map();
|
|
953
|
+
let droppedCount = 0;
|
|
954
|
+
for (const f of findings) {
|
|
955
|
+
const entry = registry.find((e) => matches(f, e));
|
|
956
|
+
if (entry === void 0) {
|
|
957
|
+
kept.push(f);
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
const verbatim = f.title === entry.title && f.description === entry.description && f.reasoning === entry.reasoning && f.severity === entry.severity && f.path === entry.path && (f.patch ?? null) === entry.patch;
|
|
961
|
+
if (verbatim && f.severity !== "critical") {
|
|
962
|
+
droppedByKey.set(answeredNoteKey(f), entry);
|
|
963
|
+
droppedCount += 1;
|
|
964
|
+
} else {
|
|
965
|
+
kept.push(f);
|
|
966
|
+
noteEntries.push([answeredNoteKey(f), answeredNote(entry)]);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return {
|
|
970
|
+
findings: kept,
|
|
971
|
+
reRaisedNotes: Object.fromEntries(noteEntries),
|
|
972
|
+
verbatimReRaised: [...droppedByKey.values()],
|
|
973
|
+
droppedCount
|
|
974
|
+
};
|
|
975
|
+
};
|
|
976
|
+
var answeredReRaiseNote = (entries, count) => {
|
|
977
|
+
if (entries.length === 0) return "";
|
|
978
|
+
const label = (e) => e.code !== "" ? `\`${escapeCodeBackticks(e.code)}\`` : `\u201C${escapeCodeBackticks(e.title)}\u201D`;
|
|
979
|
+
const lines = entries.map(
|
|
980
|
+
(e) => `> - ${label(e)} \u2014 [prior answer](${e.replyUrl}) by ${e.replyAuthor}`
|
|
981
|
+
);
|
|
982
|
+
return [
|
|
983
|
+
`> \u21A9\uFE0F **${String(count)} finding(s) re-raised without new evidence \u2014 treated as answered** (each has a human reply on its prior inline thread):`,
|
|
984
|
+
...lines
|
|
985
|
+
].join("\n");
|
|
986
|
+
};
|
|
987
|
+
var fetchThreadComments = async (ghApi, repo, prNumber) => {
|
|
988
|
+
try {
|
|
989
|
+
const rows = parseJsonl(
|
|
990
|
+
await ghApi([
|
|
991
|
+
`repos/${repo}/pulls/${String(prNumber)}/comments`,
|
|
992
|
+
"-f",
|
|
993
|
+
"per_page=100",
|
|
994
|
+
"--paginate",
|
|
995
|
+
"--jq",
|
|
996
|
+
THREAD_COMMENT_JQ
|
|
997
|
+
])
|
|
998
|
+
);
|
|
999
|
+
return rows.flatMap((row) => {
|
|
1000
|
+
const decoded = ThreadCommentCodec.decode(row);
|
|
1001
|
+
return decoded._tag === "Right" ? [decoded.right] : [];
|
|
1002
|
+
});
|
|
1003
|
+
} catch (err) {
|
|
1004
|
+
process.stderr.write(
|
|
1005
|
+
`Warning: could not fetch review threads to detect answered findings (${errMsg(err)}) \u2014 no answered-finding state for this post
|
|
1006
|
+
`
|
|
1007
|
+
);
|
|
1008
|
+
return null;
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
631
1011
|
|
|
632
1012
|
// src/render.ts
|
|
633
1013
|
var escapePipes = (text) => text.replace(/\|/g, "\\|");
|
|
634
|
-
var sanitizeFinding = (f) =>
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
1014
|
+
var sanitizeFinding = (f, answeredNotes) => {
|
|
1015
|
+
const key2 = answeredNoteKey(f);
|
|
1016
|
+
return {
|
|
1017
|
+
...f,
|
|
1018
|
+
title: escapePipes(f.title),
|
|
1019
|
+
path: escapeCodeBackticks(f.path),
|
|
1020
|
+
patchProjection: projectPatch(f.patch),
|
|
1021
|
+
answeredNote: answeredNotes !== void 0 && Object.prototype.hasOwnProperty.call(answeredNotes, key2) ? answeredNotes[key2] ?? "" : ""
|
|
1022
|
+
};
|
|
1023
|
+
};
|
|
640
1024
|
var sanitizeSystemic = (s) => ({
|
|
641
1025
|
...s,
|
|
642
1026
|
title: escapePipes(s.title),
|
|
@@ -675,6 +1059,7 @@ var render = (input) => {
|
|
|
675
1059
|
const isFullReviewRound = (input.convergenceRound ?? (isConvergenceRound(route, incomplete) && rounds.length > 0)) && isReviewVerdict(input.findings.verdict);
|
|
676
1060
|
const convergenceCounts = rounds[rounds.length - 1] ?? computeRoundCounts(input.findings);
|
|
677
1061
|
const advisoryAllowed = isFullReviewRound;
|
|
1062
|
+
const scopeMetastasis = input.scopeMetastasis !== void 0 ? input.scopeMetastasis : advisoryAllowed ? computeScopeMetastasis(rounds) : null;
|
|
678
1063
|
return eta.renderString(input.template, {
|
|
679
1064
|
findings: input.findings,
|
|
680
1065
|
envelope: input.envelope,
|
|
@@ -691,7 +1076,7 @@ var render = (input) => {
|
|
|
691
1076
|
postedAt: input.postedAt ?? "",
|
|
692
1077
|
severityCounts,
|
|
693
1078
|
convergenceSummary: isFullReviewRound ? convergenceSummary(convergenceCounts, input.convergenceThreshold) : "",
|
|
694
|
-
strays: (input.strays ?? []).map(sanitizeFinding),
|
|
1079
|
+
strays: (input.strays ?? []).map((f) => sanitizeFinding(f, input.answeredNotes)),
|
|
695
1080
|
systemic: (input.findings.systemic_problems ?? []).map(sanitizeSystemic),
|
|
696
1081
|
unanchoredCount: input.unanchoredCount ?? 0,
|
|
697
1082
|
inlineDisposition: input.inlineDisposition ?? null,
|
|
@@ -705,12 +1090,15 @@ var render = (input) => {
|
|
|
705
1090
|
// label does; post always supplies the marker, so this path cannot disagree with it in
|
|
706
1091
|
// production (issue #141 review r4).
|
|
707
1092
|
isFullReviewRound && rounds.length > 0 ? signalForRound(rounds.length, convergenceCounts, input.convergenceThreshold) : null,
|
|
708
|
-
input.jsonUrl
|
|
1093
|
+
input.jsonUrl,
|
|
1094
|
+
scopeMetastasis
|
|
709
1095
|
),
|
|
710
1096
|
roundsMarker: roundsMarker(rounds),
|
|
711
1097
|
roundsSummary: roundsSummary(rounds, input.roundCount),
|
|
712
1098
|
metastasisNote: advisoryAllowed ? metastasisNote(rounds) : "",
|
|
713
1099
|
sameRootNotes: advisoryAllowed ? sameRootNotes : {},
|
|
1100
|
+
answeredNotes: input.answeredNotes ?? {},
|
|
1101
|
+
answeredReRaiseNote: input.answeredReRaiseNote ?? "",
|
|
714
1102
|
reviewUrl: input.reviewUrl ?? null,
|
|
715
1103
|
formatTokens: (n) => Number.isFinite(n) && n >= 0 ? n.toLocaleString("en-US") : "\u2014",
|
|
716
1104
|
// N/A (never a false $0.00) when no real price map was provided — real tokens, no rates to price them.
|
|
@@ -793,7 +1181,7 @@ var partitionFindings = (findings, index) => {
|
|
|
793
1181
|
|
|
794
1182
|
// src/inline.ts
|
|
795
1183
|
var formatModels = (models) => models.length > 0 ? models.map((m) => `\`${m}\``).join("/") : "an AI model";
|
|
796
|
-
var renderCommentBody = (f, eta, template, modelsText, jsonUrl, pointer, sameRootNote) => (
|
|
1184
|
+
var renderCommentBody = (f, eta, template, modelsText, jsonUrl, pointer, sameRootNote, answeredNote2) => (
|
|
797
1185
|
// Eta.renderString returns string | Promise<string>; with autoTrim:false it's always sync.
|
|
798
1186
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
799
1187
|
eta.renderString(template, {
|
|
@@ -804,7 +1192,8 @@ var renderCommentBody = (f, eta, template, modelsText, jsonUrl, pointer, sameRoo
|
|
|
804
1192
|
modelsText,
|
|
805
1193
|
jsonUrl: jsonUrl ?? null,
|
|
806
1194
|
findingsPointer: pointer,
|
|
807
|
-
sameRootNote
|
|
1195
|
+
sameRootNote,
|
|
1196
|
+
answeredNote: answeredNote2
|
|
808
1197
|
})
|
|
809
1198
|
);
|
|
810
1199
|
var buildInlineComments = (findings, diff, context) => {
|
|
@@ -813,14 +1202,29 @@ var buildInlineComments = (findings, diff, context) => {
|
|
|
813
1202
|
const { inDiff, strays } = partitionFindings(findings, index);
|
|
814
1203
|
const eta = new Eta({ autoTrim: false });
|
|
815
1204
|
const modelsText = formatModels(models);
|
|
1205
|
+
const noteFor = (f, notes) => {
|
|
1206
|
+
if (notes === void 0) return "";
|
|
1207
|
+
const key2 = answeredNoteKey(f);
|
|
1208
|
+
return Object.prototype.hasOwnProperty.call(notes, key2) ? notes[key2] ?? "" : "";
|
|
1209
|
+
};
|
|
816
1210
|
const comments = inDiff.map((f) => {
|
|
817
1211
|
const pointer = fullFindings ? findingPointer(f, fullFindings.schema_version, jsonUrl) : "";
|
|
818
|
-
const sameRootNote = f
|
|
1212
|
+
const sameRootNote = noteFor(f, context.sameRootNotes);
|
|
1213
|
+
const answeredNote2 = noteFor(f, context.answeredNotes);
|
|
819
1214
|
const comment = {
|
|
820
1215
|
path: f.path,
|
|
821
1216
|
line: f.end_line,
|
|
822
1217
|
side: defaultSide(f.side),
|
|
823
|
-
body: renderCommentBody(
|
|
1218
|
+
body: renderCommentBody(
|
|
1219
|
+
f,
|
|
1220
|
+
eta,
|
|
1221
|
+
inlineTemplate,
|
|
1222
|
+
modelsText,
|
|
1223
|
+
jsonUrl,
|
|
1224
|
+
pointer,
|
|
1225
|
+
sameRootNote,
|
|
1226
|
+
answeredNote2
|
|
1227
|
+
)
|
|
824
1228
|
};
|
|
825
1229
|
if (f.start_line < f.end_line) {
|
|
826
1230
|
return {
|
|
@@ -847,129 +1251,6 @@ var renderStraysSection = (strays) => {
|
|
|
847
1251
|
...items
|
|
848
1252
|
].join("\n");
|
|
849
1253
|
};
|
|
850
|
-
var asRecord = (u) => typeof u === "object" && u !== null && !Array.isArray(u) ? u : null;
|
|
851
|
-
var errMsg = (e) => e instanceof Error ? e.message : String(e);
|
|
852
|
-
var annotationSafe = (msg) => msg.replaceAll(/[\r\n]+/g, " ");
|
|
853
|
-
var tryParseJson = (text) => {
|
|
854
|
-
try {
|
|
855
|
-
return { ok: true, value: JSON.parse(text) };
|
|
856
|
-
} catch {
|
|
857
|
-
return { ok: false };
|
|
858
|
-
}
|
|
859
|
-
};
|
|
860
|
-
var readFileOrNull = (path) => {
|
|
861
|
-
try {
|
|
862
|
-
return readFileSync(path, "utf-8");
|
|
863
|
-
} catch {
|
|
864
|
-
return null;
|
|
865
|
-
}
|
|
866
|
-
};
|
|
867
|
-
|
|
868
|
-
// src/transcript.ts
|
|
869
|
-
var numField = (rec, key2) => {
|
|
870
|
-
const v = rec[key2];
|
|
871
|
-
return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0;
|
|
872
|
-
};
|
|
873
|
-
var messageUsage = (entry) => {
|
|
874
|
-
const rec = asRecord(entry);
|
|
875
|
-
if (rec === null || rec["type"] !== "assistant") return null;
|
|
876
|
-
const msg = asRecord(rec["message"]);
|
|
877
|
-
if (msg === null) return null;
|
|
878
|
-
const model = msg["model"];
|
|
879
|
-
const usage = asRecord(msg["usage"]);
|
|
880
|
-
if (typeof model !== "string" || usage === null) return null;
|
|
881
|
-
const id = msg["id"];
|
|
882
|
-
return {
|
|
883
|
-
id: typeof id === "string" ? id : null,
|
|
884
|
-
model,
|
|
885
|
-
input: numField(usage, "input_tokens"),
|
|
886
|
-
output: numField(usage, "output_tokens"),
|
|
887
|
-
cacheRead: numField(usage, "cache_read_input_tokens"),
|
|
888
|
-
cacheWrite: numField(usage, "cache_creation_input_tokens")
|
|
889
|
-
};
|
|
890
|
-
};
|
|
891
|
-
var tsMsOf = (entry) => {
|
|
892
|
-
const rec = asRecord(entry);
|
|
893
|
-
const ts = rec?.["timestamp"];
|
|
894
|
-
if (typeof ts !== "string") return null;
|
|
895
|
-
const ms = Date.parse(ts);
|
|
896
|
-
return Number.isNaN(ms) ? null : ms;
|
|
897
|
-
};
|
|
898
|
-
var EMPTY_TOTALS = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
899
|
-
var parseJsonl = (text) => text.split("\n").filter((line) => line.trim() !== "").flatMap((line) => {
|
|
900
|
-
try {
|
|
901
|
-
return [JSON.parse(line)];
|
|
902
|
-
} catch {
|
|
903
|
-
return [];
|
|
904
|
-
}
|
|
905
|
-
});
|
|
906
|
-
var sumTranscriptUsage = (entries) => {
|
|
907
|
-
const summed = entries.reduce(
|
|
908
|
-
(acc, entry) => {
|
|
909
|
-
const u = messageUsage(entry);
|
|
910
|
-
if (u === null) return acc;
|
|
911
|
-
if (u.id !== null && acc.seen.has(u.id)) return acc;
|
|
912
|
-
if (u.id !== null) acc.seen.add(u.id);
|
|
913
|
-
const prev = acc.totals.get(u.model) ?? EMPTY_TOTALS;
|
|
914
|
-
acc.totals.set(u.model, {
|
|
915
|
-
input: prev.input + u.input,
|
|
916
|
-
output: prev.output + u.output,
|
|
917
|
-
cacheRead: prev.cacheRead + u.cacheRead,
|
|
918
|
-
cacheWrite: prev.cacheWrite + u.cacheWrite
|
|
919
|
-
});
|
|
920
|
-
return { totals: acc.totals, turns: acc.turns + 1, seen: acc.seen };
|
|
921
|
-
},
|
|
922
|
-
{ totals: /* @__PURE__ */ new Map(), turns: 0, seen: /* @__PURE__ */ new Set() }
|
|
923
|
-
);
|
|
924
|
-
const models = [...summed.totals].map(([model, t7]) => ({
|
|
925
|
-
model,
|
|
926
|
-
input_tokens: t7.input,
|
|
927
|
-
output_tokens: t7.output,
|
|
928
|
-
cache_read_tokens: t7.cacheRead,
|
|
929
|
-
cache_write_tokens: t7.cacheWrite
|
|
930
|
-
}));
|
|
931
|
-
const bounds = entries.reduce(
|
|
932
|
-
(acc, entry) => {
|
|
933
|
-
const ms = tsMsOf(entry);
|
|
934
|
-
if (ms === null) return acc;
|
|
935
|
-
return {
|
|
936
|
-
min: acc.min === null || ms < acc.min ? ms : acc.min,
|
|
937
|
-
max: acc.max === null || ms > acc.max ? ms : acc.max
|
|
938
|
-
};
|
|
939
|
-
},
|
|
940
|
-
{ min: null, max: null }
|
|
941
|
-
);
|
|
942
|
-
return {
|
|
943
|
-
models,
|
|
944
|
-
turns: summed.turns,
|
|
945
|
-
durationMs: bounds.min !== null && bounds.max !== null ? bounds.max - bounds.min : 0,
|
|
946
|
-
firstTsMs: bounds.min,
|
|
947
|
-
lastTsMs: bounds.max
|
|
948
|
-
};
|
|
949
|
-
};
|
|
950
|
-
var subagentFiles = (mainPath) => {
|
|
951
|
-
const dir = join(dirname(mainPath), basename(mainPath, ".jsonl"), "subagents");
|
|
952
|
-
try {
|
|
953
|
-
return readdirSync(dir).filter((name) => name.endsWith(".jsonl")).map((name) => join(dir, name));
|
|
954
|
-
} catch {
|
|
955
|
-
return [];
|
|
956
|
-
}
|
|
957
|
-
};
|
|
958
|
-
var readTranscriptTree = (mainPath) => {
|
|
959
|
-
const mainText = readFileOrNull(mainPath);
|
|
960
|
-
const mainEntries = mainText === null ? [] : parseJsonl(mainText);
|
|
961
|
-
const inlineSidechains = mainEntries.some((e) => asRecord(e)?.["isSidechain"] === true);
|
|
962
|
-
const siblings = inlineSidechains ? [] : subagentFiles(mainPath);
|
|
963
|
-
const siblingReads = siblings.flatMap((path) => {
|
|
964
|
-
const text = readFileOrNull(path);
|
|
965
|
-
return text === null ? [] : [{ path, entries: parseJsonl(text) }];
|
|
966
|
-
});
|
|
967
|
-
return {
|
|
968
|
-
entries: [...mainEntries, ...siblingReads.flatMap((r) => r.entries)],
|
|
969
|
-
files: [...mainText === null ? [] : [mainPath], ...siblingReads.map((r) => r.path)],
|
|
970
|
-
missing: mainText === null
|
|
971
|
-
};
|
|
972
|
-
};
|
|
973
1254
|
var addFormats = _addFormats;
|
|
974
1255
|
var validatorCache = /* @__PURE__ */ new Map();
|
|
975
1256
|
var compileSchema = (schemaPath) => {
|
|
@@ -1151,7 +1432,7 @@ var writesToDraft = (toolName, toolInput, draftPath) => {
|
|
|
1151
1432
|
const targets = [draftPath, basename(draftPath), "$DRAFT", "${DRAFT}"];
|
|
1152
1433
|
if (WRITE_TOOLS.has(toolName)) {
|
|
1153
1434
|
const fp = rec?.["file_path"] ?? rec?.["notebook_path"];
|
|
1154
|
-
if (typeof fp === "string" && targets.some((
|
|
1435
|
+
if (typeof fp === "string" && targets.some((t8) => fp === t8 || basename(fp) === basename(t8)))
|
|
1155
1436
|
return true;
|
|
1156
1437
|
}
|
|
1157
1438
|
if (toolName === "Bash") {
|
|
@@ -1178,6 +1459,7 @@ var sidecarPath = (draftPath, postfix) => {
|
|
|
1178
1459
|
return join(dirname(draftPath), `${basename(draftPath, ext)}${postfix}${ext}`);
|
|
1179
1460
|
};
|
|
1180
1461
|
var priorContextPath = (draftPath) => sidecarPath(draftPath, ".prior");
|
|
1462
|
+
var priorAnswersPath = (draftPath) => sidecarPath(draftPath, ".prior-answers");
|
|
1181
1463
|
var lastValidPath = (draftPath) => sidecarPath(draftPath, ".last-valid");
|
|
1182
1464
|
var spawnFloorMessage = (draftPath) => `Write your own first-pass findings to ${draftPath} before spawning subagents \u2014 a review must never depend on subagents alone, and the pre-seeded $DRAFT is a non-review sentinel: it does not count until you have replaced it with your own review. 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.`;
|
|
1183
1465
|
var forceBackgroundSpawn = (toolInput) => ({
|
|
@@ -1254,9 +1536,9 @@ var parseWallMs = (raw) => {
|
|
|
1254
1536
|
};
|
|
1255
1537
|
var parseEpochSecMs = (raw) => {
|
|
1256
1538
|
if (raw === void 0) return null;
|
|
1257
|
-
const
|
|
1258
|
-
if (!/^\d+$/.test(
|
|
1259
|
-
const n = Number.parseInt(
|
|
1539
|
+
const t8 = raw.trim();
|
|
1540
|
+
if (!/^\d+$/.test(t8)) return null;
|
|
1541
|
+
const n = Number.parseInt(t8, 10);
|
|
1260
1542
|
return Number.isFinite(n) && n > 0 ? n * 1e3 : null;
|
|
1261
1543
|
};
|
|
1262
1544
|
var anchoredElapsedMs = (src) => {
|
|
@@ -1905,8 +2187,8 @@ var priorBotCommentIds = (raw, botLogin) => {
|
|
|
1905
2187
|
const nodes = conn?.nodes;
|
|
1906
2188
|
if (!Array.isArray(nodes)) return { ids: [], truncated };
|
|
1907
2189
|
const logins = [botLogin.replace(/\[bot\]$/, ""), botLogin];
|
|
1908
|
-
const ids = nodes.flatMap((
|
|
1909
|
-
const cnodes =
|
|
2190
|
+
const ids = nodes.flatMap((t8) => {
|
|
2191
|
+
const cnodes = t8.comments?.nodes;
|
|
1910
2192
|
return Array.isArray(cnodes) ? cnodes.map((c) => priorBotCommentId(c, logins)).filter((id) => id !== null) : [];
|
|
1911
2193
|
});
|
|
1912
2194
|
return { ids, truncated };
|
|
@@ -1981,22 +2263,18 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1981
2263
|
process.exit(0);
|
|
1982
2264
|
}
|
|
1983
2265
|
const prNumber = resolution.prNumber;
|
|
1984
|
-
const diff = await
|
|
1985
|
-
|
|
1986
|
-
input.repo,
|
|
1987
|
-
|
|
1988
|
-
input.botLogin,
|
|
1989
|
-
DEFAULT_MARKER,
|
|
1990
|
-
ghApi
|
|
1991
|
-
);
|
|
2266
|
+
const [diff, existingSticky] = await Promise.all([
|
|
2267
|
+
fetchDiff(input.repo, prNumber, ghApi),
|
|
2268
|
+
findBotComment(input.repo, prNumber, input.botLogin, DEFAULT_MARKER, ghApi)
|
|
2269
|
+
]);
|
|
1992
2270
|
const existingComplete = existingSticky !== null && parseReviewComplete(existingSticky.body);
|
|
1993
2271
|
const wouldBuryCompleted = (incomplete) => incomplete && existingComplete;
|
|
1994
2272
|
const priorIsFullReview = (body) => isFullReviewSticky(body) || parseReviewedRoute(body) === null && (parseReviewComplete(body) || parseCompletedAncestor(body));
|
|
1995
2273
|
const emptyMechanicWouldBury = (route, incomplete) => route === "mechanic" && !incomplete && findings.findings.length === 0 && existingSticky !== null && priorIsFullReview(existingSticky.body);
|
|
1996
2274
|
const priorRounds = existingSticky !== null ? parseRounds(existingSticky.body) : [];
|
|
1997
2275
|
const priorSignal = existingSticky === null ? null : parseSignalMarker(existingSticky.body) ?? parseSurfaceSignal(parseFindingsMarker(existingSticky.body));
|
|
1998
|
-
const findingsMarkerFor = (findings2, signal2) => {
|
|
1999
|
-
const pointer = surfacedFindingsPointer(findings2, signal2, input.jsonUrl);
|
|
2276
|
+
const findingsMarkerFor = (findings2, signal2, scopeMetastasis2) => {
|
|
2277
|
+
const pointer = surfacedFindingsPointer(findings2, signal2, input.jsonUrl, scopeMetastasis2);
|
|
2000
2278
|
return signal2 !== null || priorSignal === null ? pointer : `${pointer}
|
|
2001
2279
|
${signalMarker(priorSignal)}`;
|
|
2002
2280
|
};
|
|
@@ -2006,14 +2284,28 @@ ${signalMarker(priorSignal)}`;
|
|
|
2006
2284
|
);
|
|
2007
2285
|
process.exit(0);
|
|
2008
2286
|
};
|
|
2287
|
+
const logAnsweredDrops = () => {
|
|
2288
|
+
if (verbatimReRaised.length > 0) {
|
|
2289
|
+
process.stderr.write(
|
|
2290
|
+
`${String(droppedCount)} verbatim re-raise(s) of answered findings were treated as answered \u2014 the preserved sticky shows each finding and its prior reply
|
|
2291
|
+
`
|
|
2292
|
+
);
|
|
2293
|
+
}
|
|
2294
|
+
};
|
|
2009
2295
|
const emptyMechanicLeaveOrNote = async (sticky) => {
|
|
2010
|
-
if (existingComplete)
|
|
2296
|
+
if (existingComplete) {
|
|
2297
|
+
logAnsweredDrops();
|
|
2298
|
+
leaveInPlace(EMPTY_MECHANIC_LEAVE_MESSAGE);
|
|
2299
|
+
}
|
|
2011
2300
|
const priorSha = parseReviewedSha(sticky.body);
|
|
2301
|
+
const dropNote = answeredDropNote;
|
|
2012
2302
|
const body = formatMarkdown(
|
|
2013
2303
|
noticeBody(
|
|
2014
2304
|
`${DEFAULT_MARKER}
|
|
2015
2305
|
|
|
2016
|
-
\u26A0\uFE0F **CI-fix pass completed with no findings** for \`${input.headSha.slice(0, 7)}\` \u2014 the completed full review of \`${priorSha ? priorSha.slice(0, 7) : "an earlier commit"}\` is preserved below
|
|
2306
|
+
\u26A0\uFE0F **CI-fix pass completed with no findings** for \`${input.headSha.slice(0, 7)}\` \u2014 the completed full review of \`${priorSha ? priorSha.slice(0, 7) : "an earlier commit"}\` is preserved below.${dropNote ? `
|
|
2307
|
+
|
|
2308
|
+
${dropNote}` : ""}`,
|
|
2017
2309
|
sticky.body
|
|
2018
2310
|
)
|
|
2019
2311
|
);
|
|
@@ -2078,13 +2370,38 @@ ${signalMarker(priorSignal)}`;
|
|
|
2078
2370
|
);
|
|
2079
2371
|
process.exit(0);
|
|
2080
2372
|
}
|
|
2081
|
-
const
|
|
2373
|
+
const loadedFindings = findingsResult.findings;
|
|
2374
|
+
const threadComments = await fetchThreadComments(ghApi, input.repo, prNumber);
|
|
2375
|
+
const answeredRegistry = threadComments === null ? [] : answeredRegistryFrom(threadComments, input.botLogin);
|
|
2376
|
+
const answeredFilter = applyAnswered(loadedFindings.findings, answeredRegistry);
|
|
2377
|
+
const reRaisedNotes = answeredFilter.reRaisedNotes;
|
|
2378
|
+
const verbatimReRaised = answeredFilter.verbatimReRaised;
|
|
2379
|
+
const droppedCount = answeredFilter.droppedCount;
|
|
2380
|
+
const droppedCodes = new Set(verbatimReRaised.flatMap((e) => e.code !== "" ? [e.code] : []));
|
|
2381
|
+
const keptCodes = new Set(
|
|
2382
|
+
answeredFilter.findings.flatMap((f) => f.code !== void 0 && f.code !== "" ? [f.code] : [])
|
|
2383
|
+
);
|
|
2384
|
+
const trulyDropped = new Set([...droppedCodes].filter((c) => !keptCodes.has(c)));
|
|
2385
|
+
const systemic = trulyDropped.size === 0 ? loadedFindings.systemic_problems ?? [] : (loadedFindings.systemic_problems ?? []).map((s) => {
|
|
2386
|
+
if (s.finding_codes === void 0) return s;
|
|
2387
|
+
const codes = s.finding_codes.filter((c) => !trulyDropped.has(c));
|
|
2388
|
+
return codes.length === s.finding_codes.length ? s : { ...s, finding_codes: codes };
|
|
2389
|
+
});
|
|
2390
|
+
const findings = {
|
|
2391
|
+
...loadedFindings,
|
|
2392
|
+
findings: [...answeredFilter.findings],
|
|
2393
|
+
...systemic.length > 0 ? { systemic_problems: systemic } : {}
|
|
2394
|
+
};
|
|
2395
|
+
const answeredDropNote = answeredReRaiseNote(verbatimReRaised, droppedCount) + (verbatimReRaised.length > 0 && findings.findings.length === 0 ? "\n> _The stop signal reflects the kept findings \u2014 this round carries none._" : "");
|
|
2082
2396
|
const envelope = loadEnvelope(input.envelopePath);
|
|
2083
2397
|
const testReport = input.testReportPath ? loadTestReport(input.testReportPath) : void 0;
|
|
2084
2398
|
const effectiveRoute = input.route ?? envelope?.route;
|
|
2085
2399
|
if (envelope === null) {
|
|
2086
2400
|
const envelopelessIncomplete = isIncompleteFindings(findings);
|
|
2087
|
-
if (wouldBuryCompleted(envelopelessIncomplete))
|
|
2401
|
+
if (wouldBuryCompleted(envelopelessIncomplete)) {
|
|
2402
|
+
logAnsweredDrops();
|
|
2403
|
+
leaveInPlace();
|
|
2404
|
+
}
|
|
2088
2405
|
if (emptyMechanicWouldBury(effectiveRoute, envelopelessIncomplete) && existingSticky !== null)
|
|
2089
2406
|
await emptyMechanicLeaveOrNote(existingSticky);
|
|
2090
2407
|
const body = formatMarkdown(
|
|
@@ -2100,6 +2417,13 @@ ${signalMarker(priorSignal)}`;
|
|
|
2100
2417
|
effort: input.effort,
|
|
2101
2418
|
rounds: priorRounds,
|
|
2102
2419
|
sameRootNotes: {},
|
|
2420
|
+
// The answered-state honesty rules apply on EVERY surface that renders the filtered
|
|
2421
|
+
// findings — the lost-envelope branch lists every finding (no inline review exists to
|
|
2422
|
+
// carry them) so the kept re-raises' annotations actually render, and names the drops
|
|
2423
|
+
// exactly like the main path (issues #151 review r1 + r2).
|
|
2424
|
+
strays: findings.findings,
|
|
2425
|
+
answeredNotes: reRaisedNotes,
|
|
2426
|
+
answeredReRaiseNote: answeredDropNote,
|
|
2103
2427
|
roundCount: priorSignal?.round ?? priorRounds.length,
|
|
2104
2428
|
convergenceRound: false,
|
|
2105
2429
|
testReport,
|
|
@@ -2122,7 +2446,10 @@ ${signalMarker(priorSignal)}`;
|
|
|
2122
2446
|
process.exit(0);
|
|
2123
2447
|
}
|
|
2124
2448
|
const thisIncomplete = envelope.incomplete === true || isIncompleteFindings(findings);
|
|
2125
|
-
if (wouldBuryCompleted(thisIncomplete))
|
|
2449
|
+
if (wouldBuryCompleted(thisIncomplete)) {
|
|
2450
|
+
logAnsweredDrops();
|
|
2451
|
+
leaveInPlace();
|
|
2452
|
+
}
|
|
2126
2453
|
if (emptyMechanicWouldBury(effectiveRoute, thisIncomplete) && existingSticky !== null)
|
|
2127
2454
|
await emptyMechanicLeaveOrNote(existingSticky);
|
|
2128
2455
|
const isRound = isConvergenceRound(effectiveRoute, thisIncomplete) && isReviewVerdict(findings.verdict);
|
|
@@ -2136,7 +2463,8 @@ ${signalMarker(priorSignal)}`;
|
|
|
2136
2463
|
models: envelope.models.map((m) => m.model),
|
|
2137
2464
|
findings,
|
|
2138
2465
|
jsonUrl: input.jsonUrl,
|
|
2139
|
-
sameRootNotes
|
|
2466
|
+
sameRootNotes,
|
|
2467
|
+
answeredNotes: reRaisedNotes
|
|
2140
2468
|
});
|
|
2141
2469
|
const { comments, longFiles } = checkLongSuggestions(rawComments);
|
|
2142
2470
|
for (const wf of longFiles) {
|
|
@@ -2162,8 +2490,12 @@ ${signalMarker(priorSignal)}`;
|
|
|
2162
2490
|
)
|
|
2163
2491
|
] : priorRounds;
|
|
2164
2492
|
const signal = isRound ? signalForRound(roundNumber, computeRoundCounts(findings), input.convergenceThreshold) : thisIncomplete || !isReviewVerdict(findings.verdict) ? null : priorSignal;
|
|
2165
|
-
const
|
|
2166
|
-
const
|
|
2493
|
+
const scopeMetastasis = isRound ? computeScopeMetastasis(rounds) : null;
|
|
2494
|
+
const findingsMarker = findingsMarkerFor(findings, signal, scopeMetastasis);
|
|
2495
|
+
const markerForm = findingsMarkerForm(
|
|
2496
|
+
surfaceFindings(findings, signal, scopeMetastasis),
|
|
2497
|
+
input.jsonUrl
|
|
2498
|
+
);
|
|
2167
2499
|
if (markerForm === "link") {
|
|
2168
2500
|
process.stderr.write(
|
|
2169
2501
|
"Warning: the findings-json marker exceeds the embed limit \u2014 degraded to the jsonUrl-link form; a decoding agent must fetch the artifact instead of the embedded JSON\n"
|
|
@@ -2187,6 +2519,9 @@ ${signalMarker(priorSignal)}`;
|
|
|
2187
2519
|
severityCounts: currentCounts,
|
|
2188
2520
|
rounds,
|
|
2189
2521
|
sameRootNotes,
|
|
2522
|
+
answeredNotes: reRaisedNotes,
|
|
2523
|
+
answeredReRaiseNote: answeredDropNote,
|
|
2524
|
+
scopeMetastasis,
|
|
2190
2525
|
roundCount: signal?.round ?? priorSignal?.round ?? priorRounds.length,
|
|
2191
2526
|
convergenceThreshold: input.convergenceThreshold,
|
|
2192
2527
|
convergenceRound: isRound,
|
|
@@ -2726,7 +3061,7 @@ var fetchCompareCommits = async (repo, defaultBranch, headSha, ghApi) => {
|
|
|
2726
3061
|
return commits;
|
|
2727
3062
|
};
|
|
2728
3063
|
var COMMENT_JQ = ".[] | {id: .id, body: .body, user: {login: .user.login}, created_at: .created_at, author_association: .author_association}";
|
|
2729
|
-
var REVIEW_COMMENT_JQ =
|
|
3064
|
+
var REVIEW_COMMENT_JQ = THREAD_COMMENT_JQ;
|
|
2730
3065
|
var REVIEW_JQ = ".[] | {body: .body, user: {login: .user.login}, submitted_at: .submitted_at, author_association: .author_association, state: .state}";
|
|
2731
3066
|
var ReviewCommentCodec = t.intersection([
|
|
2732
3067
|
t.type({ body: t.union([t.string, t.null]), user: t.type({ login: t.string }) }),
|
|
@@ -2770,13 +3105,7 @@ var priorReviewFrom = (comments, botLogin) => {
|
|
|
2770
3105
|
};
|
|
2771
3106
|
var MAX_CONVERSATION_COMMENTS = 50;
|
|
2772
3107
|
var MAX_CONVERSATION_BODY_CHARS = 4e3;
|
|
2773
|
-
var clip = (body) =>
|
|
2774
|
-
if (body.length <= MAX_CONVERSATION_BODY_CHARS) return body;
|
|
2775
|
-
const cut = body.slice(0, MAX_CONVERSATION_BODY_CHARS);
|
|
2776
|
-
const safe = /[\uD800-\uDBFF]$/.test(cut) ? cut.slice(0, -1) : cut;
|
|
2777
|
-
return `${safe}
|
|
2778
|
-
\u2026 [truncated]`;
|
|
2779
|
-
};
|
|
3108
|
+
var clip = (body) => clipText(body, MAX_CONVERSATION_BODY_CHARS);
|
|
2780
3109
|
var boundedHuman = (items, botLogin, label, project) => {
|
|
2781
3110
|
const human = items.filter(
|
|
2782
3111
|
(a) => a.user.login !== botLogin && typeof a.body === "string" && a.body.trim() !== ""
|
|
@@ -2877,12 +3206,18 @@ var gather = async (input, ghApi = runGhApi, gitRun = runGit) => {
|
|
|
2877
3206
|
]);
|
|
2878
3207
|
const issueComments = decodeArrayOrNull(IssueCommentCodec, issueRows);
|
|
2879
3208
|
const reviewComments = decodeArrayOrNull(ReviewCommentCodec, reviewCommentRows);
|
|
3209
|
+
const threadComments = decodeArrayOrNull(ThreadCommentCodec, reviewCommentRows);
|
|
2880
3210
|
const reviews = decodeArrayOrNull(ReviewCodec, reviewRows);
|
|
2881
3211
|
const prior = issueComments === null ? null : priorReviewFrom(issueComments, input.botLogin);
|
|
2882
3212
|
writeFileSync(
|
|
2883
3213
|
join(input.outDir, "prior_review.json"),
|
|
2884
3214
|
prior === null ? "null" : JSON.stringify(prior)
|
|
2885
3215
|
);
|
|
3216
|
+
const answered = threadComments === null ? [] : answeredRegistryFrom(threadComments, input.botLogin);
|
|
3217
|
+
writeFileSync(
|
|
3218
|
+
join(input.outDir, "answered.json"),
|
|
3219
|
+
JSON.stringify(answered.map(encodeAnsweredEntry))
|
|
3220
|
+
);
|
|
2886
3221
|
writeFileSync(
|
|
2887
3222
|
join(input.outDir, "pr_conversation.json"),
|
|
2888
3223
|
JSON.stringify({
|
|
@@ -3236,7 +3571,7 @@ var parseScope = (raw) => {
|
|
|
3236
3571
|
reason: 'scope contains a character (newline, carriage return, backtick, "<", ">", or "|") that would corrupt the review prompt \u2014 use plain language names/tags'
|
|
3237
3572
|
};
|
|
3238
3573
|
}
|
|
3239
|
-
const languages = Array.from(new Set(trimmed.split(SCOPE_SEPARATOR_RE).filter((
|
|
3574
|
+
const languages = Array.from(new Set(trimmed.split(SCOPE_SEPARATOR_RE).filter((t8) => t8 !== "")));
|
|
3240
3575
|
return languages.length === 0 ? { kind: "absent" } : { kind: "ok", languages };
|
|
3241
3576
|
};
|
|
3242
3577
|
|
|
@@ -3841,6 +4176,10 @@ var seedDraftCmd = defineCommand({
|
|
|
3841
4176
|
type: "string",
|
|
3842
4177
|
description: "Path to the prior-review JSON gather staged ({ id, body }, or the literal null); its embedded base64 findings marker is decoded and delivered as re-review context when it validates against the schema"
|
|
3843
4178
|
},
|
|
4179
|
+
"prior-answers": {
|
|
4180
|
+
type: "string",
|
|
4181
|
+
description: "Path to the gather-staged answered-findings registry (answered.json) \u2014 the prior inline findings whose threads a human reply answered (issue #151). Delivered out-of-band to the .prior-answers sidecar beside the prior context so the next-round agent sees the already-answered state; best-effort, never fails the seed"
|
|
4182
|
+
},
|
|
3844
4183
|
"head-sha": {
|
|
3845
4184
|
type: "string",
|
|
3846
4185
|
description: "Current head SHA, compared against the prior review's embedded reviewed-sha to distinguish a same-commit re-review from a new-commit one; an unknown or mismatched prior SHA is treated as a new commit"
|
|
@@ -3900,21 +4239,64 @@ var seedDraftCmd = defineCommand({
|
|
|
3900
4239
|
})();
|
|
3901
4240
|
return typeof raw === "object" && raw !== null && "body" in raw && typeof raw.body === "string" ? raw.body : null;
|
|
3902
4241
|
})();
|
|
3903
|
-
const
|
|
4242
|
+
const strippedPrior = priorBody === null ? null : stripSurfaceFields(parseFindingsMarker(priorBody));
|
|
4243
|
+
const priorFindings = (() => {
|
|
4244
|
+
if (strippedPrior === null || typeof strippedPrior !== "object" || Array.isArray(strippedPrior))
|
|
4245
|
+
return strippedPrior;
|
|
4246
|
+
const carried = strippedPrior["scope_metastasis"];
|
|
4247
|
+
if (ScopeMetastasisCodec.decode(carried)._tag === "Right") return strippedPrior;
|
|
4248
|
+
if (strippedPrior["verdict"] === "error") return strippedPrior;
|
|
4249
|
+
const computed = computeScopeMetastasis(parseRounds(priorBody ?? ""));
|
|
4250
|
+
return computed === null ? strippedPrior : { ...strippedPrior, scope_metastasis: computed };
|
|
4251
|
+
})();
|
|
4252
|
+
if (args["prior-answers"]) {
|
|
4253
|
+
try {
|
|
4254
|
+
const raw = JSON.parse(readFileSync(resolve$1(args["prior-answers"]), "utf-8"));
|
|
4255
|
+
if (!Array.isArray(raw)) throw new Error("expected an array");
|
|
4256
|
+
const decoded = raw.flatMap((row) => {
|
|
4257
|
+
const entry = decodeAnsweredEntry(row);
|
|
4258
|
+
return entry === null ? [] : [entry];
|
|
4259
|
+
});
|
|
4260
|
+
if (decoded.length < raw.length) {
|
|
4261
|
+
process.stderr.write(
|
|
4262
|
+
`Warning: ${String(raw.length - decoded.length)} of ${String(raw.length)} answered-registry row(s) failed to decode \u2014 the seed's answered state is incomplete
|
|
4263
|
+
`
|
|
4264
|
+
);
|
|
4265
|
+
}
|
|
4266
|
+
writeFileSync(priorAnswersPath(outPath), `${JSON.stringify(decoded, null, 2)}
|
|
4267
|
+
`);
|
|
4268
|
+
process.stderr.write(
|
|
4269
|
+
`Seeded ${priorAnswersPath(outPath)} with ${String(decoded.length)} answered finding(s) as context
|
|
4270
|
+
`
|
|
4271
|
+
);
|
|
4272
|
+
} catch (err) {
|
|
4273
|
+
process.stderr.write(
|
|
4274
|
+
`Warning: could not read the answered-findings registry ${args["prior-answers"]} (${errMsg(err)}) \u2014 no prior-answers sidecar
|
|
4275
|
+
`
|
|
4276
|
+
);
|
|
4277
|
+
}
|
|
4278
|
+
}
|
|
3904
4279
|
const seededFromPrior = priorFindings === null ? false : (() => {
|
|
3905
4280
|
try {
|
|
3906
4281
|
const schemaPath = args.schema ? resolve$1(args.schema) : schemaPathFor(kind, args["schema-version"]);
|
|
3907
|
-
|
|
3908
|
-
|
|
4282
|
+
const barePrior = typeof priorFindings === "object" && !Array.isArray(priorFindings) ? Object.fromEntries(
|
|
4283
|
+
Object.entries(priorFindings).filter(
|
|
4284
|
+
([key2]) => key2 !== "scope_metastasis"
|
|
4285
|
+
)
|
|
4286
|
+
) : priorFindings;
|
|
4287
|
+
const accepts = (doc) => validateAgainstSchema(doc, schemaPath).valid && resolve("findings", doc).kind === "ok";
|
|
4288
|
+
const seedDoc = accepts(priorFindings) ? priorFindings : accepts(barePrior) ? (process.stderr.write(
|
|
4289
|
+
`Note: the in-force schema rejects the carried scope_metastasis entry \u2014 seeding the prior without it (issue #150 review r2)
|
|
4290
|
+
`
|
|
4291
|
+
), barePrior) : null;
|
|
4292
|
+
if (seedDoc === null) return false;
|
|
4293
|
+
const resolution = resolve("findings", seedDoc);
|
|
3909
4294
|
if (resolution.kind !== "ok") return false;
|
|
3910
4295
|
if (isIncompleteFindings(resolution.value)) return false;
|
|
3911
4296
|
if (parseReviewedRoute(priorBody ?? "") !== "full review") return false;
|
|
3912
4297
|
writeFileSync(outPath, SEED_SENTINEL);
|
|
3913
|
-
writeFileSync(
|
|
3914
|
-
|
|
3915
|
-
`${JSON.stringify(priorFindings, null, 2)}
|
|
3916
|
-
`
|
|
3917
|
-
);
|
|
4298
|
+
writeFileSync(priorContextPath(outPath), `${JSON.stringify(seedDoc, null, 2)}
|
|
4299
|
+
`);
|
|
3918
4300
|
const count = resolution.value.findings.length;
|
|
3919
4301
|
process.stderr.write(
|
|
3920
4302
|
`Seeded ${outPath} with the sentinel and wrote the prior review (${String(count)} finding(s)) to ${priorContextPath(outPath)} as context
|
|
@@ -4730,7 +5112,7 @@ var awaitCiCmd = defineCommand({
|
|
|
4730
5112
|
var checkScopeCmd = defineCommand({
|
|
4731
5113
|
meta: {
|
|
4732
5114
|
name: "check-scope",
|
|
4733
|
-
description: "Validate + normalize the workflow's `scope` input \u2014 the languages/inputs the project accepts (issue #139). Prints the normalized space-separated language list for splicing into the review prompt, or nothing when the scope is empty (the reviewer then infers it from the README's first paragraph).
|
|
5115
|
+
description: "Validate + normalize the workflow's `scope` input \u2014 the languages/inputs the project accepts (issue #139). Prints the normalized space-separated language list for splicing into the review prompt, or nothing when the scope is empty (the reviewer then infers it from the README's first paragraph). A structurally malformed value is rejected rather than spliced in as-is."
|
|
4734
5116
|
},
|
|
4735
5117
|
args: {
|
|
4736
5118
|
scope: {
|