@jphutchins/code-review 0.1.0-alpha.21 → 0.1.0-alpha.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +159 -122
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { defineCommand, runMain } from 'citty';
|
|
3
|
-
import { readFileSync, writeFileSync, statSync, readdirSync } from 'fs';
|
|
4
|
-
import { resolve as resolve$1, join, dirname, basename } from 'path';
|
|
3
|
+
import { readFileSync, writeFileSync, statSync, copyFileSync, readdirSync } from 'fs';
|
|
4
|
+
import { resolve as resolve$1, join, dirname, basename, extname } from 'path';
|
|
5
5
|
import { Eta } from 'eta';
|
|
6
6
|
import parseDiff from 'parse-diff';
|
|
7
7
|
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
@@ -178,6 +178,11 @@ ${marker}` : "";
|
|
|
178
178
|
};
|
|
179
179
|
var findingsPointer = (findings, jsonUrl, limit = EMBED_LIMIT) => encodeMarker(findings, jsonUrl, limit);
|
|
180
180
|
var findingPointer = (finding, schemaVersion, jsonUrl, limit = EMBED_LIMIT) => encodeMarker({ schema_version: schemaVersion, findings: [finding] }, jsonUrl, limit);
|
|
181
|
+
var ZERO_SHA = "0000000000000000000000000000000000000000";
|
|
182
|
+
var parseReviewedSha = (body) => {
|
|
183
|
+
const sha = /<!-- reviewed-sha: ([0-9a-fA-F]{40}) -->/.exec(body)?.[1]?.toLowerCase();
|
|
184
|
+
return sha && sha !== ZERO_SHA ? sha : null;
|
|
185
|
+
};
|
|
181
186
|
var parseFindingsMarker = (body) => {
|
|
182
187
|
const match = /<!-- code-review:findings-json;base64 ([A-Za-z0-9+/=]+) -->/.exec(body);
|
|
183
188
|
const b64 = match?.[1];
|
|
@@ -251,8 +256,7 @@ var render = (input) => {
|
|
|
251
256
|
findingsPointer: input.findingsPointer ?? findingsPointer(input.findings, input.jsonUrl),
|
|
252
257
|
reviewUrl: input.reviewUrl ?? null,
|
|
253
258
|
formatTokens: (n) => Number.isFinite(n) && n >= 0 ? n.toLocaleString("en-US") : "\u2014",
|
|
254
|
-
//
|
|
255
|
-
// real tokens spent, we simply have no rates to price them (SPEC §6.2).
|
|
259
|
+
// N/A (never a false $0.00) when no real price map was provided — real tokens, no rates to price them.
|
|
256
260
|
formatCost: (n) => !pricesProvided ? "N/A" : Number.isFinite(n) ? n > 0 && n.toFixed(2) === "0.00" ? "<$0.01" : `$${n.toFixed(2)}` : "\u2014",
|
|
257
261
|
formatDuration: (ms) => {
|
|
258
262
|
if (!Number.isFinite(ms) || ms < 0) return "\u2014";
|
|
@@ -383,6 +387,14 @@ var renderStraysSection = (strays) => {
|
|
|
383
387
|
].join("\n");
|
|
384
388
|
};
|
|
385
389
|
var asRecord = (u) => typeof u === "object" && u !== null && !Array.isArray(u) ? u : null;
|
|
390
|
+
var errMsg = (e) => e instanceof Error ? e.message : String(e);
|
|
391
|
+
var tryParseJson = (text) => {
|
|
392
|
+
try {
|
|
393
|
+
return { ok: true, value: JSON.parse(text) };
|
|
394
|
+
} catch {
|
|
395
|
+
return { ok: false };
|
|
396
|
+
}
|
|
397
|
+
};
|
|
386
398
|
var readFileOrNull = (path) => {
|
|
387
399
|
try {
|
|
388
400
|
return readFileSync(path, "utf-8");
|
|
@@ -683,7 +695,7 @@ var draftState = (draftPath, resolveSchema) => {
|
|
|
683
695
|
if (err instanceof Error && err.code === "ENOENT") {
|
|
684
696
|
return { kind: "missing" };
|
|
685
697
|
}
|
|
686
|
-
return { kind: "unreadable", error:
|
|
698
|
+
return { kind: "unreadable", error: errMsg(err) };
|
|
687
699
|
}
|
|
688
700
|
let parsed;
|
|
689
701
|
try {
|
|
@@ -691,20 +703,20 @@ var draftState = (draftPath, resolveSchema) => {
|
|
|
691
703
|
} catch (err) {
|
|
692
704
|
return {
|
|
693
705
|
kind: "invalid",
|
|
694
|
-
errors: [`not valid JSON: ${
|
|
706
|
+
errors: [`not valid JSON: ${errMsg(err)}`]
|
|
695
707
|
};
|
|
696
708
|
}
|
|
697
709
|
let schemaPath;
|
|
698
710
|
try {
|
|
699
711
|
schemaPath = resolveSchema(parsed);
|
|
700
712
|
} catch (err) {
|
|
701
|
-
return { kind: "invalid", errors: [
|
|
713
|
+
return { kind: "invalid", errors: [errMsg(err)] };
|
|
702
714
|
}
|
|
703
715
|
try {
|
|
704
716
|
const { valid, errors } = validateAgainstSchema(parsed, schemaPath);
|
|
705
717
|
return valid ? { kind: "valid" } : { kind: "invalid", errors };
|
|
706
718
|
} catch (err) {
|
|
707
|
-
return { kind: "invalid", errors: [
|
|
719
|
+
return { kind: "invalid", errors: [errMsg(err)] };
|
|
708
720
|
}
|
|
709
721
|
};
|
|
710
722
|
var readNudges = (counterPath) => {
|
|
@@ -805,6 +817,10 @@ var writesToDraft = (toolName, toolInput, draftPath) => {
|
|
|
805
817
|
};
|
|
806
818
|
var singleWriterMessage = (draftPath) => `Only the main agent may write ${draftPath}. When a subagent writes it too, the concurrent writers clobber each other and the review comes out empty. Do NOT write, edit, or redirect into ${draftPath} \u2014 instead, return the findings you discovered in your reply (the field names are in the schema); the main agent collects every subagent's reported findings and writes the draft itself.`;
|
|
807
819
|
var seedMarkerPath = (draftPath) => `${draftPath}.seed`;
|
|
820
|
+
var lastValidPath = (draftPath) => {
|
|
821
|
+
const ext = extname(draftPath);
|
|
822
|
+
return join(dirname(draftPath), `${basename(draftPath, ext)}.last-valid${ext}`);
|
|
823
|
+
};
|
|
808
824
|
var mainHasWrittenDraft = (draftMtimeMs, seedMarkerMtimeMs) => draftMtimeMs !== null && (seedMarkerMtimeMs === null || draftMtimeMs > seedMarkerMtimeMs);
|
|
809
825
|
var spawnFloorMessage = (draftPath) => `Write your own first-pass findings to ${draftPath} before spawning subagents \u2014 a review must never depend on subagents alone, and a pre-seeded draft does not count until you have revised it yourself this run. Write ${draftPath} from what you have read so far (preliminary findings are fine), run \`code-review validate ${draftPath} --explain\` until it passes, then fan out; your subagents run in the background, so keep refining the draft as their reports arrive.`;
|
|
810
826
|
var forceBackgroundSpawn = (toolInput) => ({
|
|
@@ -821,6 +837,10 @@ var denyPreTool = (reason) => ({
|
|
|
821
837
|
permissionDecisionReason: reason
|
|
822
838
|
}
|
|
823
839
|
});
|
|
840
|
+
var isSubagentHookInput = (input) => {
|
|
841
|
+
const agentId = asRecord(input)?.["agent_id"];
|
|
842
|
+
return typeof agentId === "string" && agentId.length > 0;
|
|
843
|
+
};
|
|
824
844
|
var evaluateBudgetHook = (input, params) => {
|
|
825
845
|
const rec = asRecord(input);
|
|
826
846
|
const inputs = {
|
|
@@ -831,8 +851,7 @@ var evaluateBudgetHook = (input, params) => {
|
|
|
831
851
|
reserve: params.reserve
|
|
832
852
|
};
|
|
833
853
|
const phase = decideBudget(inputs);
|
|
834
|
-
const
|
|
835
|
-
const isSubagent = typeof agentId === "string" && agentId.length > 0;
|
|
854
|
+
const isSubagent = isSubagentHookInput(input);
|
|
836
855
|
switch (rec?.["hook_event_name"]) {
|
|
837
856
|
case "PostToolBatch":
|
|
838
857
|
return phase.kind === "ok" ? {} : {
|
|
@@ -1123,7 +1142,7 @@ var loadTestReport = (path) => {
|
|
|
1123
1142
|
raw = JSON.parse(readFileSync(path, "utf-8"));
|
|
1124
1143
|
} catch (err) {
|
|
1125
1144
|
process.stderr.write(
|
|
1126
|
-
`Warning: could not read test report at ${path}: ${
|
|
1145
|
+
`Warning: could not read test report at ${path}: ${errMsg(err)} \u2014 omitting test panel
|
|
1127
1146
|
`
|
|
1128
1147
|
);
|
|
1129
1148
|
return void 0;
|
|
@@ -1139,12 +1158,9 @@ var loadTestReport = (path) => {
|
|
|
1139
1158
|
return decoded.right;
|
|
1140
1159
|
};
|
|
1141
1160
|
var parseHtmlUrl = (raw) => {
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
} catch {
|
|
1146
|
-
return void 0;
|
|
1147
|
-
}
|
|
1161
|
+
const parsed = tryParseJson(raw);
|
|
1162
|
+
const htmlUrl = parsed.ok ? asRecord(parsed.value)?.["html_url"] : void 0;
|
|
1163
|
+
return typeof htmlUrl === "string" ? htmlUrl : void 0;
|
|
1148
1164
|
};
|
|
1149
1165
|
var commentPayload = (c) => ({
|
|
1150
1166
|
path: c.path,
|
|
@@ -1168,7 +1184,7 @@ var postInlineReview = async (repo, prNumber, headSha, comments, inDiff, stickyU
|
|
|
1168
1184
|
} catch (err) {
|
|
1169
1185
|
if (comments.length === 0) throw err;
|
|
1170
1186
|
process.stderr.write(
|
|
1171
|
-
`Warning: the batched inline review on PR #${String(prNumber)} was rejected (${
|
|
1187
|
+
`Warning: the batched inline review on PR #${String(prNumber)} was rejected (${errMsg(err)}) \u2014 posting the review body-only, then each comment individually to keep the ones GitHub accepts (issue #57)
|
|
1172
1188
|
`
|
|
1173
1189
|
);
|
|
1174
1190
|
const url = parseHtmlUrl(await ghApi(reviewsEndpoint, reviewBody(false)));
|
|
@@ -1183,7 +1199,7 @@ var postInlineReview = async (repo, prNumber, headSha, comments, inDiff, stickyU
|
|
|
1183
1199
|
const finding = inDiff[i];
|
|
1184
1200
|
if (finding) unposted.push(finding);
|
|
1185
1201
|
process.stderr.write(
|
|
1186
|
-
`Warning: inline comment on ${c.path}:${String(c.line)} rejected (${
|
|
1202
|
+
`Warning: inline comment on ${c.path}:${String(c.line)} rejected (${errMsg(e)}) \u2014 surfacing that finding in the sticky instead (issue #57)
|
|
1187
1203
|
`
|
|
1188
1204
|
);
|
|
1189
1205
|
}
|
|
@@ -1210,12 +1226,11 @@ var findBotComment = async (repo, prNumber, botLogin, marker, ghApi) => {
|
|
|
1210
1226
|
return { id: parsed.id, body: parsed.body };
|
|
1211
1227
|
};
|
|
1212
1228
|
var parseCommentRef = (raw) => {
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
}
|
|
1229
|
+
const parsed = tryParseJson(raw);
|
|
1230
|
+
const rec = parsed.ok ? asRecord(parsed.value) : null;
|
|
1231
|
+
const id = rec?.["id"];
|
|
1232
|
+
const html_url = rec?.["html_url"];
|
|
1233
|
+
return typeof id === "number" && typeof html_url === "string" ? { id, html_url } : null;
|
|
1219
1234
|
};
|
|
1220
1235
|
var patchComment = async (repo, commentId, body, ghApi) => {
|
|
1221
1236
|
const stdout = await ghApi(
|
|
@@ -1273,7 +1288,7 @@ var dismissReviews = async (repo, prNumber, ids, ghApi) => {
|
|
|
1273
1288
|
);
|
|
1274
1289
|
} catch (err) {
|
|
1275
1290
|
process.stderr.write(
|
|
1276
|
-
`Warning: failed to dismiss prior review #${String(id)} on PR #${String(prNumber)}: ${
|
|
1291
|
+
`Warning: failed to dismiss prior review #${String(id)} on PR #${String(prNumber)}: ${errMsg(err)}
|
|
1277
1292
|
`
|
|
1278
1293
|
);
|
|
1279
1294
|
}
|
|
@@ -1325,7 +1340,7 @@ var listPriorBotCommentIds = async (repo, prNumber, botLogin, ghApi) => {
|
|
|
1325
1340
|
]);
|
|
1326
1341
|
} catch (err) {
|
|
1327
1342
|
process.stderr.write(
|
|
1328
|
-
`Warning: could not list review threads to minimize stale comments on PR #${String(prNumber)}: ${
|
|
1343
|
+
`Warning: could not list review threads to minimize stale comments on PR #${String(prNumber)}: ${errMsg(err)}
|
|
1329
1344
|
`
|
|
1330
1345
|
);
|
|
1331
1346
|
return [];
|
|
@@ -1347,7 +1362,7 @@ var minimizeComments = async (prNumber, ids, ghApi) => {
|
|
|
1347
1362
|
minimized += 1;
|
|
1348
1363
|
} catch (err) {
|
|
1349
1364
|
process.stderr.write(
|
|
1350
|
-
`Warning: failed to minimize a stale review comment on PR #${String(prNumber)}: ${
|
|
1365
|
+
`Warning: failed to minimize a stale review comment on PR #${String(prNumber)}: ${errMsg(err)}
|
|
1351
1366
|
`
|
|
1352
1367
|
);
|
|
1353
1368
|
}
|
|
@@ -1558,7 +1573,7 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1558
1573
|
);
|
|
1559
1574
|
} catch (err) {
|
|
1560
1575
|
process.stderr.write(
|
|
1561
|
-
`Warning: failed to update the sticky summary after the review: ${
|
|
1576
|
+
`Warning: failed to update the sticky summary after the review: ${errMsg(err)}
|
|
1562
1577
|
`
|
|
1563
1578
|
);
|
|
1564
1579
|
}
|
|
@@ -1648,7 +1663,7 @@ var downloadFailingJobLogs = async (repo, runId, outDir, ghApi) => {
|
|
|
1648
1663
|
writeFileSync(join(outDir, `job_${String(job.id)}.log`), log);
|
|
1649
1664
|
} catch (err) {
|
|
1650
1665
|
process.stderr.write(
|
|
1651
|
-
`Warning: failed to download logs for job ${String(job.id)}: ${
|
|
1666
|
+
`Warning: failed to download logs for job ${String(job.id)}: ${errMsg(err)} \u2014 continuing with the logs retrieved so far
|
|
1652
1667
|
`
|
|
1653
1668
|
);
|
|
1654
1669
|
}
|
|
@@ -1754,13 +1769,6 @@ var gateCandidate = (kind, rawCandidate) => {
|
|
|
1754
1769
|
const resolution = resolve(kind, candidate);
|
|
1755
1770
|
return resolution.kind === "ok" ? { version: resolution.version, candidate } : null;
|
|
1756
1771
|
};
|
|
1757
|
-
var tryParseJson = (text) => {
|
|
1758
|
-
try {
|
|
1759
|
-
return { ok: true, value: JSON.parse(text) };
|
|
1760
|
-
} catch {
|
|
1761
|
-
return { ok: false };
|
|
1762
|
-
}
|
|
1763
|
-
};
|
|
1764
1772
|
var candidateFromJsonText = (kind, text) => {
|
|
1765
1773
|
if (text === null) return null;
|
|
1766
1774
|
const parsed = tryParseJson(text);
|
|
@@ -1789,6 +1797,10 @@ var ladderFailureDiagnostics = (input) => {
|
|
|
1789
1797
|
lines.push(
|
|
1790
1798
|
input.agentFilePath === void 0 ? "agent-file rung: no --agent-file given" : `agent-file rung: ${input.agentFilePath} did not validate (or was unreadable)`
|
|
1791
1799
|
);
|
|
1800
|
+
if (input.agentFileFallbackPath !== void 0)
|
|
1801
|
+
lines.push(
|
|
1802
|
+
`last-valid rung: ${input.agentFileFallbackPath} did not validate (or was absent)`
|
|
1803
|
+
);
|
|
1792
1804
|
}
|
|
1793
1805
|
lines.push(
|
|
1794
1806
|
isNullish(native.structuredOutput) ? "structured_output rung: absent (null) \u2014 the CLI's --json-schema likely did not enforce" : "structured_output rung: present but did not validate against the schema"
|
|
@@ -1815,13 +1827,16 @@ var okOutcome = (gated) => ({
|
|
|
1815
1827
|
});
|
|
1816
1828
|
var extractStructured = (input) => {
|
|
1817
1829
|
const native = parseNativeForExtraction(input.native);
|
|
1830
|
+
if (input.kind === "findings") {
|
|
1831
|
+
for (const path of [input.agentFilePath, input.agentFileFallbackPath]) {
|
|
1832
|
+
if (path === void 0) continue;
|
|
1833
|
+
const fromFile = candidateFromJsonText(input.kind, readFileOrNull(path));
|
|
1834
|
+
if (fromFile) return okOutcome(fromFile);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1818
1837
|
if (isErrorEnvelope(native)) {
|
|
1819
1838
|
return { kind: "error-envelope", detail: describeErrorEnvelope(native) };
|
|
1820
1839
|
}
|
|
1821
|
-
if (input.kind === "findings" && input.agentFilePath !== void 0) {
|
|
1822
|
-
const fromFile = candidateFromJsonText(input.kind, readFileOrNull(input.agentFilePath));
|
|
1823
|
-
if (fromFile) return okOutcome(fromFile);
|
|
1824
|
-
}
|
|
1825
1840
|
if (native.structuredOutput !== void 0) {
|
|
1826
1841
|
const fromStructured = gateCandidate(input.kind, native.structuredOutput);
|
|
1827
1842
|
if (fromStructured) return okOutcome(fromStructured);
|
|
@@ -1841,9 +1856,10 @@ var extractStructured = (input) => {
|
|
|
1841
1856
|
};
|
|
1842
1857
|
}
|
|
1843
1858
|
}
|
|
1859
|
+
const fallbackRung = input.agentFileFallbackPath ? ", last-valid snapshot" : "";
|
|
1844
1860
|
return {
|
|
1845
1861
|
kind: "none",
|
|
1846
|
-
detail: `no --agent-file, structured_output, JSON result, or fenced block validated against the ${input.kind} schema`
|
|
1862
|
+
detail: `no --agent-file${fallbackRung}, structured_output, JSON result, or fenced block validated against the ${input.kind} schema`
|
|
1847
1863
|
};
|
|
1848
1864
|
};
|
|
1849
1865
|
|
|
@@ -1880,8 +1896,13 @@ var mapModelUsage = (modelUsage) => Object.entries(modelUsage).map(([model, entr
|
|
|
1880
1896
|
...entry.cacheReadInputTokens !== void 0 ? { cache_read_tokens: entry.cacheReadInputTokens } : {},
|
|
1881
1897
|
...entry.cacheCreationInputTokens !== void 0 ? { cache_write_tokens: entry.cacheCreationInputTokens } : {}
|
|
1882
1898
|
}));
|
|
1883
|
-
var findingsOutcome = (native, agentFilePath) => {
|
|
1884
|
-
const ladder = extractStructured({
|
|
1899
|
+
var findingsOutcome = (native, agentFilePath, agentFileFallbackPath) => {
|
|
1900
|
+
const ladder = extractStructured({
|
|
1901
|
+
kind: "findings",
|
|
1902
|
+
native,
|
|
1903
|
+
agentFilePath,
|
|
1904
|
+
agentFileFallbackPath
|
|
1905
|
+
});
|
|
1885
1906
|
if (ladder.kind !== "ok")
|
|
1886
1907
|
return { kind: "telemetry-only", reason: describeLadderFailure(ladder) };
|
|
1887
1908
|
const resolution = resolve("findings", ladder.candidate);
|
|
@@ -1923,8 +1944,8 @@ var nativeTelemetry = (native, meta) => resolveTelemetry(
|
|
|
1923
1944
|
meta
|
|
1924
1945
|
);
|
|
1925
1946
|
var absentTelemetry = (meta) => resolveTelemetry({ models: [], turns: 0, durationMs: 0, vendorCostUsd: null }, meta);
|
|
1926
|
-
var buildEnvelope = (telemetry, native, agentFilePath) => {
|
|
1927
|
-
const outcome = findingsOutcome(native, agentFilePath);
|
|
1947
|
+
var buildEnvelope = (telemetry, native, agentFilePath, agentFileFallbackPath) => {
|
|
1948
|
+
const outcome = findingsOutcome(native, agentFilePath, agentFileFallbackPath);
|
|
1928
1949
|
switch (outcome.kind) {
|
|
1929
1950
|
case "ok":
|
|
1930
1951
|
return { schema_version: outcome.version, findings: outcome.findings, ...telemetry };
|
|
@@ -1943,11 +1964,25 @@ var adapt = (adapterName, native, agentFilePath, meta = {}) => {
|
|
|
1943
1964
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- exhaustive by design; AdapterName grows (e.g. "opencode") without collapsing this switch to an if
|
|
1944
1965
|
case "claude-code": {
|
|
1945
1966
|
if (native === void 0 || native === null)
|
|
1946
|
-
return right(
|
|
1967
|
+
return right(
|
|
1968
|
+
buildEnvelope(
|
|
1969
|
+
absentTelemetry(meta),
|
|
1970
|
+
void 0,
|
|
1971
|
+
agentFilePath,
|
|
1972
|
+
meta.agentFileFallbackPath
|
|
1973
|
+
)
|
|
1974
|
+
);
|
|
1947
1975
|
const decoded = ClaudeCodeEnvelopeCodec.decode(native);
|
|
1948
1976
|
if (decoded._tag === "Left")
|
|
1949
1977
|
return left("native envelope does not match the Claude Code output shape");
|
|
1950
|
-
return right(
|
|
1978
|
+
return right(
|
|
1979
|
+
buildEnvelope(
|
|
1980
|
+
nativeTelemetry(decoded.right, meta),
|
|
1981
|
+
native,
|
|
1982
|
+
agentFilePath,
|
|
1983
|
+
meta.agentFileFallbackPath
|
|
1984
|
+
)
|
|
1985
|
+
);
|
|
1951
1986
|
}
|
|
1952
1987
|
}
|
|
1953
1988
|
};
|
|
@@ -1971,8 +2006,7 @@ var readJSON = (path) => {
|
|
|
1971
2006
|
try {
|
|
1972
2007
|
return JSON.parse(readFileSync(resolve$1(path), "utf-8"));
|
|
1973
2008
|
} catch (err) {
|
|
1974
|
-
fail(`Cannot read ${path}: ${
|
|
1975
|
-
throw new Error("unreachable", { cause: err });
|
|
2009
|
+
return fail(`Cannot read ${path}: ${errMsg(err)}`);
|
|
1976
2010
|
}
|
|
1977
2011
|
};
|
|
1978
2012
|
var fail = (msg) => {
|
|
@@ -1985,19 +2019,19 @@ var readJSONOrAbsent = (path) => {
|
|
|
1985
2019
|
try {
|
|
1986
2020
|
return { text: readFileSync(resolve$1(path), "utf-8") };
|
|
1987
2021
|
} catch (err) {
|
|
1988
|
-
return { error:
|
|
2022
|
+
return { error: errMsg(err) };
|
|
1989
2023
|
}
|
|
1990
2024
|
})();
|
|
1991
2025
|
if ("error" in read) {
|
|
1992
2026
|
process.stderr.write(
|
|
1993
|
-
`code-review: native envelope ${path} could not be read (${read.error}) \u2014 proceeding with no native telemetry
|
|
2027
|
+
`code-review: native envelope ${path} could not be read (${read.error}) \u2014 proceeding with no native telemetry
|
|
1994
2028
|
`
|
|
1995
2029
|
);
|
|
1996
2030
|
return void 0;
|
|
1997
2031
|
}
|
|
1998
2032
|
if (read.text.trim() === "") {
|
|
1999
2033
|
process.stderr.write(
|
|
2000
|
-
`code-review: native envelope ${path} is empty \u2014 proceeding with no native telemetry
|
|
2034
|
+
`code-review: native envelope ${path} is empty \u2014 proceeding with no native telemetry
|
|
2001
2035
|
`
|
|
2002
2036
|
);
|
|
2003
2037
|
return void 0;
|
|
@@ -2006,7 +2040,7 @@ var readJSONOrAbsent = (path) => {
|
|
|
2006
2040
|
return JSON.parse(read.text);
|
|
2007
2041
|
} catch (err) {
|
|
2008
2042
|
process.stderr.write(
|
|
2009
|
-
`code-review: native envelope ${path} is not valid JSON (${
|
|
2043
|
+
`code-review: native envelope ${path} is not valid JSON (${errMsg(err)}) \u2014 proceeding with no native telemetry
|
|
2010
2044
|
`
|
|
2011
2045
|
);
|
|
2012
2046
|
return void 0;
|
|
@@ -2022,34 +2056,29 @@ var readStdinJSON = () => {
|
|
|
2022
2056
|
}
|
|
2023
2057
|
})();
|
|
2024
2058
|
if (raw.trim() === "") return null;
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
} catch {
|
|
2028
|
-
return null;
|
|
2029
|
-
}
|
|
2059
|
+
const parsed = tryParseJson(raw);
|
|
2060
|
+
return parsed.ok ? parsed.value : null;
|
|
2030
2061
|
};
|
|
2031
2062
|
var decode = (either, label) => {
|
|
2032
2063
|
try {
|
|
2033
2064
|
return unsafeUnwrap(either);
|
|
2034
2065
|
} catch {
|
|
2035
|
-
fail(`${label} does not match expected shape`);
|
|
2066
|
+
return fail(`${label} does not match expected shape`);
|
|
2036
2067
|
}
|
|
2037
|
-
throw new Error("unreachable");
|
|
2038
2068
|
};
|
|
2039
2069
|
var unwrapAdapt = (either) => {
|
|
2040
2070
|
try {
|
|
2041
2071
|
if (either._tag === "Left") throw new Error(either.left);
|
|
2042
2072
|
return either.right;
|
|
2043
2073
|
} catch (err) {
|
|
2044
|
-
fail(
|
|
2074
|
+
return fail(errMsg(err));
|
|
2045
2075
|
}
|
|
2046
|
-
throw new Error("unreachable");
|
|
2047
2076
|
};
|
|
2048
2077
|
var transcriptFallbackFrom = (path) => {
|
|
2049
2078
|
const tree = readTranscriptTree(resolve$1(path));
|
|
2050
2079
|
if (tree.missing)
|
|
2051
2080
|
process.stderr.write(
|
|
2052
|
-
`code-review adapt: transcript ${path} is unreadable \u2014 no telemetry fallback
|
|
2081
|
+
`code-review adapt: transcript ${path} is unreadable \u2014 no telemetry fallback
|
|
2053
2082
|
`
|
|
2054
2083
|
);
|
|
2055
2084
|
const usage = sumTranscriptUsage(tree.entries);
|
|
@@ -2192,7 +2221,7 @@ var costCmd = defineCommand({
|
|
|
2192
2221
|
var checkCostCmd = defineCommand({
|
|
2193
2222
|
meta: {
|
|
2194
2223
|
name: "check-cost",
|
|
2195
|
-
description: "Sum real USD spend from a
|
|
2224
|
+
description: "Sum real USD spend from a Claude Code transcript tree (main + subagents) against a price map"
|
|
2196
2225
|
},
|
|
2197
2226
|
args: {
|
|
2198
2227
|
transcript: {
|
|
@@ -2209,7 +2238,7 @@ var checkCostCmd = defineCommand({
|
|
|
2209
2238
|
const tree = readTranscriptTree(resolve$1(args.transcript));
|
|
2210
2239
|
if (tree.missing) {
|
|
2211
2240
|
process.stderr.write(
|
|
2212
|
-
`code-review check-cost: transcript ${args.transcript} is unreadable \u2014 reporting zero spend
|
|
2241
|
+
`code-review check-cost: transcript ${args.transcript} is unreadable \u2014 reporting zero spend
|
|
2213
2242
|
`
|
|
2214
2243
|
);
|
|
2215
2244
|
}
|
|
@@ -2257,10 +2286,21 @@ var transcriptPathOf = (input) => {
|
|
|
2257
2286
|
const tp = (typeof input === "object" && input !== null ? input : {})["transcript_path"];
|
|
2258
2287
|
return typeof tp === "string" ? tp : void 0;
|
|
2259
2288
|
};
|
|
2289
|
+
var snapshotIfValid = (draftPath) => {
|
|
2290
|
+
try {
|
|
2291
|
+
if (extractStructured({ kind: "findings", native: void 0, agentFilePath: draftPath }).kind === "ok")
|
|
2292
|
+
copyFileSync(draftPath, lastValidPath(draftPath));
|
|
2293
|
+
} catch (err) {
|
|
2294
|
+
process.stderr.write(
|
|
2295
|
+
`code-review: could not snapshot the last-valid draft (${errMsg(err)}) \u2014 any prior snapshot is unchanged
|
|
2296
|
+
`
|
|
2297
|
+
);
|
|
2298
|
+
}
|
|
2299
|
+
};
|
|
2260
2300
|
var budgetHookCmd = defineCommand({
|
|
2261
2301
|
meta: {
|
|
2262
2302
|
name: "budget-hook",
|
|
2263
|
-
description: "Self-dispatching Claude Code
|
|
2303
|
+
description: "Self-dispatching Claude Code budget hook: on PostToolBatch steer the agent to converge as spend/wall-clock nears the budget; on PreToolUse deny budget-burning tools under the hard reserve, gate subagent spawns until the main agent has drafted, and run permitted spawns in the background. Reads the hook payload on stdin; degrades to a no-op on error."
|
|
2264
2304
|
},
|
|
2265
2305
|
args: {
|
|
2266
2306
|
draft: {
|
|
@@ -2329,13 +2369,13 @@ var budgetHookCmd = defineCommand({
|
|
|
2329
2369
|
mtimeMsOf(seedMarkerPath(draftPath))
|
|
2330
2370
|
)
|
|
2331
2371
|
});
|
|
2372
|
+
if (asRecord(input)?.["hook_event_name"] === "PostToolBatch" && !isSubagentHookInput(input))
|
|
2373
|
+
snapshotIfValid(draftPath);
|
|
2332
2374
|
process.stdout.write(`${JSON.stringify(output)}
|
|
2333
2375
|
`);
|
|
2334
2376
|
} catch (err) {
|
|
2335
|
-
process.stderr.write(
|
|
2336
|
-
|
|
2337
|
-
`
|
|
2338
|
-
);
|
|
2377
|
+
process.stderr.write(`code-review budget-hook: degrading to no-op \u2014 ${errMsg(err)}
|
|
2378
|
+
`);
|
|
2339
2379
|
process.stdout.write("{}\n");
|
|
2340
2380
|
}
|
|
2341
2381
|
}
|
|
@@ -2343,7 +2383,7 @@ var budgetHookCmd = defineCommand({
|
|
|
2343
2383
|
var printSettingsCmd = defineCommand({
|
|
2344
2384
|
meta: {
|
|
2345
2385
|
name: "print-settings",
|
|
2346
|
-
description: "Emit
|
|
2386
|
+
description: "Emit one Claude Code --settings JSON composing the Stop deliverable gate and the budget hooks (PreToolUse convergence + PostToolBatch steer) from one self-dispatching command"
|
|
2347
2387
|
},
|
|
2348
2388
|
args: {
|
|
2349
2389
|
draft: {
|
|
@@ -2429,7 +2469,7 @@ var printSettingsCmd = defineCommand({
|
|
|
2429
2469
|
var deadlineCmd = defineCommand({
|
|
2430
2470
|
meta: {
|
|
2431
2471
|
name: "deadline",
|
|
2432
|
-
description: "Print the run's absolute deadline as Unix epoch seconds (now + --wall)
|
|
2472
|
+
description: "Print the run's absolute deadline as Unix epoch seconds (now + --wall) \u2014 exported as CODE_REVIEW_DEADLINE_EPOCH so every budget hook (main and subagents) measures the same remaining wall"
|
|
2433
2473
|
},
|
|
2434
2474
|
args: {
|
|
2435
2475
|
wall: {
|
|
@@ -2510,13 +2550,17 @@ ${printableSchema(schemaPath)}
|
|
|
2510
2550
|
var seedDraftCmd = defineCommand({
|
|
2511
2551
|
meta: {
|
|
2512
2552
|
name: "seed-draft",
|
|
2513
|
-
description: "Write a valid findings $DRAFT before the review runs: the decoded findings from a prior review when one exists and still validates (incremental re-review), else an empty-but-valid scaffold \u2014 so a valid draft exists from turn 0
|
|
2553
|
+
description: "Write a valid findings $DRAFT before the review runs: the decoded findings from a prior review when one exists and still validates (incremental re-review), else an empty-but-valid scaffold \u2014 so a valid draft exists from turn 0. Also drops a sidecar marker beside the seed so the budget hook can tell the untouched seed from a draft the agent wrote itself. Prints the mode to stdout (prior-same|prior-new when seeded, by whether the prior review examined this same commit; empty-had-prior when a prior review exists but its findings could not be loaded; empty on a first review; none when even the scaffold write failed) and always exits 0"
|
|
2514
2554
|
},
|
|
2515
2555
|
args: {
|
|
2516
2556
|
prior: {
|
|
2517
2557
|
type: "string",
|
|
2518
2558
|
description: "Path to the prior-review JSON gather staged ({ id, body }, or the literal null); its embedded base64 findings marker is decoded and becomes the seed when it validates against the schema"
|
|
2519
2559
|
},
|
|
2560
|
+
"head-sha": {
|
|
2561
|
+
type: "string",
|
|
2562
|
+
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"
|
|
2563
|
+
},
|
|
2520
2564
|
out: {
|
|
2521
2565
|
type: "string",
|
|
2522
2566
|
description: "Path to write the seed $DRAFT to (an absolute path outside the worktree)",
|
|
@@ -2550,12 +2594,12 @@ var seedDraftCmd = defineCommand({
|
|
|
2550
2594
|
writeFileSync(seedMarkerPath(outPath), "code-review seed marker\n");
|
|
2551
2595
|
} catch (err) {
|
|
2552
2596
|
process.stderr.write(
|
|
2553
|
-
`Warning: could not write the seed marker beside ${outPath} (${
|
|
2597
|
+
`Warning: could not write the seed marker beside ${outPath} (${errMsg(err)}) \u2014 the seeded draft will count as agent-written
|
|
2554
2598
|
`
|
|
2555
2599
|
);
|
|
2556
2600
|
}
|
|
2557
2601
|
};
|
|
2558
|
-
const
|
|
2602
|
+
const writeScaffold = () => {
|
|
2559
2603
|
try {
|
|
2560
2604
|
writeFileSync(outPath, `${JSON.stringify(noticeFindings(""), null, 2)}
|
|
2561
2605
|
`);
|
|
@@ -2564,16 +2608,16 @@ var seedDraftCmd = defineCommand({
|
|
|
2564
2608
|
`Seeded ${outPath} with an empty valid scaffold \u2014 no decodable prior findings to build on
|
|
2565
2609
|
`
|
|
2566
2610
|
);
|
|
2567
|
-
|
|
2611
|
+
return true;
|
|
2568
2612
|
} catch (err) {
|
|
2569
2613
|
process.stderr.write(
|
|
2570
|
-
`Warning: could not write the seed scaffold to ${outPath} (${
|
|
2614
|
+
`Warning: could not write the seed scaffold to ${outPath} (${errMsg(err)}) \u2014 the agent will create $DRAFT itself
|
|
2571
2615
|
`
|
|
2572
2616
|
);
|
|
2573
|
-
|
|
2617
|
+
return false;
|
|
2574
2618
|
}
|
|
2575
2619
|
};
|
|
2576
|
-
const
|
|
2620
|
+
const priorBody = (() => {
|
|
2577
2621
|
if (!args.prior) return null;
|
|
2578
2622
|
const raw = (() => {
|
|
2579
2623
|
try {
|
|
@@ -2582,46 +2626,47 @@ var seedDraftCmd = defineCommand({
|
|
|
2582
2626
|
return null;
|
|
2583
2627
|
}
|
|
2584
2628
|
})();
|
|
2585
|
-
|
|
2586
|
-
return body === null ? null : parseFindingsMarker(body);
|
|
2629
|
+
return typeof raw === "object" && raw !== null && "body" in raw && typeof raw.body === "string" ? raw.body : null;
|
|
2587
2630
|
})();
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
return;
|
|
2591
|
-
}
|
|
2592
|
-
const seededFromPrior = (() => {
|
|
2631
|
+
const priorFindings = priorBody === null ? null : parseFindingsMarker(priorBody);
|
|
2632
|
+
const seededFromPrior = priorFindings === null ? false : (() => {
|
|
2593
2633
|
try {
|
|
2594
2634
|
const schemaPath = args.schema ? resolve$1(args.schema) : schemaPathFor(kind, args["schema-version"]);
|
|
2595
2635
|
if (!validateAgainstSchema(priorFindings, schemaPath).valid) return false;
|
|
2596
2636
|
writeFileSync(outPath, `${JSON.stringify(priorFindings, null, 2)}
|
|
2597
2637
|
`);
|
|
2598
2638
|
writeSeedMarker();
|
|
2639
|
+
const priorList = priorFindings.findings;
|
|
2640
|
+
const count = Array.isArray(priorList) ? priorList.length : 0;
|
|
2641
|
+
process.stderr.write(
|
|
2642
|
+
`Seeded ${outPath} from the prior review (${String(count)} finding(s))
|
|
2643
|
+
`
|
|
2644
|
+
);
|
|
2599
2645
|
return true;
|
|
2600
2646
|
} catch (err) {
|
|
2601
2647
|
process.stderr.write(
|
|
2602
|
-
`Warning: could not seed from the prior review (${
|
|
2648
|
+
`Warning: could not seed from the prior review (${errMsg(err)}) \u2014 falling back to the empty scaffold
|
|
2603
2649
|
`
|
|
2604
2650
|
);
|
|
2605
2651
|
return false;
|
|
2606
2652
|
}
|
|
2607
2653
|
})();
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
}
|
|
2617
|
-
|
|
2618
|
-
}
|
|
2654
|
+
const mode = (() => {
|
|
2655
|
+
if (seededFromPrior) {
|
|
2656
|
+
const priorSha = priorBody === null ? null : parseReviewedSha(priorBody);
|
|
2657
|
+
return args["head-sha"] && priorSha && priorSha === args["head-sha"].toLowerCase() ? "prior-same" : "prior-new";
|
|
2658
|
+
}
|
|
2659
|
+
if (!writeScaffold()) return "none";
|
|
2660
|
+
return priorBody === null ? "empty" : "empty-had-prior";
|
|
2661
|
+
})();
|
|
2662
|
+
process.stdout.write(`${mode}
|
|
2663
|
+
`);
|
|
2619
2664
|
}
|
|
2620
2665
|
});
|
|
2621
2666
|
var adaptCmd = defineCommand({
|
|
2622
2667
|
meta: {
|
|
2623
2668
|
name: "adapt",
|
|
2624
|
-
description: "Map a native agent-CLI result envelope onto the abstract SPEC
|
|
2669
|
+
description: "Map a native agent-CLI result envelope onto the abstract SPEC envelope"
|
|
2625
2670
|
},
|
|
2626
2671
|
args: {
|
|
2627
2672
|
native: {
|
|
@@ -2638,6 +2683,10 @@ var adaptCmd = defineCommand({
|
|
|
2638
2683
|
type: "string",
|
|
2639
2684
|
description: "Path to a file the agent was told to write its own validated findings JSON to (wins over the native envelope's structured_output/result when it validates)"
|
|
2640
2685
|
},
|
|
2686
|
+
"agent-file-fallback": {
|
|
2687
|
+
type: "string",
|
|
2688
|
+
description: "Path to the last-valid findings snapshot, tried after --agent-file and before the native envelope; recovers the last valid state when a wall-clock kill left --agent-file truncated, so the review posts that instead of a 'did not complete' notice"
|
|
2689
|
+
},
|
|
2641
2690
|
route: {
|
|
2642
2691
|
type: "string",
|
|
2643
2692
|
description: 'Review route label to stamp into the envelope (e.g. "full review" or "mechanic")'
|
|
@@ -2648,7 +2697,7 @@ var adaptCmd = defineCommand({
|
|
|
2648
2697
|
},
|
|
2649
2698
|
transcript: {
|
|
2650
2699
|
type: "string",
|
|
2651
|
-
description: "Path to the session transcript (
|
|
2700
|
+
description: "Path to the session transcript (main .jsonl). Its tree (main + subagents) gives the true wall + turn count the native envelope under-reports, and refills per-model usage when the native envelope is empty (e.g. after a wall-clock kill)"
|
|
2652
2701
|
}
|
|
2653
2702
|
},
|
|
2654
2703
|
run: async ({ args }) => {
|
|
@@ -2656,6 +2705,7 @@ var adaptCmd = defineCommand({
|
|
|
2656
2705
|
adapt(requireAdapterName(args.adapter), readJSONOrAbsent(args.native), args["agent-file"], {
|
|
2657
2706
|
route: args.route,
|
|
2658
2707
|
effort: args.effort,
|
|
2708
|
+
agentFileFallbackPath: args["agent-file-fallback"],
|
|
2659
2709
|
...args.transcript ? {
|
|
2660
2710
|
transcriptFallback: () => transcriptFallbackFrom(args.transcript)
|
|
2661
2711
|
} : {}
|
|
@@ -2666,11 +2716,7 @@ var adaptCmd = defineCommand({
|
|
|
2666
2716
|
}
|
|
2667
2717
|
});
|
|
2668
2718
|
var isExtractSchemaKind = (s) => s === "findings" || s === "triage";
|
|
2669
|
-
var requireExtractSchemaKind = (name) => {
|
|
2670
|
-
if (isExtractSchemaKind(name)) return name;
|
|
2671
|
-
fail(`Unknown kind "${name}" for extract \u2014 expected one of: findings, triage`);
|
|
2672
|
-
throw new Error("unreachable");
|
|
2673
|
-
};
|
|
2719
|
+
var requireExtractSchemaKind = (name) => isExtractSchemaKind(name) ? name : fail(`Unknown kind "${name}" for extract \u2014 expected one of: findings, triage`);
|
|
2674
2720
|
var failClosedTriage = (outcome) => ({
|
|
2675
2721
|
safe: false,
|
|
2676
2722
|
reasons: describeLadderFailure(outcome)
|
|
@@ -2764,7 +2810,7 @@ var validateFinding = (finding, repoRoot) => {
|
|
|
2764
2810
|
var validatePatchesCmd = defineCommand({
|
|
2765
2811
|
meta: {
|
|
2766
2812
|
name: "validate-patches",
|
|
2767
|
-
description: "Validate each finding's patch against the real PR-head tree
|
|
2813
|
+
description: "Validate each finding's patch against the real PR-head tree: align the finding's range and keep the patch when it anchors, keep it unaligned for a pure insertion, or drop it when it doesn't apply"
|
|
2768
2814
|
},
|
|
2769
2815
|
args: {
|
|
2770
2816
|
findings: {
|
|
@@ -2788,24 +2834,15 @@ var validatePatchesCmd = defineCommand({
|
|
|
2788
2834
|
`);
|
|
2789
2835
|
}
|
|
2790
2836
|
});
|
|
2791
|
-
var requireAdapterName = (name) => {
|
|
2792
|
-
if (isAdapterName(name)) return name;
|
|
2793
|
-
fail(`Unknown adapter "${name}" \u2014 supported: claude-code`);
|
|
2794
|
-
throw new Error("unreachable");
|
|
2795
|
-
};
|
|
2837
|
+
var requireAdapterName = (name) => isAdapterName(name) ? name : fail(`Unknown adapter "${name}" \u2014 supported: claude-code`);
|
|
2796
2838
|
var isSchemaKind = (s) => s === "findings" || s === "triage" || s === "prices";
|
|
2797
|
-
var requireSchemaKind = (name) => {
|
|
2798
|
-
if (isSchemaKind(name)) return name;
|
|
2799
|
-
fail(`Unknown schema "${name}" \u2014 expected one of: findings, triage, prices`);
|
|
2800
|
-
throw new Error("unreachable");
|
|
2801
|
-
};
|
|
2839
|
+
var requireSchemaKind = (name) => isSchemaKind(name) ? name : fail(`Unknown schema "${name}" \u2014 expected one of: findings, triage, prices`);
|
|
2802
2840
|
var requireSchemaPath = (kind, version) => {
|
|
2803
2841
|
try {
|
|
2804
2842
|
return schemaPathFor(kind, version);
|
|
2805
2843
|
} catch (err) {
|
|
2806
|
-
fail(
|
|
2844
|
+
return fail(errMsg(err));
|
|
2807
2845
|
}
|
|
2808
|
-
throw new Error("unreachable");
|
|
2809
2846
|
};
|
|
2810
2847
|
var printSchemaCmd = defineCommand({
|
|
2811
2848
|
meta: {
|
|
@@ -2911,7 +2948,7 @@ var stopGateCmd = defineCommand({
|
|
|
2911
2948
|
bumpNudges(counterPath, nudges);
|
|
2912
2949
|
} catch (err) {
|
|
2913
2950
|
process.stderr.write(
|
|
2914
|
-
`stop-gate: cannot persist nudge counter at ${counterPath} \u2192 allowing to avoid an unbounded block loop: ${
|
|
2951
|
+
`stop-gate: cannot persist nudge counter at ${counterPath} \u2192 allowing to avoid an unbounded block loop: ${errMsg(err)}
|
|
2915
2952
|
`
|
|
2916
2953
|
);
|
|
2917
2954
|
return;
|
|
@@ -3062,7 +3099,7 @@ var main = defineCommand({
|
|
|
3062
3099
|
meta: {
|
|
3063
3100
|
name: "code-review",
|
|
3064
3101
|
version: packageVersion,
|
|
3065
|
-
description: "Deterministic commenter for agentic PR review
|
|
3102
|
+
description: "Deterministic commenter for agentic PR review"
|
|
3066
3103
|
},
|
|
3067
3104
|
subCommands: {
|
|
3068
3105
|
gather: gatherCmd,
|
|
@@ -3087,6 +3124,6 @@ if (!process.env["VITEST"]) {
|
|
|
3087
3124
|
await runMain(main);
|
|
3088
3125
|
}
|
|
3089
3126
|
|
|
3090
|
-
export { main };
|
|
3127
|
+
export { main, snapshotIfValid };
|
|
3091
3128
|
//# sourceMappingURL=index.js.map
|
|
3092
3129
|
//# sourceMappingURL=index.js.map
|