@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 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(join(dirname(mainPath), "subagents")).filter((name) => name.endsWith(".jsonl")).map((name) => join(dirname(mainPath), "subagents", name));
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 = comments.length > 0 ? await fetchBotReviews(input.repo, prNumber, input.botLogin, ghApi) : [];
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 inline review
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 comments on PR #${String(prNumber)}
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 nativeTelemetry = (native, meta) => withMeta(
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
- duration_ms: native.duration_ms,
1784
- vendor_cost_usd: native.total_cost_usd ?? null
1816
+ durationMs: native.duration_ms,
1817
+ vendorCostUsd: native.total_cost_usd ?? null
1785
1818
  },
1786
1819
  meta
1787
1820
  );
1788
- var absentTelemetry = (meta) => withMeta({ models: [], turns: 0, duration_ms: 0, vendor_cost_usd: null }, 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: usage?.firstTsMs != null ? Math.max(0, Date.now() - usage.firstTsMs) : null,
2155
- wallMs: args.wall ? parseWallMs(args.wall) : null,
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, and print-settings"
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"]) {