@jphutchins/code-review 0.1.0-alpha.10 → 0.1.0-alpha.11
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 +95 -16
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/comment.eta +8 -7
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { defineCommand, runMain } from 'citty';
|
|
3
3
|
import { readFileSync, writeFileSync, readdirSync } from 'fs';
|
|
4
|
-
import { resolve as resolve$1, join, dirname } from 'path';
|
|
4
|
+
import { resolve as resolve$1, join, dirname, basename } from 'path';
|
|
5
5
|
import { Eta } from 'eta';
|
|
6
6
|
import parseDiff from 'parse-diff';
|
|
7
7
|
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
@@ -463,8 +463,9 @@ var sumTranscriptUsage = (entries) => {
|
|
|
463
463
|
};
|
|
464
464
|
};
|
|
465
465
|
var subagentFiles = (mainPath) => {
|
|
466
|
+
const dir = join(dirname(mainPath), basename(mainPath, ".jsonl"), "subagents");
|
|
466
467
|
try {
|
|
467
|
-
return readdirSync(
|
|
468
|
+
return readdirSync(dir).filter((name) => name.endsWith(".jsonl")).map((name) => join(dir, name));
|
|
468
469
|
} catch {
|
|
469
470
|
return [];
|
|
470
471
|
}
|
|
@@ -722,6 +723,7 @@ var stopHookSettings = (command) => ({
|
|
|
722
723
|
});
|
|
723
724
|
|
|
724
725
|
// src/budget.ts
|
|
726
|
+
var DEADLINE_ENV = "CODE_REVIEW_DEADLINE_EPOCH";
|
|
725
727
|
var DEFAULT_RESERVE = { frac: 0.15, flatUsd: 0.02, flatMs: 12e4 };
|
|
726
728
|
var SOFT_MULTIPLE = 2;
|
|
727
729
|
var costAxis = (i) => i.spentUsd !== null && i.budgetUsd !== null && i.budgetUsd > 0 ? { used: i.spentUsd, limit: i.budgetUsd, flat: i.reserve.flatUsd } : null;
|
|
@@ -810,6 +812,20 @@ var parseWallMs = (raw) => {
|
|
|
810
812
|
return n * 36e5;
|
|
811
813
|
}
|
|
812
814
|
};
|
|
815
|
+
var parseEpochSecMs = (raw) => {
|
|
816
|
+
if (raw === void 0) return null;
|
|
817
|
+
const t4 = raw.trim();
|
|
818
|
+
if (!/^\d+$/.test(t4)) return null;
|
|
819
|
+
const n = Number.parseInt(t4, 10);
|
|
820
|
+
return Number.isFinite(n) && n > 0 ? n * 1e3 : null;
|
|
821
|
+
};
|
|
822
|
+
var anchoredElapsedMs = (src) => {
|
|
823
|
+
if (src.deadlineMs !== null && src.wallMs !== null)
|
|
824
|
+
return Math.max(0, src.wallMs - (src.deadlineMs - src.nowMs));
|
|
825
|
+
if (src.firstTsMs !== null) return Math.max(0, src.nowMs - src.firstTsMs);
|
|
826
|
+
return null;
|
|
827
|
+
};
|
|
828
|
+
var deadlineEpochSec = (wallMs, nowMs) => Math.floor(nowMs / 1e3) + Math.ceil(wallMs / 1e3);
|
|
813
829
|
var parseFraction = (raw, fallback) => {
|
|
814
830
|
if (raw === void 0) return fallback;
|
|
815
831
|
const n = Number.parseFloat(raw);
|
|
@@ -1362,7 +1378,7 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1362
1378
|
`
|
|
1363
1379
|
);
|
|
1364
1380
|
}
|
|
1365
|
-
const botReviews =
|
|
1381
|
+
const botReviews = await fetchBotReviews(input.repo, prNumber, input.botLogin, ghApi);
|
|
1366
1382
|
const alreadyReviewedThisSha = botReviews.some((r) => r.commitId === input.headSha);
|
|
1367
1383
|
const initialDisposition = comments.length > 0 ? alreadyReviewedThisSha ? { kind: "suppressed-existing-review", sha: input.headSha } : void 0 : strays.length > 0 ? { kind: "none-in-diff" } : void 0;
|
|
1368
1384
|
const commonRenderInput = {
|
|
@@ -1396,10 +1412,9 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1396
1412
|
renderBody(initialDisposition),
|
|
1397
1413
|
ghApi
|
|
1398
1414
|
);
|
|
1399
|
-
if (comments.length === 0) return;
|
|
1400
1415
|
if (alreadyReviewedThisSha) {
|
|
1401
1416
|
process.stderr.write(
|
|
1402
|
-
`A completed bot review already exists for ${input.headSha} \u2014 updated sticky only, no new
|
|
1417
|
+
`A completed bot review already exists for ${input.headSha} \u2014 updated sticky only, no new review
|
|
1403
1418
|
`
|
|
1404
1419
|
);
|
|
1405
1420
|
return;
|
|
@@ -1418,11 +1433,11 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1418
1433
|
ghApi
|
|
1419
1434
|
);
|
|
1420
1435
|
process.stderr.write(
|
|
1421
|
-
`Posted ${String(comments.length)} inline
|
|
1436
|
+
`Posted a review with ${String(comments.length)} inline comment(s) on PR #${String(prNumber)}
|
|
1422
1437
|
`
|
|
1423
1438
|
);
|
|
1424
1439
|
await minimizeSupersededComments(input.repo, prNumber, input.headSha, input.botLogin, ghApi);
|
|
1425
|
-
if (stickyRef !== null) {
|
|
1440
|
+
if (comments.length > 0 && stickyRef !== null) {
|
|
1426
1441
|
const confirmedDisposition = {
|
|
1427
1442
|
kind: "posted",
|
|
1428
1443
|
count: comments.length,
|
|
@@ -1776,16 +1791,34 @@ var withMeta = (base, meta) => ({
|
|
|
1776
1791
|
...meta.route ? { route: meta.route } : {},
|
|
1777
1792
|
...meta.effort ? { effort: meta.effort } : {}
|
|
1778
1793
|
});
|
|
1779
|
-
var
|
|
1794
|
+
var resolveTelemetry = (native, meta) => {
|
|
1795
|
+
const fb = native.models.length === 0 ? meta.transcriptFallback?.() : void 0;
|
|
1796
|
+
const useFallback = fb !== void 0 && fb.models.length > 0;
|
|
1797
|
+
return withMeta(
|
|
1798
|
+
useFallback ? {
|
|
1799
|
+
models: [...fb.models],
|
|
1800
|
+
turns: fb.turns,
|
|
1801
|
+
duration_ms: fb.durationMs,
|
|
1802
|
+
vendor_cost_usd: native.vendorCostUsd
|
|
1803
|
+
} : {
|
|
1804
|
+
models: native.models,
|
|
1805
|
+
turns: native.turns,
|
|
1806
|
+
duration_ms: native.durationMs,
|
|
1807
|
+
vendor_cost_usd: native.vendorCostUsd
|
|
1808
|
+
},
|
|
1809
|
+
meta
|
|
1810
|
+
);
|
|
1811
|
+
};
|
|
1812
|
+
var nativeTelemetry = (native, meta) => resolveTelemetry(
|
|
1780
1813
|
{
|
|
1781
1814
|
models: mapModelUsage(native.modelUsage),
|
|
1782
1815
|
turns: native.num_turns,
|
|
1783
|
-
|
|
1784
|
-
|
|
1816
|
+
durationMs: native.duration_ms,
|
|
1817
|
+
vendorCostUsd: native.total_cost_usd ?? null
|
|
1785
1818
|
},
|
|
1786
1819
|
meta
|
|
1787
1820
|
);
|
|
1788
|
-
var absentTelemetry = (meta) =>
|
|
1821
|
+
var absentTelemetry = (meta) => resolveTelemetry({ models: [], turns: 0, durationMs: 0, vendorCostUsd: null }, meta);
|
|
1789
1822
|
var buildEnvelope = (telemetry, native, agentFilePath) => {
|
|
1790
1823
|
const outcome = findingsOutcome(native, agentFilePath);
|
|
1791
1824
|
switch (outcome.kind) {
|
|
@@ -1908,6 +1941,16 @@ var unwrapAdapt = (either) => {
|
|
|
1908
1941
|
}
|
|
1909
1942
|
throw new Error("unreachable");
|
|
1910
1943
|
};
|
|
1944
|
+
var transcriptFallbackFrom = (path) => {
|
|
1945
|
+
const tree = readTranscriptTree(resolve$1(path));
|
|
1946
|
+
if (tree.missing)
|
|
1947
|
+
process.stderr.write(
|
|
1948
|
+
`code-review adapt: transcript ${path} is unreadable \u2014 no telemetry fallback (issue #36)
|
|
1949
|
+
`
|
|
1950
|
+
);
|
|
1951
|
+
const usage = sumTranscriptUsage(tree.entries);
|
|
1952
|
+
return { models: usage.models, turns: usage.turns, durationMs: usage.durationMs };
|
|
1953
|
+
};
|
|
1911
1954
|
var bundledPath = (...segments) => resolve$1(import.meta.dirname, "..", ...segments);
|
|
1912
1955
|
var packageVersion = JSON.parse(readFileSync(bundledPath("package.json"), "utf-8")).version;
|
|
1913
1956
|
var resolveTemplatePath = (templateArg) => templateArg ? resolve$1(templateArg) : bundledPath("templates", "comment.eta");
|
|
@@ -2148,11 +2191,17 @@ var budgetHookCmd = defineCommand({
|
|
|
2148
2191
|
const usage = tree ? sumTranscriptUsage(tree.entries) : void 0;
|
|
2149
2192
|
const prices = args.prices ? tryReadPrices(args.prices) : null;
|
|
2150
2193
|
const spentUsd = prices !== null && usage ? computeCost(usage.models, prices).totalCostUSD : null;
|
|
2194
|
+
const wallMs = args.wall ? parseWallMs(args.wall) : null;
|
|
2151
2195
|
const output = evaluateBudgetHook(input, {
|
|
2152
2196
|
spentUsd,
|
|
2153
2197
|
budgetUsd: parseBudgetUsd(args["budget-usd"]),
|
|
2154
|
-
elapsedMs:
|
|
2155
|
-
|
|
2198
|
+
elapsedMs: anchoredElapsedMs({
|
|
2199
|
+
deadlineMs: parseEpochSecMs(process.env[DEADLINE_ENV]),
|
|
2200
|
+
wallMs,
|
|
2201
|
+
firstTsMs: usage?.firstTsMs ?? null,
|
|
2202
|
+
nowMs: Date.now()
|
|
2203
|
+
}),
|
|
2204
|
+
wallMs,
|
|
2156
2205
|
reserve: {
|
|
2157
2206
|
frac: parseFraction(args["reserve-frac"], DEFAULT_RESERVE.frac),
|
|
2158
2207
|
flatUsd: parseBudgetUsd(args["reserve-usd"]) ?? DEFAULT_RESERVE.flatUsd,
|
|
@@ -2252,6 +2301,28 @@ var printSettingsCmd = defineCommand({
|
|
|
2252
2301
|
`);
|
|
2253
2302
|
}
|
|
2254
2303
|
});
|
|
2304
|
+
var deadlineCmd = defineCommand({
|
|
2305
|
+
meta: {
|
|
2306
|
+
name: "deadline",
|
|
2307
|
+
description: "Print the run's absolute deadline as Unix epoch seconds (now + --wall). The review job exports this as CODE_REVIEW_DEADLINE_EPOCH right before `claude -p` so every budget hook \u2014 the main agent's and each fan-out subagent's \u2014 measures the SAME true remaining wall instead of its own transcript's start, which reads \u22480 in a fresh subagent and leaves the fan-out unsteered (issue #45)."
|
|
2308
|
+
},
|
|
2309
|
+
args: {
|
|
2310
|
+
wall: {
|
|
2311
|
+
type: "string",
|
|
2312
|
+
description: "Wall-clock budget for the run (e.g. 24m, 1200s, 2h) \u2014 the deadline is now + this",
|
|
2313
|
+
required: true
|
|
2314
|
+
}
|
|
2315
|
+
},
|
|
2316
|
+
run: async ({ args }) => {
|
|
2317
|
+
const wallMs = parseWallMs(args.wall);
|
|
2318
|
+
if (wallMs === null) {
|
|
2319
|
+
fail(`--wall must be a duration like 24m, 1200s, or 2h (got '${args.wall}')`);
|
|
2320
|
+
} else {
|
|
2321
|
+
process.stdout.write(`${String(deadlineEpochSec(wallMs, Date.now()))}
|
|
2322
|
+
`);
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
});
|
|
2255
2326
|
var derivedSchemaVersion = (kind, raw) => kind === "findings" ? declaredVersion(raw) : void 0;
|
|
2256
2327
|
var validateCmd = defineCommand({
|
|
2257
2328
|
meta: {
|
|
@@ -2319,13 +2390,20 @@ var adaptCmd = defineCommand({
|
|
|
2319
2390
|
effort: {
|
|
2320
2391
|
type: "string",
|
|
2321
2392
|
description: 'Effort label to stamp into the envelope (e.g. "max" or "low")'
|
|
2393
|
+
},
|
|
2394
|
+
transcript: {
|
|
2395
|
+
type: "string",
|
|
2396
|
+
description: "Path to the session transcript (the main .jsonl); when the native envelope carries no per-model usage \u2014 a wall-clock kill leaves it empty (issue #39) \u2014 telemetry is recovered from the transcript tree (main + subagents) so cost is real, not $0.00 (issue #36)"
|
|
2322
2397
|
}
|
|
2323
2398
|
},
|
|
2324
2399
|
run: async ({ args }) => {
|
|
2325
2400
|
const envelope = unwrapAdapt(
|
|
2326
2401
|
adapt(requireAdapterName(args.adapter), readJSONOrAbsent(args.native), args["agent-file"], {
|
|
2327
2402
|
route: args.route,
|
|
2328
|
-
effort: args.effort
|
|
2403
|
+
effort: args.effort,
|
|
2404
|
+
...args.transcript ? {
|
|
2405
|
+
transcriptFallback: () => transcriptFallbackFrom(args.transcript)
|
|
2406
|
+
} : {}
|
|
2329
2407
|
})
|
|
2330
2408
|
);
|
|
2331
2409
|
process.stdout.write(`${JSON.stringify(envelope, null, 2)}
|
|
@@ -2733,7 +2811,7 @@ var main = defineCommand({
|
|
|
2733
2811
|
meta: {
|
|
2734
2812
|
name: "code-review",
|
|
2735
2813
|
version: packageVersion,
|
|
2736
|
-
description: "Deterministic commenter for agentic PR review \u2014 gather, render, inline, post, adapt, extract, validate-patches, cost, check-cost, validate, stop-gate, budget-hook,
|
|
2814
|
+
description: "Deterministic commenter for agentic PR review \u2014 gather, render, inline, post, adapt, extract, validate-patches, cost, check-cost, validate, stop-gate, budget-hook, print-settings, and deadline"
|
|
2737
2815
|
},
|
|
2738
2816
|
subCommands: {
|
|
2739
2817
|
gather: gatherCmd,
|
|
@@ -2749,7 +2827,8 @@ var main = defineCommand({
|
|
|
2749
2827
|
"print-schema": printSchemaCmd,
|
|
2750
2828
|
"stop-gate": stopGateCmd,
|
|
2751
2829
|
"budget-hook": budgetHookCmd,
|
|
2752
|
-
"print-settings": printSettingsCmd
|
|
2830
|
+
"print-settings": printSettingsCmd,
|
|
2831
|
+
deadline: deadlineCmd
|
|
2753
2832
|
}
|
|
2754
2833
|
});
|
|
2755
2834
|
if (!process.env["VITEST"]) {
|