@mrciphersmith/keryx 0.2.106 → 0.2.108
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 +6 -1
- package/dist/cli.js +648 -80
- package/package.json +1 -1
- package/src/gdskills/bundled/skills/review/review-layout/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.md +2 -2
- package/src/gdskills/bundled/skills/review/review-orchestrator/reviewer-finding.schema.json +8 -0
- package/src/gdskills/contracts/review-finding.schema.json +34 -1
package/README.md
CHANGED
|
@@ -325,7 +325,12 @@ Grouped by what you are trying to do, not by internal module layout.
|
|
|
325
325
|
trends.
|
|
326
326
|
- **review** — managed review packages, standalone under `.metaproject/reviews/`
|
|
327
327
|
or inside the flow package when attached to a flow, so review findings become
|
|
328
|
-
durable project artifacts.
|
|
328
|
+
durable project artifacts. A finding points at the code it **quotes**: the
|
|
329
|
+
line is derived by locating that quote at the commit the round records, not
|
|
330
|
+
taken on the reviewer's word, and a quote that cannot be found is marked
|
|
331
|
+
unlocatable rather than carrying a number nobody checked. Each round also
|
|
332
|
+
states its price — estimated before dispatch, recorded after, and divided by
|
|
333
|
+
the findings that survived it.
|
|
329
334
|
|
|
330
335
|
**Operate agents**
|
|
331
336
|
|
package/dist/cli.js
CHANGED
|
@@ -48579,7 +48579,7 @@ import path47 from "path";
|
|
|
48579
48579
|
// package.json
|
|
48580
48580
|
var package_default = {
|
|
48581
48581
|
name: "@mrciphersmith/keryx",
|
|
48582
|
-
version: "0.2.
|
|
48582
|
+
version: "0.2.108",
|
|
48583
48583
|
description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
|
|
48584
48584
|
private: false,
|
|
48585
48585
|
publishConfig: {
|
|
@@ -62741,7 +62741,7 @@ ${learningSourceLessons(selection).map((lesson) => `- ${lesson}
|
|
|
62741
62741
|
|
|
62742
62742
|
// src/review/managed.ts
|
|
62743
62743
|
init_validator();
|
|
62744
|
-
import { mkdir as mkdir53, readFile as readFile86, readdir as readdir28, rm as rm11 } from "fs/promises";
|
|
62744
|
+
import { mkdir as mkdir53, readFile as readFile86, readdir as readdir28, realpath as realpath8, rm as rm11, stat as stat16 } from "fs/promises";
|
|
62745
62745
|
import path153 from "path";
|
|
62746
62746
|
init_fs();
|
|
62747
62747
|
init_store2();
|
|
@@ -64692,6 +64692,163 @@ function renderFilterStatsLine(stats) {
|
|
|
64692
64692
|
return `filter_stats: total=${show(stats.total)} prefilter=${show(stats.dropped_prefilter)} low_confidence=${show(stats.dropped_low_confidence)} refuted=${show(stats.dropped_refuted)} scope_b=${show(stats.dropped_scope_b)} findings_cap=${show(stats.dropped_findings_cap)} dismissed_by_round=${show(stats.dismissed_by_round)} retained=${show(stats.retained)}`;
|
|
64693
64693
|
}
|
|
64694
64694
|
|
|
64695
|
+
// src/review/locate.ts
|
|
64696
|
+
var MAX_QUOTE_LINES = 200;
|
|
64697
|
+
var MAX_LOCATE_FILE_LINES = 50000;
|
|
64698
|
+
var MAX_LOCATE_FILE_BYTES = 4000000;
|
|
64699
|
+
function quoteLines(quote) {
|
|
64700
|
+
const lines = quote.replace(/\r\n/g, `
|
|
64701
|
+
`).split(`
|
|
64702
|
+
`);
|
|
64703
|
+
while (lines.length > 0 && lines[0].trim() === "") {
|
|
64704
|
+
lines.shift();
|
|
64705
|
+
}
|
|
64706
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
|
|
64707
|
+
lines.pop();
|
|
64708
|
+
}
|
|
64709
|
+
if (lines.length === 0) {
|
|
64710
|
+
return [];
|
|
64711
|
+
}
|
|
64712
|
+
const indents = lines.filter((line) => line.trim() !== "").map((line) => (line.match(/^[ \t]*/)?.[0] ?? "").length);
|
|
64713
|
+
const common = indents.length === 0 ? 0 : Math.min(...indents);
|
|
64714
|
+
return lines.map((line) => line.slice(common));
|
|
64715
|
+
}
|
|
64716
|
+
function normalise(line) {
|
|
64717
|
+
return line.trim().replace(/\s+/g, " ");
|
|
64718
|
+
}
|
|
64719
|
+
function deindent(lines) {
|
|
64720
|
+
const indents = lines.filter((line) => line.trim() !== "").map((line) => (line.match(/^[ \t]*/)?.[0] ?? "").length);
|
|
64721
|
+
const common = indents.length === 0 ? 0 : Math.min(...indents);
|
|
64722
|
+
return lines.map((line) => line.slice(common));
|
|
64723
|
+
}
|
|
64724
|
+
var MAX_HITS = 2;
|
|
64725
|
+
function findRuns(haystack, needle, shape) {
|
|
64726
|
+
const hits = [];
|
|
64727
|
+
if (needle.length === 0 || needle.length > haystack.length) {
|
|
64728
|
+
return hits;
|
|
64729
|
+
}
|
|
64730
|
+
for (let start = 0;start + needle.length <= haystack.length; start += 1) {
|
|
64731
|
+
const window = shape(haystack.slice(start, start + needle.length));
|
|
64732
|
+
if (window.every((line, offset) => line === needle[offset])) {
|
|
64733
|
+
hits.push(start + 1);
|
|
64734
|
+
if (hits.length >= MAX_HITS) {
|
|
64735
|
+
return hits;
|
|
64736
|
+
}
|
|
64737
|
+
}
|
|
64738
|
+
}
|
|
64739
|
+
return hits;
|
|
64740
|
+
}
|
|
64741
|
+
function ambiguity(hits, pass) {
|
|
64742
|
+
return {
|
|
64743
|
+
state: "unlocatable",
|
|
64744
|
+
reason: `the quote matches at least ${hits.length} places${pass}(lines ${hits.join(", ")}, and the scan stopped there); an anchor chosen among them would be a guess`
|
|
64745
|
+
};
|
|
64746
|
+
}
|
|
64747
|
+
function locateQuote(fileText, quote) {
|
|
64748
|
+
const needle = quoteLines(quote);
|
|
64749
|
+
if (needle.length === 0) {
|
|
64750
|
+
return { state: "unlocatable", reason: "the quote is empty" };
|
|
64751
|
+
}
|
|
64752
|
+
if (needle.length > MAX_QUOTE_LINES) {
|
|
64753
|
+
return {
|
|
64754
|
+
state: "unlocatable",
|
|
64755
|
+
reason: `the quote is ${needle.length} lines, past the ${MAX_QUOTE_LINES}-line bound; quote the few lines the finding is about`
|
|
64756
|
+
};
|
|
64757
|
+
}
|
|
64758
|
+
const haystack = fileText.replace(/\r\n/g, `
|
|
64759
|
+
`).split(`
|
|
64760
|
+
`);
|
|
64761
|
+
if (haystack.length > MAX_LOCATE_FILE_LINES) {
|
|
64762
|
+
return {
|
|
64763
|
+
state: "unlocatable",
|
|
64764
|
+
reason: `the file is ${haystack.length} lines, past the ${MAX_LOCATE_FILE_LINES}-line bound for locating`
|
|
64765
|
+
};
|
|
64766
|
+
}
|
|
64767
|
+
const exact = findRuns(haystack, deindent(needle).map((line) => line.trimEnd()), (window) => deindent(window).map((line) => line.trimEnd()));
|
|
64768
|
+
if (exact.length === 1) {
|
|
64769
|
+
return { state: "derived", method: "exact", line: exact[0] };
|
|
64770
|
+
}
|
|
64771
|
+
if (exact.length > 1) {
|
|
64772
|
+
return ambiguity(exact, " in the file ");
|
|
64773
|
+
}
|
|
64774
|
+
const loose = findRuns(haystack, needle.map(normalise), (window) => window.map(normalise));
|
|
64775
|
+
if (loose.length === 1) {
|
|
64776
|
+
return { state: "derived", method: "whitespace-normalised", line: loose[0] };
|
|
64777
|
+
}
|
|
64778
|
+
if (loose.length > 1) {
|
|
64779
|
+
return ambiguity(loose, " once whitespace is normalised ");
|
|
64780
|
+
}
|
|
64781
|
+
return { state: "unlocatable", reason: "the quote does not appear in the file" };
|
|
64782
|
+
}
|
|
64783
|
+
async function locateFinding(finding, readFile) {
|
|
64784
|
+
const quote = typeof finding.quote === "string" ? finding.quote : "";
|
|
64785
|
+
if (quote.trim() === "") {
|
|
64786
|
+
return;
|
|
64787
|
+
}
|
|
64788
|
+
const reported = typeof finding.line === "number" ? finding.line : null;
|
|
64789
|
+
const file = typeof finding.file === "string" ? finding.file : "";
|
|
64790
|
+
if (file === "") {
|
|
64791
|
+
return { state: "unlocatable", reason: "the finding quotes code but names no file", reported_line: reported };
|
|
64792
|
+
}
|
|
64793
|
+
const text = await readFile(file);
|
|
64794
|
+
if (text === null) {
|
|
64795
|
+
return { state: "unlocatable", reason: `no such file at this round's head: ${file}`, reported_line: reported };
|
|
64796
|
+
}
|
|
64797
|
+
const outcome = locateQuote(text, quote);
|
|
64798
|
+
return outcome.state === "derived" ? { state: "derived", method: outcome.method, line: outcome.line, reported_line: reported } : { state: "unlocatable", reason: outcome.reason, reported_line: reported };
|
|
64799
|
+
}
|
|
64800
|
+
|
|
64801
|
+
// src/review/repair.ts
|
|
64802
|
+
var REPAIRABLE = new Set(["id", "problem"]);
|
|
64803
|
+
function isBlank(value) {
|
|
64804
|
+
return typeof value !== "string" || value.trim() === "";
|
|
64805
|
+
}
|
|
64806
|
+
function mintId(index, taken) {
|
|
64807
|
+
let n = index + 1;
|
|
64808
|
+
let candidate = `F-${String(n).padStart(3, "0")}`;
|
|
64809
|
+
while (taken.has(candidate)) {
|
|
64810
|
+
n += 1;
|
|
64811
|
+
candidate = `F-${String(n).padStart(3, "0")}`;
|
|
64812
|
+
}
|
|
64813
|
+
return candidate;
|
|
64814
|
+
}
|
|
64815
|
+
function acceptRepair(before, after) {
|
|
64816
|
+
if (before.length !== after.length) {
|
|
64817
|
+
throw new Error(`Refusing a repair that changed the number of findings: ${before.length} in, ${after.length} out. A repair may fill fields in; it may never add or drop a finding.`);
|
|
64818
|
+
}
|
|
64819
|
+
for (const [index, finding] of after.entries()) {
|
|
64820
|
+
const had = before[index];
|
|
64821
|
+
const keys = new Set(Object.keys(finding));
|
|
64822
|
+
const illegal = [...keys].filter((key) => !had.has(key) && !REPAIRABLE.has(key));
|
|
64823
|
+
if (illegal.length > 0) {
|
|
64824
|
+
throw new Error(`Refusing a repair that wrote ${illegal.join(", ")} onto finding ${index + 1}: only ${[...REPAIRABLE].join(" and ")} may be filled in. Everything else is a claim the reviewer has to make.`);
|
|
64825
|
+
}
|
|
64826
|
+
const removed = [...had].filter((key) => !keys.has(key));
|
|
64827
|
+
if (removed.length > 0) {
|
|
64828
|
+
throw new Error(`Refusing a repair that removed ${removed.join(", ")} from finding ${index + 1}: a repair may fill a field in; it may never clear one.`);
|
|
64829
|
+
}
|
|
64830
|
+
}
|
|
64831
|
+
}
|
|
64832
|
+
function repairMechanicalOmissions(findings) {
|
|
64833
|
+
const before = findings.map((finding) => new Set(Object.keys(finding)));
|
|
64834
|
+
const taken = new Set(findings.map((finding) => finding.id).filter((id) => !isBlank(id)));
|
|
64835
|
+
const repairs = [];
|
|
64836
|
+
for (const [index, finding] of findings.entries()) {
|
|
64837
|
+
if (isBlank(finding.id)) {
|
|
64838
|
+
const minted = mintId(index, taken);
|
|
64839
|
+
taken.add(minted);
|
|
64840
|
+
finding.id = minted;
|
|
64841
|
+
repairs.push({ finding: minted, field: "id", source: `position ${index + 1} in the report` });
|
|
64842
|
+
}
|
|
64843
|
+
if (isBlank(finding.problem) && !isBlank(finding["title"])) {
|
|
64844
|
+
finding.problem = String(finding["title"]).trim();
|
|
64845
|
+
repairs.push({ finding: finding.id, field: "problem", source: "the finding's own title" });
|
|
64846
|
+
}
|
|
64847
|
+
}
|
|
64848
|
+
acceptRepair(before, findings);
|
|
64849
|
+
return repairs;
|
|
64850
|
+
}
|
|
64851
|
+
|
|
64695
64852
|
// src/review/managed.ts
|
|
64696
64853
|
init_review_notes();
|
|
64697
64854
|
init_types3();
|
|
@@ -64761,6 +64918,9 @@ async function createManagedReviewPackage(input) {
|
|
|
64761
64918
|
source: input.findings,
|
|
64762
64919
|
reviewers
|
|
64763
64920
|
});
|
|
64921
|
+
const repairs = repairMechanicalOmissions(reported);
|
|
64922
|
+
const head = await resolveTargetHead(input);
|
|
64923
|
+
await anchorFindings(reported, input, head);
|
|
64764
64924
|
assignGlobalIds(reported, reviewId);
|
|
64765
64925
|
const merged = mergeVerifications(reported, input.verifications ?? [], {
|
|
64766
64926
|
mode: input.verificationMode ?? DEFAULT_VERIFICATION_MODE
|
|
@@ -64796,7 +64956,6 @@ async function createManagedReviewPackage(input) {
|
|
|
64796
64956
|
externalRetained: scoped.external.length,
|
|
64797
64957
|
...input.refuted === undefined ? {} : { roundDismissed: roundDismissed.length }
|
|
64798
64958
|
});
|
|
64799
|
-
const head = await resolveTargetHead(input);
|
|
64800
64959
|
const manifest = buildManifest2({
|
|
64801
64960
|
input,
|
|
64802
64961
|
target: targetWithHead(input.target, head),
|
|
@@ -64805,7 +64964,8 @@ async function createManagedReviewPackage(input) {
|
|
|
64805
64964
|
flowMatch,
|
|
64806
64965
|
coverage,
|
|
64807
64966
|
at,
|
|
64808
|
-
filterStats
|
|
64967
|
+
filterStats,
|
|
64968
|
+
repairs
|
|
64809
64969
|
});
|
|
64810
64970
|
const validation = await validateManagedReviewManifest(input.cwd, manifest);
|
|
64811
64971
|
if (!validation.valid) {
|
|
@@ -65067,6 +65227,8 @@ function buildManifest2(args) {
|
|
|
65067
65227
|
},
|
|
65068
65228
|
coverage: args.coverage,
|
|
65069
65229
|
filter_stats: args.filterStats,
|
|
65230
|
+
...args.repairs.length === 0 ? {} : { repairs: args.repairs },
|
|
65231
|
+
...args.input.cost === undefined ? {} : { cost: args.input.cost },
|
|
65070
65232
|
...crossFamily === undefined ? {} : { cross_family_review: crossFamily },
|
|
65071
65233
|
createdAt: args.at,
|
|
65072
65234
|
updatedAt: args.at
|
|
@@ -65132,6 +65294,80 @@ function triage(findings, args) {
|
|
|
65132
65294
|
return triaged;
|
|
65133
65295
|
});
|
|
65134
65296
|
}
|
|
65297
|
+
async function anchorFindings(findings, input, head) {
|
|
65298
|
+
const read = input.readTreeFile ?? (head === null ? readFromWorkingTree(input) : async (relative) => {
|
|
65299
|
+
const contained = await containedRelativePath(input.cwd, relative);
|
|
65300
|
+
return contained === null ? null : readFileAtCommit(input.cwd, head, contained);
|
|
65301
|
+
});
|
|
65302
|
+
for (const finding of findings) {
|
|
65303
|
+
const locator = await locateFinding(finding, read);
|
|
65304
|
+
if (locator === undefined) {
|
|
65305
|
+
delete finding.locator;
|
|
65306
|
+
continue;
|
|
65307
|
+
}
|
|
65308
|
+
if (locator.state === "derived") {
|
|
65309
|
+
finding.line = locator.line;
|
|
65310
|
+
finding.locator = {
|
|
65311
|
+
state: "derived",
|
|
65312
|
+
method: locator.method,
|
|
65313
|
+
...locator.reported_line === undefined ? {} : { reported_line: locator.reported_line }
|
|
65314
|
+
};
|
|
65315
|
+
continue;
|
|
65316
|
+
}
|
|
65317
|
+
finding.line = null;
|
|
65318
|
+
finding.locator = {
|
|
65319
|
+
state: "unlocatable",
|
|
65320
|
+
reason: locator.reason,
|
|
65321
|
+
...locator.reported_line === undefined ? {} : { reported_line: locator.reported_line }
|
|
65322
|
+
};
|
|
65323
|
+
}
|
|
65324
|
+
}
|
|
65325
|
+
async function containedRelativePath(cwd, relative) {
|
|
65326
|
+
const root = await realpath8(cwd).catch(() => path153.resolve(cwd));
|
|
65327
|
+
const resolved = path153.resolve(root, relative);
|
|
65328
|
+
const real = await realpath8(resolved).catch(() => null);
|
|
65329
|
+
if (real === null || real !== root && !real.startsWith(`${root}${path153.sep}`)) {
|
|
65330
|
+
return null;
|
|
65331
|
+
}
|
|
65332
|
+
return real;
|
|
65333
|
+
}
|
|
65334
|
+
async function readFileAtCommit(cwd, commit, absolute) {
|
|
65335
|
+
const relative = path153.relative(await realpath8(cwd).catch(() => path153.resolve(cwd)), absolute);
|
|
65336
|
+
try {
|
|
65337
|
+
const proc = Bun.spawn(["git", "show", `${commit}:${relative}`], {
|
|
65338
|
+
cwd,
|
|
65339
|
+
stdout: "pipe",
|
|
65340
|
+
stderr: "ignore",
|
|
65341
|
+
stdin: "ignore"
|
|
65342
|
+
});
|
|
65343
|
+
const [stdout, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
|
65344
|
+
if (exitCode !== 0 || stdout.length > MAX_LOCATE_FILE_BYTES) {
|
|
65345
|
+
return null;
|
|
65346
|
+
}
|
|
65347
|
+
return stdout;
|
|
65348
|
+
} catch {
|
|
65349
|
+
return null;
|
|
65350
|
+
}
|
|
65351
|
+
}
|
|
65352
|
+
function readFromWorkingTree(input) {
|
|
65353
|
+
return async (relative) => {
|
|
65354
|
+
const root = await realpath8(input.cwd).catch(() => path153.resolve(input.cwd));
|
|
65355
|
+
const resolved = path153.resolve(root, relative);
|
|
65356
|
+
const real = await realpath8(resolved).catch(() => null);
|
|
65357
|
+
if (real === null || real !== root && !real.startsWith(`${root}${path153.sep}`)) {
|
|
65358
|
+
return null;
|
|
65359
|
+
}
|
|
65360
|
+
try {
|
|
65361
|
+
const info = await stat16(real);
|
|
65362
|
+
if (!info.isFile() || info.size > MAX_LOCATE_FILE_BYTES) {
|
|
65363
|
+
return null;
|
|
65364
|
+
}
|
|
65365
|
+
return await readFile86(real, "utf8");
|
|
65366
|
+
} catch {
|
|
65367
|
+
return null;
|
|
65368
|
+
}
|
|
65369
|
+
};
|
|
65370
|
+
}
|
|
65135
65371
|
function toContractFinding(finding) {
|
|
65136
65372
|
const record = {
|
|
65137
65373
|
id: finding.id,
|
|
@@ -65149,6 +65385,12 @@ function toContractFinding(finding) {
|
|
|
65149
65385
|
if (finding.line !== undefined) {
|
|
65150
65386
|
record.line = finding.line;
|
|
65151
65387
|
}
|
|
65388
|
+
if (finding.quote !== undefined) {
|
|
65389
|
+
record.quote = finding.quote;
|
|
65390
|
+
}
|
|
65391
|
+
if (finding.locator !== undefined) {
|
|
65392
|
+
record.locator = finding.locator;
|
|
65393
|
+
}
|
|
65152
65394
|
if (finding.symbol !== undefined) {
|
|
65153
65395
|
record.symbol = finding.symbol;
|
|
65154
65396
|
}
|
|
@@ -65801,6 +66043,73 @@ async function loadDocpackSchema(cwd) {
|
|
|
65801
66043
|
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
|
|
65802
66044
|
}
|
|
65803
66045
|
|
|
66046
|
+
// src/review/cost.ts
|
|
66047
|
+
function estimateTokens2(text) {
|
|
66048
|
+
return Math.ceil(text.length / 4);
|
|
66049
|
+
}
|
|
66050
|
+
function grouped(value) {
|
|
66051
|
+
return value.toLocaleString("en-US");
|
|
66052
|
+
}
|
|
66053
|
+
function renderScopeEstimate(scopedDiff, reviewers) {
|
|
66054
|
+
const per = estimateTokens2(scopedDiff);
|
|
66055
|
+
const lines = [
|
|
66056
|
+
`### Estimated cost`,
|
|
66057
|
+
"",
|
|
66058
|
+
`- \u2248 ${grouped(per)} prompt tokens of scoped diff per reviewer (characters \xF7 4 \u2014 an estimate, not a measurement)`
|
|
66059
|
+
];
|
|
66060
|
+
if (reviewers > 0) {
|
|
66061
|
+
lines.push(`- \u2248 ${grouped(per * reviewers)} across ${reviewers} reviewer${reviewers === 1 ? "" : "s"}, before their own prompts, tools and any verifier wave`);
|
|
66062
|
+
} else {
|
|
66063
|
+
lines.push("- reviewer count not supplied, so the fan-out is not multiplied in \u2014 pass `--reviewers a,b` to see the round's total");
|
|
66064
|
+
}
|
|
66065
|
+
return lines.join(`
|
|
66066
|
+
`);
|
|
66067
|
+
}
|
|
66068
|
+
function costFrom(input) {
|
|
66069
|
+
const cost = {};
|
|
66070
|
+
if (typeof input.inputTokens === "number") {
|
|
66071
|
+
cost.input_tokens = input.inputTokens;
|
|
66072
|
+
}
|
|
66073
|
+
if (typeof input.outputTokens === "number") {
|
|
66074
|
+
cost.output_tokens = input.outputTokens;
|
|
66075
|
+
}
|
|
66076
|
+
if (typeof input.spentUsd === "number") {
|
|
66077
|
+
cost.spent_usd = input.spentUsd;
|
|
66078
|
+
}
|
|
66079
|
+
return Object.keys(cost).length === 0 ? undefined : cost;
|
|
66080
|
+
}
|
|
66081
|
+
function renderCostPerFinding(cost, retained) {
|
|
66082
|
+
if (cost === undefined) {
|
|
66083
|
+
return [
|
|
66084
|
+
"cost: not recorded",
|
|
66085
|
+
" Nothing reported what this round used. That is NOT zero: pass --tokens-in/--tokens-out",
|
|
66086
|
+
" or --spent at ingest so the price of a round is a number somebody can weigh."
|
|
66087
|
+
];
|
|
66088
|
+
}
|
|
66089
|
+
const tokens = cost.input_tokens === undefined && cost.output_tokens === undefined ? undefined : (cost.input_tokens ?? 0) + (cost.output_tokens ?? 0);
|
|
66090
|
+
const lines = [];
|
|
66091
|
+
if (tokens !== undefined) {
|
|
66092
|
+
lines.push(`tokens: ${grouped(tokens)}`);
|
|
66093
|
+
}
|
|
66094
|
+
if (cost.spent_usd !== undefined) {
|
|
66095
|
+
lines.push(`spent: ${cost.spent_usd} USD`);
|
|
66096
|
+
}
|
|
66097
|
+
lines.push(`retained findings: ${retained}`);
|
|
66098
|
+
if (retained === 0) {
|
|
66099
|
+
lines.push(" Nothing was retained, so there is no per-finding figure \u2014 only the bill.");
|
|
66100
|
+
return lines;
|
|
66101
|
+
}
|
|
66102
|
+
if (tokens !== undefined) {
|
|
66103
|
+
const per = tokens / retained;
|
|
66104
|
+
lines.push(per > 0 && Math.round(per) === 0 ? "per retained finding: < 1 token" : `per retained finding: ${grouped(Math.round(per))} tokens`);
|
|
66105
|
+
}
|
|
66106
|
+
if (cost.spent_usd !== undefined) {
|
|
66107
|
+
const per = cost.spent_usd / retained;
|
|
66108
|
+
lines.push(per > 0 && Number(per.toFixed(4)) === 0 ? "per retained finding: < 0.0001 USD" : `per retained finding: ${per.toFixed(4)} USD`);
|
|
66109
|
+
}
|
|
66110
|
+
return lines;
|
|
66111
|
+
}
|
|
66112
|
+
|
|
65804
66113
|
// src/review/reviewers.ts
|
|
65805
66114
|
init_project_skills();
|
|
65806
66115
|
import { readFile as readFile87, readdir as readdir29 } from "fs/promises";
|
|
@@ -66977,6 +67286,8 @@ var CREATE_FLAGS = [
|
|
|
66977
67286
|
"--max-findings",
|
|
66978
67287
|
"--spent",
|
|
66979
67288
|
"--spend-ceiling",
|
|
67289
|
+
"--tokens-in",
|
|
67290
|
+
"--tokens-out",
|
|
66980
67291
|
"--parallel",
|
|
66981
67292
|
"--outstanding"
|
|
66982
67293
|
];
|
|
@@ -67017,8 +67328,12 @@ var SCOPE_FLAGS = [
|
|
|
67017
67328
|
"--base",
|
|
67018
67329
|
"--json",
|
|
67019
67330
|
"--scoped-diff",
|
|
67020
|
-
"--append"
|
|
67331
|
+
"--append",
|
|
67332
|
+
"--reviewers"
|
|
67021
67333
|
];
|
|
67334
|
+
function reviewerList(args) {
|
|
67335
|
+
return (optionValue(args, "--reviewers") ?? "").split(",").map((item) => item.trim()).filter(Boolean);
|
|
67336
|
+
}
|
|
67022
67337
|
var FLOOR_FLAGS = ["--ref", "--base", "--diff", "--context", "--json", "--report-only"];
|
|
67023
67338
|
var FLOOR_VALUE_FLAGS = ["--ref", "--base", "--diff", "--context"];
|
|
67024
67339
|
function rejectValuelessOptions(args, valueFlags, usage) {
|
|
@@ -67196,7 +67511,12 @@ async function runCreate(mode, args) {
|
|
|
67196
67511
|
maxFindingsPerReviewer: parseNonNegativeInteger(optionValue(args, "--max-findings"), "--max-findings"),
|
|
67197
67512
|
spend: parseMoney(optionValue(args, "--spent"), "--spent"),
|
|
67198
67513
|
spendCeiling: parseMoney(optionValue(args, "--spend-ceiling"), "--spend-ceiling"),
|
|
67199
|
-
concurrency: parseConcurrency(args)
|
|
67514
|
+
concurrency: parseConcurrency(args),
|
|
67515
|
+
cost: costFrom({
|
|
67516
|
+
inputTokens: parseNonNegativeInteger(optionValue(args, "--tokens-in"), "--tokens-in"),
|
|
67517
|
+
outputTokens: parseNonNegativeInteger(optionValue(args, "--tokens-out"), "--tokens-out"),
|
|
67518
|
+
spentUsd: parseMoney(optionValue(args, "--spent"), "--spent")
|
|
67519
|
+
})
|
|
67200
67520
|
};
|
|
67201
67521
|
const result = await createManagedReviewPackage(input);
|
|
67202
67522
|
console.log(`# managed review: ${result.reviewId}`);
|
|
@@ -67861,6 +68181,8 @@ async function runScope(args) {
|
|
|
67861
68181
|
console.log(renderScopedDiff(scope));
|
|
67862
68182
|
} else {
|
|
67863
68183
|
console.log(renderReviewScopeMarkdown(scope));
|
|
68184
|
+
console.log("");
|
|
68185
|
+
console.log(renderScopeEstimate(renderScopedDiff(scope), reviewerList(args).length));
|
|
67864
68186
|
}
|
|
67865
68187
|
const append = optionValue(args, "--append");
|
|
67866
68188
|
if (append !== undefined) {
|
|
@@ -68116,6 +68438,9 @@ async function runComplete3(args) {
|
|
|
68116
68438
|
console.log(`# managed review complete: ${manifest.reviewId}`);
|
|
68117
68439
|
console.log(`status: ${manifest.status}`);
|
|
68118
68440
|
console.log(`dispositions recorded: ${dispositions.length}`);
|
|
68441
|
+
for (const line of renderCostPerFinding(manifest.cost, manifest.filter_stats?.retained ?? 0)) {
|
|
68442
|
+
console.log(line);
|
|
68443
|
+
}
|
|
68119
68444
|
for (const note of reviewNotes.written) {
|
|
68120
68445
|
console.log(`review-note: ${note.finding} -> ${note.path} (attested by ${note.attestation})`);
|
|
68121
68446
|
}
|
|
@@ -75053,7 +75378,7 @@ init_workspace_service();
|
|
|
75053
75378
|
init_store2();
|
|
75054
75379
|
init_fs();
|
|
75055
75380
|
import { createHash as createHash35, randomUUID as randomUUID17 } from "crypto";
|
|
75056
|
-
import { appendFile as appendFile7, mkdir as mkdir58, open as open2, readFile as readFile94, rename as rename7, rm as rm12, stat as
|
|
75381
|
+
import { appendFile as appendFile7, mkdir as mkdir58, open as open2, readFile as readFile94, rename as rename7, rm as rm12, stat as stat17, writeFile as writeFile50 } from "fs/promises";
|
|
75057
75382
|
import path172 from "path";
|
|
75058
75383
|
|
|
75059
75384
|
// src/sac/policy-experiment.ts
|
|
@@ -75230,7 +75555,7 @@ var ledgerDigestChunkBytes = 64 * 1024;
|
|
|
75230
75555
|
var isMissing = (error) => error instanceof Error && error.code === "ENOENT";
|
|
75231
75556
|
var checkpointHash = (body) => createHash35("sha256").update(JSON.stringify(body), "utf8").digest("hex");
|
|
75232
75557
|
async function ledgerByteLength(ledger) {
|
|
75233
|
-
const value = await
|
|
75558
|
+
const value = await stat17(ledger, { bigint: true });
|
|
75234
75559
|
if (value.size > BigInt(Number.MAX_SAFE_INTEGER))
|
|
75235
75560
|
throw new Error("invalid access receipt ledger: ledger-too-large");
|
|
75236
75561
|
return Number(value.size);
|
|
@@ -76654,7 +76979,7 @@ function buildToolRegistry() {
|
|
|
76654
76979
|
init_fs();
|
|
76655
76980
|
init_contained_read();
|
|
76656
76981
|
import path175 from "path";
|
|
76657
|
-
import { lstat as lstat5, readdir as readdir34, realpath as
|
|
76982
|
+
import { lstat as lstat5, readdir as readdir34, realpath as realpath9, stat as stat18 } from "fs/promises";
|
|
76658
76983
|
var URI_PREFIX = "metaproject://";
|
|
76659
76984
|
function mimeForPath(filePath) {
|
|
76660
76985
|
if (filePath.endsWith(".json") || filePath.endsWith(".jsonl")) {
|
|
@@ -76682,10 +77007,10 @@ async function walkFiles(root, ownerRoot = root, ancestors = new Set) {
|
|
|
76682
77007
|
const ownerInfo = await lstat5(ownerRoot).catch(() => null);
|
|
76683
77008
|
if (!ownerInfo || !ownerInfo.isDirectory() || ownerInfo.isSymbolicLink())
|
|
76684
77009
|
return [];
|
|
76685
|
-
const ownerReal = await
|
|
77010
|
+
const ownerReal = await realpath9(ownerRoot).catch(() => null);
|
|
76686
77011
|
if (!ownerReal)
|
|
76687
77012
|
return [];
|
|
76688
|
-
const canonical = await
|
|
77013
|
+
const canonical = await realpath9(root).catch(() => null);
|
|
76689
77014
|
if (!canonical || !isPathInside(ownerReal, canonical) || ancestors.has(canonical))
|
|
76690
77015
|
return [];
|
|
76691
77016
|
const nextAncestors = new Set(ancestors);
|
|
@@ -76699,8 +77024,8 @@ async function walkFiles(root, ownerRoot = root, ancestors = new Set) {
|
|
|
76699
77024
|
}
|
|
76700
77025
|
for (const entry of entries) {
|
|
76701
77026
|
const full = path175.join(root, entry.name);
|
|
76702
|
-
const info = await
|
|
76703
|
-
if (!info || !isPathInside(ownerReal, await
|
|
77027
|
+
const info = await stat18(full).catch(() => null);
|
|
77028
|
+
if (!info || !isPathInside(ownerReal, await realpath9(full).catch(() => "")))
|
|
76704
77029
|
continue;
|
|
76705
77030
|
if (info.isDirectory()) {
|
|
76706
77031
|
out.push(...await walkFiles(full, ownerRoot, nextAncestors));
|
|
@@ -76812,7 +77137,7 @@ async function readResource(cwd, roots, uri) {
|
|
|
76812
77137
|
if (!resolved) {
|
|
76813
77138
|
throw new Error(`Resource path is outside its root (rejected): ${uri}`);
|
|
76814
77139
|
}
|
|
76815
|
-
const info = await
|
|
77140
|
+
const info = await stat18(resolved.absolute).catch(() => null);
|
|
76816
77141
|
if (!info || !info.isFile()) {
|
|
76817
77142
|
throw new Error(`Resource not found: ${uri}`);
|
|
76818
77143
|
}
|
|
@@ -80137,6 +80462,36 @@ var MAX_BACKGROUND_OUTPUT_BYTES = 2 * 1024 * 1024;
|
|
|
80137
80462
|
var DEFAULT_INITIAL_BUFFER_MS = 500;
|
|
80138
80463
|
var MAX_TRACKED_JOBS = 50;
|
|
80139
80464
|
var TERMINATED_OUTPUT_TAIL_BYTES = 4000;
|
|
80465
|
+
var TASK_OUTPUT_HEAD_BYTES = 24000;
|
|
80466
|
+
var ENV_SHELL_IDLE_MS = "KERYX_SHELL_IDLE_MS";
|
|
80467
|
+
var ENV_SHELL_TIMEOUT_MS_FALLBACK = "KERYX_SHELL_TIMEOUT_MS";
|
|
80468
|
+
var DEFAULT_SHELL_IDLE_MS = 120000;
|
|
80469
|
+
var MIN_TASK_IDLE_TIMEOUT_MS = 1000;
|
|
80470
|
+
var MAX_TASK_IDLE_TIMEOUT_MS = 1800000;
|
|
80471
|
+
function parseEnvMs(raw) {
|
|
80472
|
+
if (raw === undefined || raw.trim().length === 0)
|
|
80473
|
+
return;
|
|
80474
|
+
const n = Number.parseInt(raw.trim(), 10);
|
|
80475
|
+
if (!Number.isFinite(n) || n < 0)
|
|
80476
|
+
return Number.NaN;
|
|
80477
|
+
return n;
|
|
80478
|
+
}
|
|
80479
|
+
function resolveShellIdleMs(env = process.env) {
|
|
80480
|
+
const idle = parseEnvMs(env[ENV_SHELL_IDLE_MS]);
|
|
80481
|
+
if (idle !== undefined) {
|
|
80482
|
+
return Number.isNaN(idle) ? DEFAULT_SHELL_IDLE_MS : idle;
|
|
80483
|
+
}
|
|
80484
|
+
const legacy = parseEnvMs(env[ENV_SHELL_TIMEOUT_MS_FALLBACK]);
|
|
80485
|
+
if (legacy !== undefined) {
|
|
80486
|
+
return Number.isNaN(legacy) ? DEFAULT_SHELL_IDLE_MS : legacy;
|
|
80487
|
+
}
|
|
80488
|
+
return DEFAULT_SHELL_IDLE_MS;
|
|
80489
|
+
}
|
|
80490
|
+
function clampTaskIdleTimeoutMs(ms) {
|
|
80491
|
+
if (!Number.isFinite(ms))
|
|
80492
|
+
return MIN_TASK_IDLE_TIMEOUT_MS;
|
|
80493
|
+
return Math.min(MAX_TASK_IDLE_TIMEOUT_MS, Math.max(MIN_TASK_IDLE_TIMEOUT_MS, Math.trunc(ms)));
|
|
80494
|
+
}
|
|
80140
80495
|
function sleep(ms) {
|
|
80141
80496
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
80142
80497
|
}
|
|
@@ -80214,11 +80569,34 @@ function createJobRegistry(options) {
|
|
|
80214
80569
|
const initialBufferMs = options?.initialBufferMs ?? DEFAULT_INITIAL_BUFFER_MS;
|
|
80215
80570
|
const onEvent = options?.onEvent;
|
|
80216
80571
|
const maxTrackedJobs = options?.maxTrackedJobs ?? MAX_TRACKED_JOBS;
|
|
80572
|
+
const idleMs = options?.idleMs ?? resolveShellIdleMs();
|
|
80217
80573
|
let nextId = 0;
|
|
80218
80574
|
const jobs = new Map;
|
|
80219
80575
|
function runningJobs() {
|
|
80220
80576
|
return [...jobs.values()].filter((j) => j.info.status === "running");
|
|
80221
80577
|
}
|
|
80578
|
+
function backgroundRunningJobs() {
|
|
80579
|
+
return runningJobs().filter((j) => j.info.phase === "background");
|
|
80580
|
+
}
|
|
80581
|
+
function clearIdleTimer(job) {
|
|
80582
|
+
if (job.idleTimer === undefined)
|
|
80583
|
+
return;
|
|
80584
|
+
clearTimeout(job.idleTimer);
|
|
80585
|
+
job.idleTimer = undefined;
|
|
80586
|
+
}
|
|
80587
|
+
function armIdleTimer(job) {
|
|
80588
|
+
clearIdleTimer(job);
|
|
80589
|
+
if (job.info.status !== "running")
|
|
80590
|
+
return;
|
|
80591
|
+
const timeout = job.info.idleTimeoutMs;
|
|
80592
|
+
if (timeout <= 0)
|
|
80593
|
+
return;
|
|
80594
|
+
const timer = setTimeout(() => {
|
|
80595
|
+
requestKill(job, "idle");
|
|
80596
|
+
}, timeout);
|
|
80597
|
+
timer.unref?.();
|
|
80598
|
+
job.idleTimer = timer;
|
|
80599
|
+
}
|
|
80222
80600
|
function evictOldestTerminatedIfOverCap() {
|
|
80223
80601
|
if (jobs.size <= maxTrackedJobs)
|
|
80224
80602
|
return;
|
|
@@ -80239,20 +80617,32 @@ function createJobRegistry(options) {
|
|
|
80239
80617
|
}
|
|
80240
80618
|
function appendOutput(job, chunk, stream) {
|
|
80241
80619
|
job.outputBuffer += chunk;
|
|
80620
|
+
if (job.outputHead.length < TASK_OUTPUT_HEAD_BYTES) {
|
|
80621
|
+
job.outputHead = (job.outputHead + chunk).slice(0, TASK_OUTPUT_HEAD_BYTES);
|
|
80622
|
+
job.info.outputHead = job.outputHead;
|
|
80623
|
+
}
|
|
80624
|
+
armIdleTimer(job);
|
|
80242
80625
|
onEvent?.({ type: "output", jobId: job.info.jobId, chunk, stream });
|
|
80243
80626
|
if (job.outputBuffer.length > MAX_BACKGROUND_OUTPUT_BYTES) {
|
|
80244
80627
|
const dropped = job.outputBuffer.length - MAX_BACKGROUND_OUTPUT_BYTES;
|
|
80245
80628
|
job.outputBuffer = job.outputBuffer.slice(dropped);
|
|
80246
80629
|
job.readCursor = Math.max(0, job.readCursor - dropped);
|
|
80247
|
-
|
|
80248
|
-
job.killRequested = true;
|
|
80249
|
-
terminateJob(job, killGraceMs);
|
|
80250
|
-
}
|
|
80630
|
+
requestKill(job, "output-cap");
|
|
80251
80631
|
}
|
|
80252
80632
|
}
|
|
80633
|
+
function requestKill(job, reason) {
|
|
80634
|
+
if (job.info.status !== "running")
|
|
80635
|
+
return Promise.resolve();
|
|
80636
|
+
if (job.killRequested)
|
|
80637
|
+
return job.exitedPromise;
|
|
80638
|
+
job.killRequested = true;
|
|
80639
|
+
job.pendingKillReason = reason;
|
|
80640
|
+
return terminateJob(job, killGraceMs);
|
|
80641
|
+
}
|
|
80253
80642
|
async function terminateJob(job, graceMs) {
|
|
80254
80643
|
if (job.info.status !== "running")
|
|
80255
80644
|
return;
|
|
80645
|
+
clearIdleTimer(job);
|
|
80256
80646
|
job.killRequested = true;
|
|
80257
80647
|
job.handle.kill("SIGTERM");
|
|
80258
80648
|
const exited = await Promise.race([job.exitedPromise.then(() => true), sleep(graceMs).then(() => false)]);
|
|
@@ -80261,14 +80651,17 @@ function createJobRegistry(options) {
|
|
|
80261
80651
|
}
|
|
80262
80652
|
}
|
|
80263
80653
|
return {
|
|
80264
|
-
async start(command) {
|
|
80265
|
-
const
|
|
80266
|
-
if (
|
|
80267
|
-
const
|
|
80268
|
-
|
|
80269
|
-
|
|
80270
|
-
|
|
80271
|
-
|
|
80654
|
+
async start(command, opts) {
|
|
80655
|
+
const phase = opts?.phase ?? "background";
|
|
80656
|
+
if (phase === "background") {
|
|
80657
|
+
const running = backgroundRunningJobs();
|
|
80658
|
+
if (running.length >= maxConcurrent) {
|
|
80659
|
+
const names = running.map((j) => j.info.command).join(", ");
|
|
80660
|
+
return {
|
|
80661
|
+
ok: false,
|
|
80662
|
+
error: `background task limit reached (${maxConcurrent} running: ${names}); wait for one to finish or kill it with shell_job_kill first`
|
|
80663
|
+
};
|
|
80664
|
+
}
|
|
80272
80665
|
}
|
|
80273
80666
|
let handle;
|
|
80274
80667
|
try {
|
|
@@ -80279,7 +80672,7 @@ function createJobRegistry(options) {
|
|
|
80279
80672
|
error: `command failed to start: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
80280
80673
|
};
|
|
80281
80674
|
}
|
|
80282
|
-
const jobId = `
|
|
80675
|
+
const jobId = `task-${++nextId}-${handle.pid}`;
|
|
80283
80676
|
let resolveExited;
|
|
80284
80677
|
const exitedPromise = new Promise((resolve) => {
|
|
80285
80678
|
resolveExited = resolve;
|
|
@@ -80290,26 +80683,46 @@ function createJobRegistry(options) {
|
|
|
80290
80683
|
pid: handle.pid,
|
|
80291
80684
|
command,
|
|
80292
80685
|
status: "running",
|
|
80686
|
+
phase,
|
|
80687
|
+
idleTimeoutMs: opts?.idleTimeoutMs ?? idleMs,
|
|
80293
80688
|
startedAt: nowIso3()
|
|
80294
80689
|
},
|
|
80295
80690
|
handle,
|
|
80296
80691
|
outputBuffer: "",
|
|
80692
|
+
outputHead: "",
|
|
80297
80693
|
readCursor: 0,
|
|
80298
80694
|
exited: false,
|
|
80299
80695
|
killRequested: false,
|
|
80696
|
+
phaseEmitted: false,
|
|
80300
80697
|
resolveExited,
|
|
80301
80698
|
exitedPromise
|
|
80302
80699
|
};
|
|
80700
|
+
if (opts?.description !== undefined)
|
|
80701
|
+
job.info.description = opts.description;
|
|
80303
80702
|
jobs.set(jobId, job);
|
|
80304
80703
|
evictOldestTerminatedIfOverCap();
|
|
80305
|
-
onEvent?.({
|
|
80704
|
+
onEvent?.({
|
|
80705
|
+
type: "start",
|
|
80706
|
+
jobId,
|
|
80707
|
+
pid: handle.pid,
|
|
80708
|
+
command,
|
|
80709
|
+
startedAt: job.info.startedAt,
|
|
80710
|
+
...opts?.description !== undefined ? { description: opts.description } : {}
|
|
80711
|
+
});
|
|
80712
|
+
if (phase === "background") {
|
|
80713
|
+
job.phaseEmitted = true;
|
|
80714
|
+
onEvent?.({ type: "phase", jobId, phase: "background" });
|
|
80715
|
+
}
|
|
80306
80716
|
handle.onOutput((chunk, stream) => appendOutput(job, chunk, stream));
|
|
80307
80717
|
handle.onExit((info) => {
|
|
80308
80718
|
if (job.exited)
|
|
80309
80719
|
return;
|
|
80310
80720
|
job.exited = true;
|
|
80311
|
-
|
|
80312
|
-
|
|
80721
|
+
clearIdleTimer(job);
|
|
80722
|
+
const terminal = job.killRequested ? "killed" : info.exitCode === 0 ? "completed" : "failed";
|
|
80723
|
+
job.info.status = terminal;
|
|
80724
|
+
if (job.killRequested) {
|
|
80725
|
+
job.info.killReason = job.pendingKillReason ?? "model";
|
|
80313
80726
|
}
|
|
80314
80727
|
job.info.exitCode = info.exitCode;
|
|
80315
80728
|
job.info.endedAt = nowIso3();
|
|
@@ -80317,13 +80730,15 @@ function createJobRegistry(options) {
|
|
|
80317
80730
|
onEvent?.({
|
|
80318
80731
|
type: "exit",
|
|
80319
80732
|
jobId,
|
|
80320
|
-
status:
|
|
80733
|
+
status: terminal,
|
|
80734
|
+
...job.info.killReason !== undefined ? { killReason: job.info.killReason } : {},
|
|
80321
80735
|
exitCode: info.exitCode,
|
|
80322
80736
|
endedAt: job.info.endedAt
|
|
80323
80737
|
});
|
|
80324
80738
|
shrinkTerminatedOutput(job);
|
|
80325
80739
|
});
|
|
80326
|
-
|
|
80740
|
+
armIdleTimer(job);
|
|
80741
|
+
if (phase === "background" && initialBufferMs > 0) {
|
|
80327
80742
|
await sleep(initialBufferMs);
|
|
80328
80743
|
}
|
|
80329
80744
|
const output = job.outputBuffer.slice(job.readCursor);
|
|
@@ -80345,7 +80760,41 @@ function createJobRegistry(options) {
|
|
|
80345
80760
|
job.readCursor = job.outputBuffer.length;
|
|
80346
80761
|
return { ok: true, output };
|
|
80347
80762
|
},
|
|
80348
|
-
async
|
|
80763
|
+
async waitForExit(jobId, ms) {
|
|
80764
|
+
const job = jobs.get(jobId);
|
|
80765
|
+
if (job === undefined)
|
|
80766
|
+
return "unknown";
|
|
80767
|
+
if (job.exited || job.info.status !== "running")
|
|
80768
|
+
return "exited";
|
|
80769
|
+
let timer;
|
|
80770
|
+
const timedOut = await Promise.race([
|
|
80771
|
+
job.exitedPromise.then(() => false),
|
|
80772
|
+
new Promise((resolve) => {
|
|
80773
|
+
timer = setTimeout(() => resolve(true), ms);
|
|
80774
|
+
timer.unref?.();
|
|
80775
|
+
})
|
|
80776
|
+
]);
|
|
80777
|
+
if (timer !== undefined)
|
|
80778
|
+
clearTimeout(timer);
|
|
80779
|
+
return timedOut ? "timeout" : "exited";
|
|
80780
|
+
},
|
|
80781
|
+
promote(jobId) {
|
|
80782
|
+
const job = jobs.get(jobId);
|
|
80783
|
+
if (job === undefined) {
|
|
80784
|
+
return { ok: false, error: `unknown job_id: ${jobId}` };
|
|
80785
|
+
}
|
|
80786
|
+
job.info.phase = "background";
|
|
80787
|
+
if (!job.phaseEmitted) {
|
|
80788
|
+
job.phaseEmitted = true;
|
|
80789
|
+
onEvent?.({ type: "phase", jobId, phase: "background" });
|
|
80790
|
+
}
|
|
80791
|
+
const others = backgroundRunningJobs().filter((j) => j.info.jobId !== jobId);
|
|
80792
|
+
if (job.info.status === "running" && others.length >= maxConcurrent) {
|
|
80793
|
+
return { ok: true, overCap: others.map((j) => j.info.command).join(", ") };
|
|
80794
|
+
}
|
|
80795
|
+
return { ok: true };
|
|
80796
|
+
},
|
|
80797
|
+
async kill(jobId, reason = "model") {
|
|
80349
80798
|
const job = jobs.get(jobId);
|
|
80350
80799
|
if (job === undefined) {
|
|
80351
80800
|
return { ok: false, error: `unknown job_id: ${jobId}` };
|
|
@@ -80353,11 +80802,11 @@ function createJobRegistry(options) {
|
|
|
80353
80802
|
if (job.info.status !== "running") {
|
|
80354
80803
|
return { ok: false, error: `job ${jobId} is not running (status: ${job.info.status})` };
|
|
80355
80804
|
}
|
|
80356
|
-
await
|
|
80805
|
+
await requestKill(job, reason);
|
|
80357
80806
|
return { ok: true };
|
|
80358
80807
|
},
|
|
80359
80808
|
async sweepAll() {
|
|
80360
|
-
await Promise.all(runningJobs().map((job) =>
|
|
80809
|
+
await Promise.all(runningJobs().map((job) => requestKill(job, "session-exit")));
|
|
80361
80810
|
}
|
|
80362
80811
|
};
|
|
80363
80812
|
}
|
|
@@ -80365,7 +80814,7 @@ function shellJobOutputTool(registry) {
|
|
|
80365
80814
|
return {
|
|
80366
80815
|
definition: {
|
|
80367
80816
|
name: "shell_job_output",
|
|
80368
|
-
description: "Return output produced by a
|
|
80817
|
+
description: "Return output produced by a shell task SINCE the previous call for that id \u2014 never the full transcript " + "again. Input: { job_id: string } \u2014 pass the task_id shell_exec returned for a command that outlived its " + "yield (or a background:true job). Poll this instead of re-running shell_exec to check on a long command.",
|
|
80369
80818
|
inputSchema: {
|
|
80370
80819
|
type: "object",
|
|
80371
80820
|
properties: { job_id: { type: "string" } },
|
|
@@ -80391,7 +80840,7 @@ function shellJobKillTool(registry) {
|
|
|
80391
80840
|
return {
|
|
80392
80841
|
definition: {
|
|
80393
80842
|
name: "shell_job_kill",
|
|
80394
|
-
description: "
|
|
80843
|
+
description: "Stop a running shell task \u2014 its entire process group, including any descendant it backgrounded. Input: " + "{ job_id: string } \u2014 the task_id shell_exec returned for a command still running after its yield (or a " + "background:true job). Only tasks in this session's own registry can be targeted.",
|
|
80395
80844
|
inputSchema: {
|
|
80396
80845
|
type: "object",
|
|
80397
80846
|
properties: { job_id: { type: "string" } },
|
|
@@ -80596,6 +81045,37 @@ function resolveShellTimeoutMs(env = process.env) {
|
|
|
80596
81045
|
}
|
|
80597
81046
|
return n;
|
|
80598
81047
|
}
|
|
81048
|
+
var DEFAULT_SHELL_YIELD_MS = 1e4;
|
|
81049
|
+
var ENV_SHELL_YIELD_MS = "KERYX_SHELL_YIELD_MS";
|
|
81050
|
+
function resolveShellYieldMs(env = process.env) {
|
|
81051
|
+
const raw = env[ENV_SHELL_YIELD_MS];
|
|
81052
|
+
if (raw === undefined || raw.trim().length === 0) {
|
|
81053
|
+
return DEFAULT_SHELL_YIELD_MS;
|
|
81054
|
+
}
|
|
81055
|
+
const n = Number.parseInt(raw.trim(), 10);
|
|
81056
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
81057
|
+
return DEFAULT_SHELL_YIELD_MS;
|
|
81058
|
+
}
|
|
81059
|
+
return n;
|
|
81060
|
+
}
|
|
81061
|
+
function boundOutput2(text) {
|
|
81062
|
+
return text.length > MAX_OUTPUT_BYTES3 ? `${text.slice(0, MAX_OUTPUT_BYTES3)}
|
|
81063
|
+
\u2026(truncated)` : text;
|
|
81064
|
+
}
|
|
81065
|
+
function taskHandleResult(args) {
|
|
81066
|
+
return {
|
|
81067
|
+
output: JSON.stringify({
|
|
81068
|
+
task_id: args.taskId,
|
|
81069
|
+
job_id: args.taskId,
|
|
81070
|
+
pid: args.pid,
|
|
81071
|
+
status: "running",
|
|
81072
|
+
output: boundOutput2(args.output),
|
|
81073
|
+
notice: args.notice,
|
|
81074
|
+
...args.overCap !== undefined ? { over_cap: args.overCap } : {}
|
|
81075
|
+
}),
|
|
81076
|
+
isError: false
|
|
81077
|
+
};
|
|
81078
|
+
}
|
|
80599
81079
|
function makeCommandRunner(root) {
|
|
80600
81080
|
return async (command) => {
|
|
80601
81081
|
let netClose = async () => {};
|
|
@@ -80689,14 +81169,19 @@ ${notice}` : notice,
|
|
|
80689
81169
|
}
|
|
80690
81170
|
};
|
|
80691
81171
|
}
|
|
80692
|
-
function shellExecTool(root, run = makeCommandRunner(root), jobRegistry) {
|
|
81172
|
+
function shellExecTool(root, run = makeCommandRunner(root), jobRegistry, options) {
|
|
80693
81173
|
return {
|
|
80694
81174
|
definition: {
|
|
80695
81175
|
name: "shell_exec",
|
|
80696
|
-
description: "Run a shell command in the project root (e.g. `git status`, `bun test`). Requires the user's approval " + "before it runs. Input: { command: string, background?: boolean }.
|
|
81176
|
+
description: "Run a shell command in the project root (e.g. `git status`, `bun test`). Requires the user's approval " + "before it runs. Input: { command: string, background?: boolean, description?: string, " + "idle_timeout_ms?: integer }. EVERY command returns within a bounded wait \u2014 a short one returns its " + "output directly, and one still running when the wait elapses keeps running as a task: you get " + "{task_id, pid, status, output} instead of its final output, and read the rest with " + "shell_job_output(task_id) and stop it with shell_job_kill(task_id). So a slow command never freezes " + "the session and never needs to be guessed at in advance. background:true only SKIPS the wait (use it " + "for a dev server or a watch build you know you will not read inline); it changes nothing else. " + "description is a short label for the task list. idle_timeout_ms is for a command that is deliberately " + "SILENT for a long time (a sleep, a slow poll) \u2014 a task that produces no output for its idle timeout " + "is killed, so raise it rather than lose the command. Combined stdout+stderr is CAPPED at 20,000 bytes " + "from the start of output \u2014 do not use sed/grep/cat/awk here to locate code (they can silently " + "truncate before reaching what you need, and repeating the command returns the same truncated head); " + "use search_code or graph_symbol instead, which are built for that and stay within the cap.",
|
|
80697
81177
|
inputSchema: {
|
|
80698
81178
|
type: "object",
|
|
80699
|
-
properties: {
|
|
81179
|
+
properties: {
|
|
81180
|
+
command: { type: "string" },
|
|
81181
|
+
background: { type: "boolean" },
|
|
81182
|
+
description: { type: "string" },
|
|
81183
|
+
idle_timeout_ms: { type: "integer" }
|
|
81184
|
+
},
|
|
80700
81185
|
required: ["command"],
|
|
80701
81186
|
additionalProperties: false
|
|
80702
81187
|
},
|
|
@@ -80707,20 +81192,66 @@ function shellExecTool(root, run = makeCommandRunner(root), jobRegistry) {
|
|
|
80707
81192
|
if (command.length === 0) {
|
|
80708
81193
|
return { output: "shell_exec requires a non-empty 'command'", isError: true };
|
|
80709
81194
|
}
|
|
80710
|
-
|
|
80711
|
-
|
|
81195
|
+
const background = input.background === true;
|
|
81196
|
+
if (jobRegistry === undefined) {
|
|
81197
|
+
if (background) {
|
|
80712
81198
|
return { output: "shell_exec: background jobs are not available in this session", isError: true };
|
|
80713
81199
|
}
|
|
80714
|
-
|
|
80715
|
-
|
|
80716
|
-
|
|
81200
|
+
return run(command);
|
|
81201
|
+
}
|
|
81202
|
+
const startOpts = { phase: background ? "background" : "foreground" };
|
|
81203
|
+
if (typeof input.description === "string" && input.description.length > 0) {
|
|
81204
|
+
startOpts.description = input.description;
|
|
81205
|
+
}
|
|
81206
|
+
if (typeof input.idle_timeout_ms === "number") {
|
|
81207
|
+
startOpts.idleTimeoutMs = clampTaskIdleTimeoutMs(input.idle_timeout_ms);
|
|
81208
|
+
}
|
|
81209
|
+
const started = await jobRegistry.start(command, startOpts);
|
|
81210
|
+
if (!started.ok) {
|
|
81211
|
+
return { output: started.error, isError: true };
|
|
81212
|
+
}
|
|
81213
|
+
const taskId = started.jobId;
|
|
81214
|
+
if (background) {
|
|
81215
|
+
return taskHandleResult({
|
|
81216
|
+
taskId,
|
|
81217
|
+
pid: started.pid,
|
|
81218
|
+
output: started.output,
|
|
81219
|
+
notice: `running as a background task; read new output with shell_job_output("${taskId}") and stop it with shell_job_kill("${taskId}")`
|
|
81220
|
+
});
|
|
81221
|
+
}
|
|
81222
|
+
const yieldMs = options?.yieldMs ?? resolveShellYieldMs();
|
|
81223
|
+
const outcome = await jobRegistry.waitForExit(taskId, yieldMs);
|
|
81224
|
+
let collected = started.output;
|
|
81225
|
+
const pending = jobRegistry.readOutput(taskId);
|
|
81226
|
+
if (pending.ok)
|
|
81227
|
+
collected += pending.output;
|
|
81228
|
+
if (outcome === "timeout") {
|
|
81229
|
+
const promoted = jobRegistry.promote(taskId);
|
|
81230
|
+
if (!promoted.ok) {
|
|
81231
|
+
return { output: promoted.error, isError: true };
|
|
80717
81232
|
}
|
|
80718
|
-
return {
|
|
80719
|
-
|
|
80720
|
-
|
|
80721
|
-
|
|
81233
|
+
return taskHandleResult({
|
|
81234
|
+
taskId,
|
|
81235
|
+
pid: started.pid,
|
|
81236
|
+
output: collected,
|
|
81237
|
+
notice: `still running after ${yieldMs}ms, so it is now a background task; read new output with shell_job_output("${taskId}") and stop it with shell_job_kill("${taskId}")`,
|
|
81238
|
+
...promoted.overCap !== undefined ? { overCap: promoted.overCap } : {}
|
|
81239
|
+
});
|
|
80722
81240
|
}
|
|
80723
|
-
|
|
81241
|
+
const info = jobRegistry.get(taskId);
|
|
81242
|
+
if (outcome === "unknown" || info === undefined) {
|
|
81243
|
+
const so_far = boundOutput2(collected.trim());
|
|
81244
|
+
const lost = `shell_exec: task ${taskId} is no longer tracked, so its exit status is unknown`;
|
|
81245
|
+
return { output: so_far.length > 0 ? `${so_far}
|
|
81246
|
+
${lost}` : lost, isError: true };
|
|
81247
|
+
}
|
|
81248
|
+
const exitCode = info.exitCode ?? 0;
|
|
81249
|
+
const bounded = boundOutput2((info.outputHead ?? collected).trim());
|
|
81250
|
+
const body = bounded.length > 0 ? bounded : `(no output; exit ${exitCode})`;
|
|
81251
|
+
const idleKilled = info.status === "killed" && info.killReason === "idle";
|
|
81252
|
+
const output = idleKilled ? `${body}
|
|
81253
|
+
shell_exec: no output for ${info.idleTimeoutMs}ms, so the command was killed (raise it for this command with idle_timeout_ms, or change the default with ${ENV_SHELL_IDLE_MS})` : body;
|
|
81254
|
+
return { output, isError: info.status !== "completed" };
|
|
80724
81255
|
}
|
|
80725
81256
|
};
|
|
80726
81257
|
}
|
|
@@ -86652,7 +87183,7 @@ init_store3();
|
|
|
86652
87183
|
init_proposal_lifecycle();
|
|
86653
87184
|
init_workspace_service();
|
|
86654
87185
|
import { randomUUID as randomUUID27 } from "crypto";
|
|
86655
|
-
import { readdir as readdir36, stat as
|
|
87186
|
+
import { readdir as readdir36, stat as stat19 } from "fs/promises";
|
|
86656
87187
|
import path190 from "path";
|
|
86657
87188
|
|
|
86658
87189
|
// src/sac/lifecycle-flag.ts
|
|
@@ -86912,7 +87443,7 @@ async function collectReceiptTargets(cwd, owner) {
|
|
|
86912
87443
|
async function attributeToSession(absolutePath, sessionsNewestFirst) {
|
|
86913
87444
|
let mtimeMs;
|
|
86914
87445
|
try {
|
|
86915
|
-
mtimeMs = (await
|
|
87446
|
+
mtimeMs = (await stat19(absolutePath)).mtimeMs;
|
|
86916
87447
|
} catch {
|
|
86917
87448
|
return;
|
|
86918
87449
|
}
|
|
@@ -94130,9 +94661,14 @@ function openExternalInspector(otui, chrome, options) {
|
|
|
94130
94661
|
var MAX_BACKGROUND_JOB_OUTPUT_CHARS = 20000;
|
|
94131
94662
|
var STATUS_GLYPH4 = {
|
|
94132
94663
|
running: "\u25D0",
|
|
94133
|
-
|
|
94664
|
+
completed: "\u25CF",
|
|
94665
|
+
failed: "\u25B2",
|
|
94134
94666
|
killed: "\u2717"
|
|
94135
94667
|
};
|
|
94668
|
+
function appendBounded(output, chunk) {
|
|
94669
|
+
const combined = output + chunk;
|
|
94670
|
+
return combined.length > MAX_BACKGROUND_JOB_OUTPUT_CHARS ? combined.slice(-MAX_BACKGROUND_JOB_OUTPUT_CHARS) : combined;
|
|
94671
|
+
}
|
|
94136
94672
|
function clip7(s, max) {
|
|
94137
94673
|
if (max <= 0) {
|
|
94138
94674
|
return "";
|
|
@@ -94161,6 +94697,9 @@ function formatJobMeta(entry, now = Date.now()) {
|
|
|
94161
94697
|
["Ended", entry.endedAt ?? "\u2014"],
|
|
94162
94698
|
["Exit code", entry.exitCode !== undefined ? String(entry.exitCode) : "\u2014"]
|
|
94163
94699
|
];
|
|
94700
|
+
if (entry.killReason !== undefined) {
|
|
94701
|
+
rows.push(["Kill reason", entry.killReason]);
|
|
94702
|
+
}
|
|
94164
94703
|
const width = rows.reduce((max, [label]) => Math.max(max, label.length), 0);
|
|
94165
94704
|
return rows.map(([label, value]) => `${label.padEnd(width)} ${value}`).join(`
|
|
94166
94705
|
`);
|
|
@@ -94171,36 +94710,64 @@ function formatJobOutput(entry) {
|
|
|
94171
94710
|
|
|
94172
94711
|
class BackgroundJobStore {
|
|
94173
94712
|
jobs = new Map;
|
|
94713
|
+
pending = new Map;
|
|
94174
94714
|
listeners = new Set;
|
|
94175
94715
|
apply(event) {
|
|
94176
|
-
|
|
94177
|
-
|
|
94178
|
-
|
|
94179
|
-
|
|
94180
|
-
|
|
94181
|
-
|
|
94182
|
-
|
|
94183
|
-
|
|
94184
|
-
|
|
94185
|
-
|
|
94186
|
-
|
|
94187
|
-
|
|
94188
|
-
|
|
94189
|
-
|
|
94190
|
-
|
|
94191
|
-
|
|
94192
|
-
|
|
94193
|
-
|
|
94194
|
-
|
|
94195
|
-
|
|
94196
|
-
|
|
94197
|
-
|
|
94198
|
-
|
|
94199
|
-
|
|
94200
|
-
|
|
94201
|
-
|
|
94716
|
+
switch (event.type) {
|
|
94717
|
+
case "start":
|
|
94718
|
+
this.pending.set(event.jobId, {
|
|
94719
|
+
jobId: event.jobId,
|
|
94720
|
+
command: event.command,
|
|
94721
|
+
pid: event.pid,
|
|
94722
|
+
status: "running",
|
|
94723
|
+
startedAt: event.startedAt,
|
|
94724
|
+
output: ""
|
|
94725
|
+
});
|
|
94726
|
+
return;
|
|
94727
|
+
case "phase": {
|
|
94728
|
+
const entry = this.pending.get(event.jobId);
|
|
94729
|
+
if (entry === undefined) {
|
|
94730
|
+
return;
|
|
94731
|
+
}
|
|
94732
|
+
this.pending.delete(event.jobId);
|
|
94733
|
+
this.jobs.set(event.jobId, entry);
|
|
94734
|
+
this.emit({ id: event.jobId, kind: "start" });
|
|
94735
|
+
return;
|
|
94736
|
+
}
|
|
94737
|
+
case "output": {
|
|
94738
|
+
const hidden = this.pending.get(event.jobId);
|
|
94739
|
+
if (hidden !== undefined) {
|
|
94740
|
+
hidden.output = appendBounded(hidden.output, event.chunk);
|
|
94741
|
+
return;
|
|
94742
|
+
}
|
|
94743
|
+
const current = this.jobs.get(event.jobId);
|
|
94744
|
+
if (current === undefined) {
|
|
94745
|
+
return;
|
|
94746
|
+
}
|
|
94747
|
+
current.output = appendBounded(current.output, event.chunk);
|
|
94748
|
+
this.emit({ id: event.jobId, kind: "output" });
|
|
94749
|
+
return;
|
|
94750
|
+
}
|
|
94751
|
+
case "exit": {
|
|
94752
|
+
if (this.pending.delete(event.jobId)) {
|
|
94753
|
+
return;
|
|
94754
|
+
}
|
|
94755
|
+
const current = this.jobs.get(event.jobId);
|
|
94756
|
+
if (current === undefined) {
|
|
94757
|
+
return;
|
|
94758
|
+
}
|
|
94759
|
+
current.status = event.status;
|
|
94760
|
+
current.endedAt = event.endedAt;
|
|
94761
|
+
if (event.exitCode !== undefined) {
|
|
94762
|
+
current.exitCode = event.exitCode;
|
|
94763
|
+
}
|
|
94764
|
+
if (event.killReason !== undefined) {
|
|
94765
|
+
current.killReason = event.killReason;
|
|
94766
|
+
}
|
|
94767
|
+
this.emit({ id: event.jobId, kind: "exit" });
|
|
94768
|
+
return;
|
|
94769
|
+
}
|
|
94202
94770
|
}
|
|
94203
|
-
this.emit({ id: event.jobId, kind: "exit" });
|
|
94204
94771
|
}
|
|
94205
94772
|
get(jobId) {
|
|
94206
94773
|
return this.jobs.get(jobId);
|
|
@@ -94215,6 +94782,7 @@ class BackgroundJobStore {
|
|
|
94215
94782
|
};
|
|
94216
94783
|
}
|
|
94217
94784
|
removeAll() {
|
|
94785
|
+
this.pending.clear();
|
|
94218
94786
|
if (this.jobs.size === 0) {
|
|
94219
94787
|
return;
|
|
94220
94788
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrciphersmith/keryx",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.108",
|
|
4
4
|
"description": "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -227,7 +227,7 @@ Report done only once all of these hold:
|
|
|
227
227
|
- The measurement was taken at the worst real case — the longest translation in
|
|
228
228
|
the catalog, the narrowest supported host, the state where the sibling is
|
|
229
229
|
absent — not at the convenient one.
|
|
230
|
-
- Every finding names `file
|
|
230
|
+
- Every finding names the `file` of the component that renders the box and `quote`s the code it is about; `line` is derived at ingest from that quote, never counted.
|
|
231
231
|
- Every `blocker` and `major` carries `class_scope` with `sites` and an
|
|
232
232
|
`enumeration_method` naming the search that derived the set of elements sharing
|
|
233
233
|
the sizing shape.
|
|
@@ -1350,10 +1350,10 @@ Each reviewer must return a `REVIEW_RESULT` object matching `.metaproject/skills
|
|
|
1350
1350
|
|
|
1351
1351
|
Before consolidation, validate every reviewer result:
|
|
1352
1352
|
- Required status: `DONE`, `DONE_WITH_CONCERNS`, `NEEDS_CONTEXT`, or `BLOCKED`.
|
|
1353
|
-
- Required finding fields: id, severity, file,
|
|
1353
|
+
- Required finding fields: id, severity, file, `quote`, problem, impact, suggested_fix, evidence, confidence, reviewer. **`quote`, not `line`:** copy the code the finding is about verbatim out of `file` and do not count lines — `keryx review ingest` locates the quote at the round's head and derives `line` from where it matched, so a number reported here is overwritten. A model is poor at counting lines and good at repeating text it has just read; this asks only for the second. Quote enough to be unambiguous: a snippet appearing twice in the file is recorded `unlocatable` rather than anchored to the first hit. A finding about the round rather than a site carries neither.
|
|
1354
1354
|
- Every blocker must include evidence and a concrete suggested fix.
|
|
1355
1355
|
- Findings without evidence are downgraded to `info` or returned to the reviewer for clarification.
|
|
1356
|
-
- Duplicate findings are merged by `dedupe_key` or by `(file,
|
|
1356
|
+
- Duplicate findings are merged by `dedupe_key` or by `(file, quote, problem)`.
|
|
1357
1357
|
- `NEEDS_CONTEXT` triggers one targeted context refill. If still unresolved, keep it as an explicit open question, not as a blocker.
|
|
1358
1358
|
- If a reviewer exceeds `max_findings`, keep blockers/majors first and summarize lower severity findings.
|
|
1359
1359
|
|
|
@@ -74,12 +74,20 @@
|
|
|
74
74
|
]
|
|
75
75
|
},
|
|
76
76
|
"line": {
|
|
77
|
+
"description": "Do not count lines. Quote the code in `quote` instead and leave this null: `keryx review ingest` finds the quote in `file` and writes the line the match started on, overwriting anything reported here. A model is poor at counting lines and good at repeating text it has just read, and this asks it only for the second. Kept in the contract because a finding with nothing to quote — one about the round rather than a site — still has nowhere else to point.",
|
|
77
78
|
"type": [
|
|
78
79
|
"integer",
|
|
79
80
|
"null"
|
|
80
81
|
],
|
|
81
82
|
"minimum": 1
|
|
82
83
|
},
|
|
84
|
+
"quote": {
|
|
85
|
+
"description": "The code this finding is about, copied verbatim out of `file`. A few lines is enough — it is matched against the file at the round's head, exactly first and then with whitespace collapsed. A quote that appears in more than one place is recorded as `unlocatable` rather than anchored to the first hit, so quote enough to be unambiguous.",
|
|
86
|
+
"type": [
|
|
87
|
+
"string",
|
|
88
|
+
"null"
|
|
89
|
+
]
|
|
90
|
+
},
|
|
83
91
|
"problem": {
|
|
84
92
|
"type": "string"
|
|
85
93
|
},
|
|
@@ -100,7 +100,40 @@
|
|
|
100
100
|
"description": "Which repository `file` and `line` are in. Null, the default, means the repository under review. A cross-repo claim names the producer here and pins the ref it was read at in `evidence`; without this field a finding about another service's behaviour could not say where its evidence lived, so the rule that such a claim is `info` unless it has a `file:line` at a pinned ref could not be checked by anything but the reviewer's word. Declared in BOTH this schema and skills/review/review-orchestrator/reviewer-finding.schema.json: this one is additionalProperties:false, so a finding carrying `repo` is rejected here if only the reviewer-side schema knows about it."
|
|
101
101
|
},
|
|
102
102
|
"file": { "type": ["string", "null"] },
|
|
103
|
-
"line": {
|
|
103
|
+
"line": {
|
|
104
|
+
"title": "Where the finding points — DERIVED when the finding quotes code",
|
|
105
|
+
"description": "Reported by the reviewer only when there is nothing to quote. When `quote` is present this field is OUTPUT: `review ingest` finds the quote in `file` at the round's head and writes the line the match started on, overwriting whatever the reviewer said and recording both in `locator`. The reason is the failure it replaces — nothing compared a reported line to anything, so a number that had drifted travelled into findings.json, into the report, and into the disposition recorded against it. On a fix round the file has moved under the finding by construction, which is when the reported number is least trustworthy and most acted upon.",
|
|
106
|
+
"type": ["integer", "null"],
|
|
107
|
+
"minimum": 1
|
|
108
|
+
},
|
|
109
|
+
"quote": {
|
|
110
|
+
"title": "The code this finding is about, copied verbatim out of `file`",
|
|
111
|
+
"description": "What replaces asking the model for a line number: a model is poor at counting lines and good at repeating text it has just read. The quote is matched against `file` at the round's head — exactly first, then with whitespace collapsed, then not at all. A quote that matches in more than one place is `unlocatable` rather than anchored to the first hit, because choosing among them would be a guess wearing a line number. Absent for a finding that is about the round rather than a site.",
|
|
112
|
+
"type": ["string", "null"]
|
|
113
|
+
},
|
|
114
|
+
"locator": {
|
|
115
|
+
"title": "How `line` was arrived at",
|
|
116
|
+
"description": "Present exactly when the finding carries a quote. `derived` means the quote was found and `line` is where; `unlocatable` means it was not, and then `line` is null rather than a number nobody checked. `reported_line` keeps what the reviewer said, so a round can measure how far the claims drifted from the code — which is the only way to know whether deriving them repaired anything or is insurance.",
|
|
117
|
+
"type": "object",
|
|
118
|
+
"additionalProperties": false,
|
|
119
|
+
"required": ["state"],
|
|
120
|
+
"properties": {
|
|
121
|
+
"state": { "type": "string", "enum": ["derived", "unlocatable"] },
|
|
122
|
+
"method": { "type": "string", "enum": ["exact", "whitespace-normalised"] },
|
|
123
|
+
"reason": { "type": ["string", "null"] },
|
|
124
|
+
"reported_line": { "type": ["integer", "null"], "minimum": 1 }
|
|
125
|
+
},
|
|
126
|
+
"allOf": [
|
|
127
|
+
{
|
|
128
|
+
"if": { "properties": { "state": { "const": "derived" } }, "required": ["state"] },
|
|
129
|
+
"then": { "required": ["method"] }
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"if": { "properties": { "state": { "const": "unlocatable" } }, "required": ["state"] },
|
|
133
|
+
"then": { "required": ["reason"] }
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
},
|
|
104
137
|
"symbol": { "type": ["string", "null"] },
|
|
105
138
|
"problem": { "type": "string" },
|
|
106
139
|
"impact": { "type": "string" },
|