@jphutchins/code-review 0.1.0-alpha.46 → 0.1.0-alpha.48

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
@@ -33,6 +33,7 @@ var LineNumber = t.refinement(
33
33
  var Confidence = t.refinement(t.number, (n) => n >= 0 && n <= 1, "Confidence");
34
34
  var Likelihood = t.refinement(t.number, (n) => n >= 0 && n <= 1, "Likelihood");
35
35
  var FiniteNumber = t.refinement(t.number, (n) => Number.isFinite(n), "FiniteNumber");
36
+ var NonNegativePrice = t.refinement(t.number, (n) => n >= 0, "NonNegativePrice");
36
37
  var SCHEMA_VERSION_RE = /^(0|[1-9]\d*)\.(\d+)\.(\d+)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
37
38
  var SchemaVersion = t.refinement(
38
39
  t.string,
@@ -170,6 +171,15 @@ var ConvergenceStrict = t.refinement(
170
171
  "ConvergenceStrict"
171
172
  );
172
173
  var ConvergenceCodec = t.exact(ConvergenceStrict);
174
+ var LineDelta = t.refinement(
175
+ t.number,
176
+ (n) => Number.isInteger(n) && n >= 0,
177
+ "LineDelta"
178
+ );
179
+ var ChangeLinesCodec = t.exact(t.type({ added: LineDelta, removed: LineDelta }));
180
+ var ChangeSizeCodec = t.exact(
181
+ t.partial({ code: ChangeLinesCodec, tests: ChangeLinesCodec, docs: ChangeLinesCodec })
182
+ );
173
183
  var FindingsCodec = t.exact(
174
184
  t.intersection([
175
185
  t.type({
@@ -183,10 +193,20 @@ var FindingsCodec = t.exact(
183
193
  // Pipeline-stamped only (issue #150); see ScopeMetastasisCodec.
184
194
  scope_metastasis: ScopeMetastasisCodec,
185
195
  // Pipeline-stamped only (issue #174); see ConvergenceCodec.
186
- convergence: ConvergenceCodec
196
+ convergence: ConvergenceCodec,
197
+ // Agent-written best-effort (issue #182); see ChangeSizeCodec.
198
+ change_size: ChangeSizeCodec
187
199
  })
188
200
  ])
189
201
  );
202
+ var PIPELINE_STAMPED_FIELDS = /* @__PURE__ */ new Set([
203
+ "convergence",
204
+ "scope_metastasis"
205
+ ]);
206
+ var RECOVERABLE_OPTIONAL_FIELDS = /* @__PURE__ */ new Set([
207
+ ...PIPELINE_STAMPED_FIELDS,
208
+ "change_size"
209
+ ]);
190
210
  var TriageCodec = t.type({
191
211
  safe: t.boolean,
192
212
  reasons: t.string
@@ -219,6 +239,11 @@ var ResultEnvelopeCodec = t.intersection([
219
239
  vendor_cost_usd: t.union([t.number, t.null]),
220
240
  route: t.string,
221
241
  effort: t.string,
242
+ // The ISO-8601 UTC instant the run completed (stamped by `adapt` when it built this envelope, issue
243
+ // #170). Cost recomputation prices a time-slotted model at THIS instant, so the same envelope prices
244
+ // to the same slot deterministically wherever it is re-rendered — not at each command's wall clock.
245
+ // Absent on a pre-#170 envelope (cost then falls back to the caller's instant).
246
+ generated_at: t.string,
222
247
  // The run produced a notice rather than a completed review (security-gate block, agent kill, no
223
248
  // recoverable findings). An empty `findings` array alone can't say this — a genuine clean review
224
249
  // is also empty — so the render suppresses "clean review" and the sticky precedence guard refuses
@@ -226,12 +251,44 @@ var ResultEnvelopeCodec = t.intersection([
226
251
  incomplete: t.boolean
227
252
  })
228
253
  ]);
229
- var ModelPricesCodec = t.type({
230
- in: t.number,
231
- out: t.number,
232
- cache_read: t.number,
233
- cache_write: t.number
254
+ var FlatModelPricesShape = t.type({
255
+ in: NonNegativePrice,
256
+ out: NonNegativePrice,
257
+ cache_read: NonNegativePrice,
258
+ cache_write: NonNegativePrice
234
259
  });
260
+ var FLAT_PRICE_KEYS = new Set(Object.keys(FlatModelPricesShape.props));
261
+ var FlatModelPricesStrict = t.refinement(
262
+ FlatModelPricesShape,
263
+ (p) => Object.keys(p).every((k) => FLAT_PRICE_KEYS.has(k)),
264
+ "FlatModelPricesStrict"
265
+ );
266
+ var FlatModelPricesCodec = t.exact(FlatModelPricesStrict);
267
+ var UTC_HHMM_RE = /^([01][0-9]|2[0-3]):[0-5][0-9]$/;
268
+ var UtcHHMM = t.refinement(t.string, (s) => UTC_HHMM_RE.test(s), "UtcHHMM");
269
+ var PriceSlotShape = t.intersection([
270
+ t.type({ utc_from: UtcHHMM, utc_to: UtcHHMM }),
271
+ FlatModelPricesShape
272
+ ]);
273
+ var PRICE_SLOT_KEYS = /* @__PURE__ */ new Set(["utc_from", "utc_to", ...Object.keys(FlatModelPricesShape.props)]);
274
+ var PriceSlotStrict = t.refinement(
275
+ PriceSlotShape,
276
+ (s) => Object.keys(s).every((k) => PRICE_SLOT_KEYS.has(k)),
277
+ "PriceSlotStrict"
278
+ );
279
+ var PriceSlotCodec = t.exact(PriceSlotStrict);
280
+ var NonEmptyPriceSlots = t.refinement(
281
+ t.array(PriceSlotCodec),
282
+ (a) => a.length >= 1,
283
+ "NonEmptyPriceSlots"
284
+ );
285
+ var SlottedModelPricesStrict = t.refinement(
286
+ t.type({ slots: NonEmptyPriceSlots }),
287
+ (s) => Object.keys(s).every((k) => k === "slots"),
288
+ "SlottedModelPricesStrict"
289
+ );
290
+ var SlottedModelPricesCodec = t.exact(SlottedModelPricesStrict);
291
+ var ModelPricesCodec = t.union([FlatModelPricesCodec, SlottedModelPricesCodec]);
235
292
  var PriceMapCodec = t.type({
236
293
  _updated: t.string,
237
294
  _unit: t.string,
@@ -265,35 +322,64 @@ var defaultWarn = (message) => {
265
322
  process.stderr.write(`${message}
266
323
  `);
267
324
  };
268
- var computeModelCost = (entry, prices, warn) => {
269
- const p = prices.models[entry.model];
270
- const cacheRead = entry.cache_read_tokens ?? 0;
271
- const cacheWrite = entry.cache_write_tokens ?? 0;
272
- if (!p) {
325
+ var parseInstant = (iso) => {
326
+ if (iso === void 0) return void 0;
327
+ if (iso.includes("T") && !/(Z|[+-]\d{2}:?\d{2})$/.test(iso)) return void 0;
328
+ const d = new Date(iso);
329
+ return Number.isNaN(d.getTime()) ? void 0 : d;
330
+ };
331
+ var utcMinuteOfDay = (at) => at.getUTCHours() * 60 + at.getUTCMinutes();
332
+ var hhmmToMinutes = (hhmm) => {
333
+ const [h, m] = hhmm.split(":");
334
+ return Number(h) * 60 + Number(m);
335
+ };
336
+ var hhmmOf = (minute) => `${String(Math.floor(minute / 60)).padStart(2, "0")}:${String(minute % 60).padStart(2, "0")}`;
337
+ var slotCovers = (slot, minute) => {
338
+ const from = hhmmToMinutes(slot.utc_from);
339
+ const to = hhmmToMinutes(slot.utc_to);
340
+ return from < to ? minute >= from && minute < to : minute >= from || minute < to;
341
+ };
342
+ var resolveFlatPrices = (model, p, pricedAt, warn) => {
343
+ if (!("slots" in p)) return p;
344
+ if (pricedAt === void 0) {
273
345
  warn(
274
- `code-review cost: unknown model "${entry.model}" \u2014 no entry in price map; cost for this model set to $0`
346
+ `code-review cost: model "${model}" has time-slotted prices but no run instant was supplied to select a slot; cost for this model set to $0`
275
347
  );
276
- return {
277
- model: entry.model,
278
- inputTokens: entry.input_tokens,
279
- outputTokens: entry.output_tokens,
280
- cacheReadTokens: cacheRead,
281
- cacheWriteTokens: cacheWrite,
282
- costUSD: 0
283
- };
348
+ return null;
284
349
  }
285
- const costUSD = (entry.input_tokens * p.in + entry.output_tokens * p.out + cacheRead * p.cache_read + cacheWrite * p.cache_write) / 1e6;
286
- return {
350
+ const minute = utcMinuteOfDay(pricedAt);
351
+ const covering = p.slots.filter((s) => slotCovers(s, minute));
352
+ if (covering.length === 1) return covering[0] ?? null;
353
+ warn(
354
+ `code-review cost: model "${model}" \u2014 ${String(covering.length)} price slots cover ${hhmmOf(minute)} UTC (expected exactly 1); a model's slots must partition the 24h day with no gap or overlap; cost for this model set to $0`
355
+ );
356
+ return null;
357
+ };
358
+ var computeModelCost = (entry, prices, pricedAt, warn) => {
359
+ const p = prices.models[entry.model];
360
+ const cacheRead = entry.cache_read_tokens ?? 0;
361
+ const cacheWrite = entry.cache_write_tokens ?? 0;
362
+ const zero = {
287
363
  model: entry.model,
288
364
  inputTokens: entry.input_tokens,
289
365
  outputTokens: entry.output_tokens,
290
366
  cacheReadTokens: cacheRead,
291
367
  cacheWriteTokens: cacheWrite,
292
- costUSD
368
+ costUSD: 0
293
369
  };
370
+ if (!p) {
371
+ warn(
372
+ `code-review cost: unknown model "${entry.model}" \u2014 no entry in price map; cost for this model set to $0`
373
+ );
374
+ return zero;
375
+ }
376
+ const rate = resolveFlatPrices(entry.model, p, pricedAt, warn);
377
+ if (rate === null) return zero;
378
+ const costUSD = (entry.input_tokens * rate.in + entry.output_tokens * rate.out + cacheRead * rate.cache_read + cacheWrite * rate.cache_write) / 1e6;
379
+ return { ...zero, costUSD };
294
380
  };
295
- var computeCost = (models, prices, warn = defaultWarn) => {
296
- const lines = models.map((entry) => computeModelCost(entry, prices, warn));
381
+ var computeCost = (models, prices, pricedAt, warn = defaultWarn) => {
382
+ const lines = models.map((entry) => computeModelCost(entry, prices, pricedAt, warn));
297
383
  return {
298
384
  lines,
299
385
  totalInputTokens: lines.reduce((s, l) => s + l.inputTokens, 0),
@@ -477,13 +563,7 @@ var normalizeCodeCounts = (codes, priorCodes) => {
477
563
  if (typeof codes !== "object" || codes === null || Array.isArray(codes)) return void 0;
478
564
  const entries = Object.entries(codes).filter(
479
565
  (e) => typeof e[1] === "number" && Number.isSafeInteger(e[1]) && e[1] > 0
480
- ).sort((a, b) => {
481
- if (b[1] !== a[1]) return b[1] - a[1];
482
- const aPrior = hasCode(priorCodes, a[0]) ? 1 : 0;
483
- const bPrior = hasCode(priorCodes, b[0]) ? 1 : 0;
484
- if (aPrior !== bPrior) return bPrior - aPrior;
485
- return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0;
486
- });
566
+ );
487
567
  if (entries.length === 0) return void 0;
488
568
  const sorted = entries.sort((a, b) => {
489
569
  if (b[1] !== a[1]) return b[1] - a[1];
@@ -666,6 +746,23 @@ var priorBelowFloorNits = (priorDoc, floor = DEFAULT_NIT_VISIBILITY_FLOOR) => {
666
746
  };
667
747
  var convergenceBadge = (c) => c.converged ? `**Convergence** \u{1F3C1} ${formatScore(c.score)} \u2264 ${String(c.threshold)} \u2014 converged` : `**Convergence** \u{1F504} ${formatScore(c.score)} > ${String(c.threshold)} \u2014 iterating`;
668
748
  var convergenceSummary = (doc, threshold = DEFAULT_CONVERGENCE_THRESHOLD) => convergenceBadge(convergenceSignal(doc, threshold));
749
+ var changeSizeSummary = (changeSize) => {
750
+ if (changeSize === void 0) return "";
751
+ const cell = (label, d) => d === void 0 ? null : `+${String(d.added)} / \u2212${String(d.removed)} ${label}`;
752
+ return [
753
+ cell("code", changeSize.code),
754
+ cell("tests", changeSize.tests),
755
+ cell("docs", changeSize.docs)
756
+ ].filter((c) => c !== null).join(" \xB7 ");
757
+ };
758
+ var nextRoundNumber = (priorTraj, priorConvRounds) => {
759
+ const last = (rounds) => rounds.length > 0 ? rounds[rounds.length - 1]?.round ?? rounds.length : 0;
760
+ return Math.max(last(priorTraj), last(priorConvRounds)) + 1;
761
+ };
762
+ var inProgressConvergence = (prior, runningRound) => {
763
+ const rounds = prior.rounds ?? [];
764
+ return rounds.length === 0 ? "" : `${roundsSummary(rounds, runningRound)} \u2192 \u23F3`;
765
+ };
669
766
  var SURFACE_SCHEMA_VERSION = "0.8.0";
670
767
  var convergenceSignal = (doc, threshold = DEFAULT_CONVERGENCE_THRESHOLD) => {
671
768
  const score = convergenceScore(doc, threshold);
@@ -1131,7 +1228,8 @@ var render = (input) => {
1131
1228
  const usageAvailable = input.envelope !== null;
1132
1229
  const hasUsage = input.envelope !== null && input.envelope.models.length > 0;
1133
1230
  const incomplete = (input.incomplete ?? input.envelope?.incomplete ?? false) || isIncompleteFindings(input.findings);
1134
- const costReport = input.envelope ? computeCost(input.envelope.models, input.prices) : null;
1231
+ const pricedAt = parseInstant(input.envelope?.generated_at) ?? input.pricedAt;
1232
+ const costReport = input.envelope ? computeCost(input.envelope.models, input.prices, pricedAt) : null;
1135
1233
  const pricesProvided = input.pricesProvided ?? true;
1136
1234
  const route = input.route ?? input.envelope?.route ?? null;
1137
1235
  const effort = input.effort ?? input.envelope?.effort ?? null;
@@ -1154,6 +1252,10 @@ var render = (input) => {
1154
1252
  effort,
1155
1253
  modelNames,
1156
1254
  testReport: input.testReport ?? null,
1255
+ // The agent's best-effort role split (issue #182) and the pipeline's deterministic cloc table; both
1256
+ // chrome, gated to a completed review by the template's !incomplete block.
1257
+ changeSummary: changeSizeSummary(input.findings.change_size),
1258
+ clocDiff: input.clocDiff !== void 0 ? escapeFence(input.clocDiff) : null,
1157
1259
  reviewedSha: input.reviewedSha ?? "0000000000000000000000000000000000000000",
1158
1260
  postedAt: input.postedAt ?? "",
1159
1261
  severityCounts,
@@ -1170,7 +1272,6 @@ var render = (input) => {
1170
1272
  // (issue #174) — no separate signal or rounds marker rides beside it. post always supplies the
1171
1273
  // precomputed marker; the standalone `render` command falls back to encoding the doc here.
1172
1274
  findingsPointer: input.findingsPointer ?? findingsPointer(input.findings, input.jsonUrl),
1173
- roundsMarker: "",
1174
1275
  roundsSummary: roundsSummary(trajectory, input.roundCount),
1175
1276
  metastasisNote: advisoryAllowed ? metastasisNote(trajectory) : "",
1176
1277
  sameRootNotes: advisoryAllowed ? sameRootNotes : {},
@@ -1777,11 +1878,24 @@ var triageTable = [
1777
1878
  ];
1778
1879
  var pricesTable = [
1779
1880
  {
1881
+ // Retained non-latest so `validate`/`print-schema --schema-version 0.1` still resolves (the
1882
+ // keep-old-minors convention the findings table follows). The price map carries no version signal,
1883
+ // and the schema is a single unversioned file, so both entries point at it; the codec's oneOf
1884
+ // accepts a flat 0.1-era map unchanged.
1780
1885
  minor: "0.1",
1781
1886
  defaultVersion: "0.1.0",
1782
1887
  schemaFile: "prices.schema.json",
1783
1888
  codec: PriceMapCodec,
1784
1889
  normalize: identity,
1890
+ latest: false
1891
+ },
1892
+ {
1893
+ // v0.2.0 (issue #170): a model's value gained the time-slotted alternative (flat | { slots }).
1894
+ minor: "0.2",
1895
+ defaultVersion: "0.2.0",
1896
+ schemaFile: "prices.schema.json",
1897
+ codec: PriceMapCodec,
1898
+ normalize: identity,
1785
1899
  latest: true
1786
1900
  }
1787
1901
  ];
@@ -1826,7 +1940,8 @@ var resolveFindings = (raw) => {
1826
1940
  return decoded._tag === "Left" ? { kind: "invalid-shape", errors: formatErrors(decoded.left) } : { kind: "ok", version, value: entry.normalize(decoded.right) };
1827
1941
  };
1828
1942
  var resolveSingleVersion = (kind, raw) => {
1829
- const entry = tableFor(kind)[0];
1943
+ const table = tableFor(kind);
1944
+ const entry = table.find((e) => e.latest) ?? table[0];
1830
1945
  if (!entry) throw new Error(`Registry invariant violated \u2014 no entry for "${kind}"`);
1831
1946
  const decoded = entry.codec.decode(raw);
1832
1947
  return decoded._tag === "Left" ? { kind: "invalid-shape", errors: formatErrors(decoded.left) } : { kind: "ok", version: entry.defaultVersion, value: entry.normalize(decoded.right) };
@@ -2055,7 +2170,6 @@ var checkLongSuggestions = (comments) => {
2055
2170
  });
2056
2171
  return { comments: adjusted, longFiles };
2057
2172
  };
2058
- var PIPELINE_STAMPED_FIELDS = /* @__PURE__ */ new Set(["convergence", "scope_metastasis"]);
2059
2173
  var decodeFindings = (doc) => {
2060
2174
  const resolution = resolve("findings", doc);
2061
2175
  switch (resolution.kind) {
@@ -2076,14 +2190,14 @@ var loadFindings = (path) => {
2076
2190
  return { kind: "corrupt" };
2077
2191
  }
2078
2192
  const first = decodeFindings(raw);
2079
- if (first.kind === "invalid-shape" && typeof raw === "object" && raw !== null && !Array.isArray(raw) && Object.keys(raw).some((k) => PIPELINE_STAMPED_FIELDS.has(k))) {
2193
+ if (first.kind === "invalid-shape" && typeof raw === "object" && raw !== null && !Array.isArray(raw) && Object.keys(raw).some((k) => RECOVERABLE_OPTIONAL_FIELDS.has(k))) {
2080
2194
  const stripped = Object.fromEntries(
2081
- Object.entries(raw).filter(([key2]) => !PIPELINE_STAMPED_FIELDS.has(key2))
2195
+ Object.entries(raw).filter(([key2]) => !RECOVERABLE_OPTIONAL_FIELDS.has(key2))
2082
2196
  );
2083
2197
  const retry = decodeFindings(stripped);
2084
2198
  if (retry.kind === "ok") {
2085
2199
  process.stderr.write(
2086
- "Warning: the review draft carried an invalid pipeline-stamped field (convergence/scope_metastasis) \u2014 stripped it and used the rest; the pipeline re-stamps convergence\n"
2200
+ "Warning: the review draft carried an invalid best-effort field (convergence/scope_metastasis/change_size) \u2014 stripped it and used the rest; the pipeline re-stamps convergence\n"
2087
2201
  );
2088
2202
  return retry;
2089
2203
  }
@@ -2133,6 +2247,19 @@ var loadTestReport = (path) => {
2133
2247
  }
2134
2248
  return decoded.right;
2135
2249
  };
2250
+ var loadClocDiff = (path) => {
2251
+ let raw;
2252
+ try {
2253
+ raw = readFileSync(path, "utf-8");
2254
+ } catch (err) {
2255
+ process.stderr.write(
2256
+ `Warning: could not read cloc diff at ${path}: ${errMsg(err)} \u2014 omitting the cloc collapsible
2257
+ `
2258
+ );
2259
+ return void 0;
2260
+ }
2261
+ return raw.trim() === "" ? void 0 : raw;
2262
+ };
2136
2263
  var parseHtmlUrl = (raw) => {
2137
2264
  const parsed = tryParseJson(raw);
2138
2265
  const htmlUrl = parsed.ok ? asRecord(parsed.value)?.["html_url"] : void 0;
@@ -2378,8 +2505,7 @@ var post = async (input, ghApi = runGhApi) => {
2378
2505
  const priorBody = existingSticky?.body ?? "";
2379
2506
  const priorTraj = priorTrajectory(priorDoc, priorBody);
2380
2507
  const priorConv = carriedConvergence(priorDoc, priorBody);
2381
- const lastRound = (rounds) => rounds.length > 0 ? rounds[rounds.length - 1]?.round ?? rounds.length : 0;
2382
- const priorRoundCount = Math.max(lastRound(priorTraj), lastRound(priorConv?.rounds ?? []));
2508
+ const priorRoundCount = nextRoundNumber(priorTraj, priorConv?.rounds ?? []) - 1;
2383
2509
  const stampConvergence = (doc, conv) => ({
2384
2510
  ...doc,
2385
2511
  convergence: conv ?? void 0
@@ -2511,6 +2637,7 @@ ${dropNote}` : ""}`,
2511
2637
  const answeredDropNote = answeredReRaiseNote(verbatimReRaised, droppedCount) + (verbatimReRaised.length > 0 && findings.findings.length === 0 ? "\n> _The stop signal reflects the kept findings \u2014 this round carries none._" : "");
2512
2638
  const envelope = loadEnvelope(input.envelopePath);
2513
2639
  const testReport = input.testReportPath ? loadTestReport(input.testReportPath) : void 0;
2640
+ const clocDiff = input.clocDiffPath ? loadClocDiff(input.clocDiffPath) : void 0;
2514
2641
  const effectiveRoute = input.route ?? envelope?.route;
2515
2642
  if (envelope === null) {
2516
2643
  const envelopelessIncomplete = isIncompleteFindings(findings);
@@ -2548,6 +2675,7 @@ ${dropNote}` : ""}`,
2548
2675
  roundCount: priorRoundCount,
2549
2676
  convergenceRound: false,
2550
2677
  testReport,
2678
+ clocDiff,
2551
2679
  inlineDisposition: { kind: "no-envelope" },
2552
2680
  runUrl: input.runUrl,
2553
2681
  jsonUrl: input.jsonUrl,
@@ -2626,6 +2754,7 @@ ${dropNote}` : ""}`,
2626
2754
  reviewedSha: input.headSha,
2627
2755
  effort: input.effort,
2628
2756
  testReport,
2757
+ clocDiff,
2629
2758
  severityCounts: currentCounts,
2630
2759
  sameRootNotes,
2631
2760
  answeredNotes: reRaisedNotes,
@@ -2639,7 +2768,8 @@ ${dropNote}` : ""}`,
2639
2768
  runUrl: input.runUrl,
2640
2769
  jsonUrl: input.jsonUrl,
2641
2770
  findingsPointer: findingsMarker,
2642
- postedAt: input.postedAt
2771
+ postedAt: input.postedAt,
2772
+ pricedAt: input.pricedAt
2643
2773
  };
2644
2774
  const longFilesNote = longFiles.length > 0 ? `
2645
2775
 
@@ -2724,12 +2854,22 @@ var bodyRefsRun = (body, runUrl) => {
2724
2854
  const runId = runIdFromUrl(runUrl);
2725
2855
  return runId === null ? body.includes(runUrl) : new RegExp(`/actions/runs/${runId}(?!\\d)`).test(body);
2726
2856
  };
2727
- var announceBody = (headSha, runUrl, existingBody) => noticeBody(
2728
- `${DEFAULT_MARKER}
2857
+ var announceBody = (headSha, runUrl, existingBody) => {
2858
+ const priorDoc = existingBody !== void 0 ? parseFindingsMarker(existingBody) : null;
2859
+ const prior = existingBody !== void 0 ? carriedConvergence(priorDoc, existingBody) : null;
2860
+ const progress = prior !== null ? inProgressConvergence(
2861
+ prior,
2862
+ nextRoundNumber(priorTrajectory(priorDoc, existingBody ?? ""), prior.rounds ?? [])
2863
+ ) : "";
2864
+ return noticeBody(
2865
+ `${DEFAULT_MARKER}
2729
2866
 
2730
- \u{1F504} **Code review in progress** for \`${headSha.slice(0, 7)}\` \u2014 see the [workflow run](${runUrl}) for progress; this comment is updated with the review when it completes.`,
2731
- existingBody
2732
- );
2867
+ \u{1F504} **Code review in progress** for \`${headSha.slice(0, 7)}\` \u2014 see the [workflow run](${runUrl}) for progress; this comment is updated with the review when it completes.${progress ? `
2868
+
2869
+ ${progress}` : ""}`,
2870
+ existingBody
2871
+ );
2872
+ };
2733
2873
  var announce = async (input, ghApi = runGhApi) => {
2734
2874
  const candidates = await fetchPrCandidates(input.repo, input.headSha, ghApi);
2735
2875
  const resolution = resolvePr(candidates, input.headBranch);
@@ -3074,6 +3214,7 @@ var renderOutputs = (result) => {
3074
3214
  conclusion=${result.conclusion}
3075
3215
  diff_size=${String(result.diffSize)}
3076
3216
  stacked=${String(result.stacked)}
3217
+ base_sha=${result.baseSha}
3077
3218
  `;
3078
3219
  }
3079
3220
  };
@@ -3343,7 +3484,8 @@ var gather = async (input, ghApi = runGhApi, gitRun = runGit) => {
3343
3484
  pr: prNumber,
3344
3485
  conclusion: input.conclusion,
3345
3486
  diffSize: Buffer.byteLength(prDiff, "utf8"),
3346
- stacked
3487
+ stacked,
3488
+ baseSha: meta.base_sha
3347
3489
  };
3348
3490
  };
3349
3491
 
@@ -3576,10 +3718,16 @@ var nativeTelemetry = (native, meta) => resolveTelemetry(
3576
3718
  );
3577
3719
  var absentTelemetry = (meta) => resolveTelemetry({ models: [], turns: 0, durationMs: 0, vendorCostUsd: null }, meta);
3578
3720
  var buildEnvelope = (telemetry, native, agentFilePath, agentFileFallbackPath, seedUnrevised) => {
3721
+ const generated_at = (/* @__PURE__ */ new Date()).toISOString();
3579
3722
  const outcome = findingsOutcome(native, agentFilePath, agentFileFallbackPath);
3580
3723
  switch (outcome.kind) {
3581
3724
  case "ok":
3582
- return { schema_version: outcome.version, findings: outcome.findings, ...telemetry };
3725
+ return {
3726
+ schema_version: outcome.version,
3727
+ findings: outcome.findings,
3728
+ generated_at,
3729
+ ...telemetry
3730
+ };
3583
3731
  case "telemetry-only": {
3584
3732
  const reason = seedUnrevised ? "the review agent did not write a review (its draft is still the pre-seeded sentinel)" : outcome.reason;
3585
3733
  return {
@@ -3588,6 +3736,7 @@ var buildEnvelope = (telemetry, native, agentFilePath, agentFileFallbackPath, se
3588
3736
 
3589
3737
  ${reason}`),
3590
3738
  incomplete: true,
3739
+ generated_at,
3591
3740
  ...telemetry
3592
3741
  };
3593
3742
  }
@@ -3792,6 +3941,7 @@ var resolvePrices = (pricesArg) => {
3792
3941
  return { kind: "absent", path: bundledPath("schema", "prices.example.json") };
3793
3942
  };
3794
3943
  var TEST_REPORT_DESCRIPTION = 'Path to a JSON test summary: {"passed": number, "failed": number, "total": number, "failures"?: [{"name": string, "message"?: string}]}';
3944
+ var CLOC_DIFF_DESCRIPTION = "Path to a raw `cloc --git --diff <base> <head>` table, rendered verbatim in the sticky's cloc collapsible. Best-effort: an absent, unreadable, or empty file omits the collapsible.";
3795
3945
  var CONVERGENCE_THRESHOLD_DESCRIPTION = "Advisory convergence tolerance: the per-finding convergence score (each finding's severity floor + confidence-and-likelihood-weighted headroom; ceilings critical 4 \xB7 major 2 \xB7 minor 1 \xB7 nit 0) at or below which the sticky reads as converged. The floor values and the systemic-likelihood rule are documented in the README and the findings schema (default: 1)";
3796
3946
  var NIT_VISIBILITY_FLOOR_DESCRIPTION = "Nit visibility floor: nits whose confidence \xD7 likelihood falls below this are hidden from humans (no inline comment; a collapsed aside in the sticky) but kept in the machine blob as adjudicated. In [0, 1] (default: 0.25)";
3797
3947
  var renderCmd = defineCommand({
@@ -3834,6 +3984,10 @@ var renderCmd = defineCommand({
3834
3984
  type: "string",
3835
3985
  description: TEST_REPORT_DESCRIPTION
3836
3986
  },
3987
+ "cloc-diff": {
3988
+ type: "string",
3989
+ description: CLOC_DIFF_DESCRIPTION
3990
+ },
3837
3991
  "convergence-threshold": {
3838
3992
  type: "string",
3839
3993
  description: CONVERGENCE_THRESHOLD_DESCRIPTION
@@ -3851,6 +4005,7 @@ var renderCmd = defineCommand({
3851
4005
  const prices = decode(PriceMapCodec.decode(readJSON(priceResolution.path)), "prices");
3852
4006
  const template = readFileSync(templatePath, "utf-8");
3853
4007
  const testReport = args["test-report"] ? decode(TestSummaryCodec.decode(readJSON(args["test-report"])), "test report") : void 0;
4008
+ const clocDiff = args["cloc-diff"] ? loadClocDiff(args["cloc-diff"]) : void 0;
3854
4009
  const route = args.route || envelope.route || null;
3855
4010
  const isRound = isConvergenceRound(route, envelope.incomplete === true || isIncompleteFindings(findings)) && isReviewVerdict(findings.verdict);
3856
4011
  const threshold = parseConvergenceThreshold(args["convergence-threshold"]);
@@ -3868,10 +4023,12 @@ var renderCmd = defineCommand({
3868
4023
  route: args.route,
3869
4024
  effort: args.effort,
3870
4025
  testReport,
4026
+ clocDiff,
3871
4027
  convergenceThreshold: threshold,
3872
4028
  nitVisibilityFloor: parseNitVisibilityFloor(args["nit-visibility-floor"]),
3873
4029
  convergenceRound: isRound,
3874
- postedAt: formatUtc(/* @__PURE__ */ new Date())
4030
+ postedAt: formatUtc(/* @__PURE__ */ new Date()),
4031
+ pricedAt: /* @__PURE__ */ new Date()
3875
4032
  });
3876
4033
  process.stdout.write(output2);
3877
4034
  }
@@ -3936,7 +4093,11 @@ var costCmd = defineCommand({
3936
4093
  run: async ({ args }) => {
3937
4094
  const envelope = decode(ResultEnvelopeCodec.decode(readJSON(args.envelope)), "envelope");
3938
4095
  const prices = decode(PriceMapCodec.decode(readJSON(args.prices)), "prices");
3939
- const report = computeCost(envelope.models, prices);
4096
+ const report = computeCost(
4097
+ envelope.models,
4098
+ prices,
4099
+ parseInstant(envelope.generated_at) ?? /* @__PURE__ */ new Date()
4100
+ );
3940
4101
  process.stdout.write(JSON.stringify(report, null, 2));
3941
4102
  }
3942
4103
  });
@@ -3967,7 +4128,11 @@ var checkCostCmd = defineCommand({
3967
4128
  const usage = sumTranscriptUsage(tree.entries);
3968
4129
  const priceResolution = resolvePrices(args.prices);
3969
4130
  const prices = decode(PriceMapCodec.decode(readJSON(priceResolution.path)), "prices");
3970
- const report = computeCost(usage.models, prices);
4131
+ const report = computeCost(
4132
+ usage.models,
4133
+ prices,
4134
+ usage.lastTsMs !== null ? new Date(usage.lastTsMs) : /* @__PURE__ */ new Date()
4135
+ );
3971
4136
  process.stdout.write(
3972
4137
  `${JSON.stringify(
3973
4138
  {
@@ -4111,7 +4276,17 @@ var budgetHookCmd = defineCommand({
4111
4276
  const tree = transcriptPath ? readTranscriptTree(resolve$1(transcriptPath)) : void 0;
4112
4277
  const usage = tree ? sumTranscriptUsage(tree.entries) : void 0;
4113
4278
  const prices = args.prices ? tryReadPrices(args.prices) : null;
4114
- const spentUsd = prices !== null && usage ? computeCost(usage.models, prices).totalCostUSD : null;
4279
+ const spentUsd = prices !== null && usage ? (
4280
+ // Price at the transcript's last activity instant, not the wall clock (issue #170 review
4281
+ // r2). Silent warn: this budget-steering cost is recomputed on EVERY tool event, so a
4282
+ // misconfigured slot map would otherwise flood stderr; the final post's cost render warns.
4283
+ computeCost(
4284
+ usage.models,
4285
+ prices,
4286
+ usage.lastTsMs !== null ? new Date(usage.lastTsMs) : /* @__PURE__ */ new Date(),
4287
+ () => void 0
4288
+ ).totalCostUSD
4289
+ ) : null;
4115
4290
  const wallMs = args.wall ? parseWallMs(args.wall) : null;
4116
4291
  const output2 = evaluateBudgetHook(input, {
4117
4292
  spentUsd,
@@ -4312,7 +4487,6 @@ ${printableSchema(schemaPath)}
4312
4487
  }
4313
4488
  });
4314
4489
  var isSurfaceStampedDoc = (doc) => typeof doc === "object" && doc !== null && !Array.isArray(doc) && doc["schema_version"] === SURFACE_SCHEMA_VERSION;
4315
- var PIPELINE_STAMPED_FIELDS2 = /* @__PURE__ */ new Set(["scope_metastasis", "convergence"]);
4316
4490
  var withoutScopeMetastasis = (doc) => typeof doc === "object" && doc !== null && !Array.isArray(doc) ? Object.fromEntries(Object.entries(doc).filter(([key2]) => key2 !== "scope_metastasis")) : doc;
4317
4491
  var seedDraftCmd = defineCommand({
4318
4492
  meta: {
@@ -4457,12 +4631,12 @@ var seedDraftCmd = defineCommand({
4457
4631
  const schemaPath = args.schema ? resolve$1(args.schema) : schemaPathFor(kind, args["schema-version"]);
4458
4632
  const barePrior = typeof priorFindings === "object" && !Array.isArray(priorFindings) ? Object.fromEntries(
4459
4633
  Object.entries(priorFindings).filter(
4460
- ([key2]) => !PIPELINE_STAMPED_FIELDS2.has(key2)
4634
+ ([key2]) => !RECOVERABLE_OPTIONAL_FIELDS.has(key2)
4461
4635
  )
4462
4636
  ) : priorFindings;
4463
4637
  const accepts = (doc) => validateAgainstSchema(doc, schemaPath).valid && resolve("findings", doc).kind === "ok";
4464
4638
  const seedDoc = accepts(priorFindings) ? priorFindings : accepts(barePrior) ? (process.stderr.write(
4465
- `Note: the in-force schema rejects the carried scope_metastasis entry \u2014 seeding the prior without it (issue #150 review r2)
4639
+ `Note: the in-force schema rejects a carried recoverable field (scope_metastasis/convergence/change_size) \u2014 seeding the prior without it (issue #150 review r2 / #182 review r2)
4466
4640
  `
4467
4641
  ), barePrior) : null;
4468
4642
  if (seedDoc === null) return false;
@@ -4951,6 +5125,10 @@ var postCmd = defineCommand({
4951
5125
  type: "string",
4952
5126
  description: TEST_REPORT_DESCRIPTION
4953
5127
  },
5128
+ "cloc-diff": {
5129
+ type: "string",
5130
+ description: CLOC_DIFF_DESCRIPTION
5131
+ },
4954
5132
  "run-url": {
4955
5133
  type: "string",
4956
5134
  description: "Workflow run URL (transcript/traces), rendered as a link in the LLM Disclosure aside"
@@ -4984,11 +5162,13 @@ var postCmd = defineCommand({
4984
5162
  headBranch: args["head-branch"],
4985
5163
  effort: args.effort,
4986
5164
  testReportPath: args["test-report"],
5165
+ clocDiffPath: args["cloc-diff"],
4987
5166
  runUrl: args["run-url"],
4988
5167
  jsonUrl: args["json-url"],
4989
5168
  convergenceThreshold: parseConvergenceThreshold(args["convergence-threshold"]),
4990
5169
  nitVisibilityFloor: parseNitVisibilityFloor(args["nit-visibility-floor"]),
4991
- postedAt: formatUtc(/* @__PURE__ */ new Date())
5170
+ postedAt: formatUtc(/* @__PURE__ */ new Date()),
5171
+ pricedAt: /* @__PURE__ */ new Date()
4992
5172
  });
4993
5173
  }
4994
5174
  });