@usepipr/runtime 0.3.6 → 0.3.8

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.
@@ -272,7 +272,10 @@ const piprConfigSchema = z.strictObject({
272
272
  providers: z.array(providerConfigSchema).min(1),
273
273
  publication: z.strictObject({
274
274
  maxInlineComments: z.number().int().min(0).max(50).optional(),
275
- autoResolve: autoResolveConfigSchema
275
+ autoResolve: autoResolveConfigSchema,
276
+ showHeader: z.boolean().default(true),
277
+ showFooter: z.boolean().default(true),
278
+ showStats: z.boolean().default(true)
276
279
  }),
277
280
  limits: z.strictObject({
278
281
  timeoutSeconds: z.number().int().positive().max(3600).optional(),
@@ -526,7 +529,7 @@ function compareStableSemver(left, right) {
526
529
  }
527
530
  //#endregion
528
531
  //#region src/shared/version.ts
529
- const runtimeVersion = "0.3.6";
532
+ const runtimeVersion = "0.3.8";
530
533
  //#endregion
531
534
  //#region src/config/version-compat.ts
532
535
  async function resolveConfigVersionCompatibility(options) {
@@ -776,7 +779,10 @@ function planToRuntimeSettings(plan, options) {
776
779
  providers,
777
780
  publication: {
778
781
  maxInlineComments: plan.publication.maxInlineComments,
779
- autoResolve: normalizeAutoResolveConfig(plan.publication.autoResolve, defaultProvider.id)
782
+ autoResolve: normalizeAutoResolveConfig(plan.publication.autoResolve, defaultProvider.id),
783
+ showHeader: plan.publication.showHeader ?? true,
784
+ showFooter: plan.publication.showFooter ?? true,
785
+ showStats: plan.publication.showStats ?? true
780
786
  },
781
787
  limits: plan.limits
782
788
  },
@@ -3544,8 +3550,8 @@ function isOfficialInitRecipeId(recipe) {
3544
3550
  //#endregion
3545
3551
  //#region src/config/init.ts
3546
3552
  const supportedOfficialInitAdapters = ["github"];
3547
- const defaultWorkflowActionRef = "somus/pipr@v0.3.6";
3548
- const defaultSdkVersion = "0.3.6";
3553
+ const defaultWorkflowActionRef = "somus/pipr@v0.3.8";
3554
+ const defaultSdkVersion = "0.3.8";
3549
3555
  function resolveOfficialInitAdapters(adapters) {
3550
3556
  if (adapters === void 0) return [...supportedOfficialInitAdapters];
3551
3557
  if (adapters.length === 0) return [];
@@ -4746,6 +4752,24 @@ const ignoredWorkspacePaths = /* @__PURE__ */ new Set([
4746
4752
  ".fallow",
4747
4753
  "coverage"
4748
4754
  ]);
4755
+ const eventRecordSchema = z.record(z.string(), z.unknown());
4756
+ const tokenCountSchema = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
4757
+ const assistantMessageEventSchema = z.looseObject({
4758
+ type: z.literal("message_end"),
4759
+ message: z.looseObject({
4760
+ role: z.literal("assistant"),
4761
+ model: z.string().min(1).optional(),
4762
+ responseModel: z.string().min(1).optional()
4763
+ })
4764
+ });
4765
+ const assistantUsageMessageSchema = z.looseObject({
4766
+ role: z.literal("assistant"),
4767
+ usage: z.looseObject({
4768
+ input: tokenCountSchema,
4769
+ output: tokenCountSchema,
4770
+ cost: z.looseObject({ total: z.number().nonnegative() })
4771
+ })
4772
+ });
4749
4773
  async function runPi(options) {
4750
4774
  const started = Date.now();
4751
4775
  const sandbox = await createPiRunSandbox(options.workspace);
@@ -4768,10 +4792,13 @@ async function runPi(options) {
4768
4792
  started,
4769
4793
  timeoutSeconds: options.timeoutSeconds
4770
4794
  });
4771
- return result.exitCode === 0 ? {
4795
+ const events = parsePiJsonEvents(result.stdout);
4796
+ return {
4772
4797
  ...result,
4773
- stdout: extractAssistantTextFromJsonEvents(result.stdout) ?? result.stdout
4774
- } : result;
4798
+ ...events?.models.length ? { models: events.models } : {},
4799
+ ...events?.usage ? { usage: events.usage } : {},
4800
+ stdout: result.exitCode === 0 ? events?.assistantText ?? result.stdout : result.stdout
4801
+ };
4775
4802
  } finally {
4776
4803
  await preparedTools?.custom?.close();
4777
4804
  await chmodRecursive(sandbox.root, 493);
@@ -4893,19 +4920,69 @@ async function chmodRecursive(target, mode) {
4893
4920
  const entries = await readdir(target, { withFileTypes: true });
4894
4921
  for (const entry of entries) await chmodRecursive(path.join(target, entry.name), mode);
4895
4922
  }
4896
- function extractAssistantTextFromJsonEvents(stdout) {
4923
+ function parsePiJsonEvents(stdout) {
4897
4924
  const events = [];
4898
4925
  for (const line of stdout.split(/\r?\n/).map((item) => item.trim()).filter(Boolean)) try {
4899
4926
  const value = JSON.parse(line);
4900
- if (!isPlainObject(value)) return;
4901
- events.push(value);
4927
+ const parsed = eventRecordSchema.safeParse(value);
4928
+ if (!parsed.success) return;
4929
+ events.push(parsed.data);
4902
4930
  } catch {
4903
4931
  return;
4904
4932
  }
4905
4933
  if (!events.some((event) => typeof event.type === "string")) return;
4906
- let text;
4907
- for (const event of events) text = assistantTextFromEvent(event) ?? text;
4908
- return text;
4934
+ let assistantText;
4935
+ for (const event of events) assistantText = assistantTextFromEvent(event) ?? assistantText;
4936
+ const assistantMessages = assistantMessagesFromEvents(events);
4937
+ const models = assistantModels(assistantMessages);
4938
+ const usage = assistantUsage(assistantMessages);
4939
+ return {
4940
+ ...assistantText !== void 0 ? { assistantText } : {},
4941
+ models,
4942
+ ...usage ? { usage } : {}
4943
+ };
4944
+ }
4945
+ function assistantMessagesFromEvents(events) {
4946
+ return events.flatMap((event) => {
4947
+ const parsed = assistantMessageEventSchema.safeParse(event);
4948
+ return parsed.success ? [parsed.data.message] : [];
4949
+ });
4950
+ }
4951
+ function assistantModels(messages) {
4952
+ const models = [];
4953
+ for (const message of messages) {
4954
+ const model = message.responseModel ?? message.model;
4955
+ if (model && !models.includes(model)) models.push(model);
4956
+ }
4957
+ return models;
4958
+ }
4959
+ function assistantUsage(messages) {
4960
+ const usageMessages = messages.flatMap((message) => {
4961
+ const parsed = assistantUsageMessageSchema.safeParse(message);
4962
+ return parsed.success ? [parsed.data] : [];
4963
+ });
4964
+ if (usageMessages.length === 0) return;
4965
+ let inputTokens = 0;
4966
+ let outputTokens = 0;
4967
+ let costUsd = 0;
4968
+ let partial = usageMessages.length !== messages.length;
4969
+ for (const message of usageMessages) {
4970
+ const nextInputTokens = inputTokens + message.usage.input;
4971
+ if (Number.isSafeInteger(nextInputTokens)) inputTokens = nextInputTokens;
4972
+ else partial = true;
4973
+ const nextOutputTokens = outputTokens + message.usage.output;
4974
+ if (Number.isSafeInteger(nextOutputTokens)) outputTokens = nextOutputTokens;
4975
+ else partial = true;
4976
+ const nextCostUsd = costUsd + message.usage.cost.total;
4977
+ if (Number.isFinite(nextCostUsd)) costUsd = nextCostUsd;
4978
+ else partial = true;
4979
+ }
4980
+ return {
4981
+ status: partial ? "partial" : "complete",
4982
+ inputTokens,
4983
+ outputTokens,
4984
+ costUsd
4985
+ };
4909
4986
  }
4910
4987
  function assistantTextFromEvent(event) {
4911
4988
  if (event.type === "message_end" || event.type === "turn_end") return assistantMessageText(event.message);
@@ -5521,16 +5598,27 @@ async function runPiForPrompt(options, provider, prompt) {
5521
5598
  const customTools = customToolsForRun(options);
5522
5599
  const timeoutSeconds = promptTimeoutSeconds(options);
5523
5600
  logPiStart(options, provider, prompt, builtinTools, runtimeTools, customTools);
5524
- const result = await (options.runtime.piRunner ?? runPi)({
5525
- workspace: options.runtime.workspace,
5526
- provider,
5527
- prompt,
5528
- env: options.runtime.env,
5529
- piExecutable: options.runtime.piExecutable,
5530
- builtinTools,
5531
- runtimeTools,
5532
- customTools,
5533
- timeoutSeconds
5601
+ let result;
5602
+ try {
5603
+ result = await (options.runtime.piRunner ?? runPi)({
5604
+ workspace: options.runtime.workspace,
5605
+ provider,
5606
+ prompt,
5607
+ env: options.runtime.env,
5608
+ piExecutable: options.runtime.piExecutable,
5609
+ builtinTools,
5610
+ runtimeTools,
5611
+ customTools,
5612
+ timeoutSeconds
5613
+ });
5614
+ } catch (error) {
5615
+ options.runtime.piRunSink?.({ models: [provider.model] });
5616
+ throw error;
5617
+ }
5618
+ const reportedModels = result.models?.map((model) => model.trim()).filter(Boolean);
5619
+ options.runtime.piRunSink?.({
5620
+ models: reportedModels?.length ? reportedModels : [provider.model],
5621
+ ...result.usage ? { usage: result.usage } : {}
5534
5622
  });
5535
5623
  logPiResult(options, provider, result, timeoutSeconds);
5536
5624
  assertSuccessfulPiResult(result, options.runtime.log);
@@ -5674,8 +5762,63 @@ const mainCommentTitles = /* @__PURE__ */ new Set([
5674
5762
  "# Pipr Review",
5675
5763
  mainCommentTitle
5676
5764
  ]);
5765
+ const mainCommentHeaderHiddenMarker = "<!-- pipr:header:hidden -->";
5766
+ const mainCommentFooterHiddenMarker = "<!-- pipr:footer:hidden -->";
5767
+ const reviewStatsStartMarker = "<!-- pipr:stats:start -->";
5768
+ const reviewStatsEndMarker = "<!-- pipr:stats:end -->";
5769
+ const reviewStatsHiddenMarker = "<!-- pipr:stats:hidden -->";
5677
5770
  const escapedRepositoryUrl = piprRepositoryUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5678
5771
  const mainCommentAttributionPattern = new RegExp(`^<sub>Review generated by \\[Pipr\\]\\(${escapedRepositoryUrl}\\) for commit \`[^\`]+\`\\.(?: Config SDK \\d+\\.\\d+\\.\\d+ is behind (?:Pipr \\d+\\.\\d+\\.\\d+|\\[Pipr \\d+\\.\\d+\\.\\d+\\]\\(${escapedRepositoryUrl}/releases/tag/v\\d+\\.\\d+\\.\\d+\\))\\.)?</sub>$`);
5772
+ const maxReviewStatsModelLength = 200;
5773
+ const credentialLikeModelPattern = /(?:(?:AKIA|ASIA)[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35}|(?:sk|rk)_live_[A-Za-z0-9]{16,}|sk-(?:proj-)?[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{16,}|glpat-[A-Za-z0-9_-]{16,}|npm_[A-Za-z0-9]{16,}|xox[baprs]-[A-Za-z0-9-]{16,})/i;
5774
+ const highEntropyModelSegmentPattern = /[A-Za-z0-9]{24,}/;
5775
+ const reviewStatsModelSchema = z.string().min(1).max(maxReviewStatsModelLength).transform((model) => sanitizeReviewStatsModel(model) ?? "[invalid model]");
5776
+ const reviewStatsSchema = z.strictObject({
5777
+ models: z.array(reviewStatsModelSchema).min(1).max(20),
5778
+ agentRuns: z.number().int().positive(),
5779
+ durationMs: z.number().int().nonnegative(),
5780
+ inputTokens: z.number().int().nonnegative(),
5781
+ outputTokens: z.number().int().nonnegative(),
5782
+ costUsd: z.number().nonnegative(),
5783
+ usageStatus: z.enum([
5784
+ "complete",
5785
+ "partial",
5786
+ "unavailable"
5787
+ ])
5788
+ });
5789
+ function accumulateReviewStats(prior, current) {
5790
+ if (!prior) return current;
5791
+ if (!current) return prior;
5792
+ const inputTokens = addUsageTotal(prior.inputTokens, current.inputTokens, Number.isSafeInteger);
5793
+ const outputTokens = addUsageTotal(prior.outputTokens, current.outputTokens, Number.isSafeInteger);
5794
+ const costUsd = addUsageTotal(prior.costUsd, current.costUsd, Number.isFinite);
5795
+ const usageStatus = inputTokens.complete && outputTokens.complete && costUsd.complete && prior.usageStatus === current.usageStatus ? prior.usageStatus : "partial";
5796
+ return {
5797
+ models: [.../* @__PURE__ */ new Set([...prior.models, ...current.models])].slice(0, 20),
5798
+ agentRuns: Math.min(Number.MAX_SAFE_INTEGER, prior.agentRuns + current.agentRuns),
5799
+ durationMs: Math.min(Number.MAX_SAFE_INTEGER, prior.durationMs + current.durationMs),
5800
+ inputTokens: inputTokens.total,
5801
+ outputTokens: outputTokens.total,
5802
+ costUsd: costUsd.total,
5803
+ usageStatus
5804
+ };
5805
+ }
5806
+ function addUsageTotal(prior, current, isValid) {
5807
+ const total = prior + current;
5808
+ return isValid(total) && total >= 0 ? {
5809
+ total,
5810
+ complete: true
5811
+ } : {
5812
+ total: prior,
5813
+ complete: false
5814
+ };
5815
+ }
5816
+ function sanitizeReviewStatsModel(model) {
5817
+ const normalized = model.replace(/\s+/g, " ").trim();
5818
+ if (credentialLikeModelPattern.test(normalized) || highEntropyModelSegmentPattern.test(normalized)) return "[redacted credential]";
5819
+ const sanitized = redactPotentialSecrets(normalized);
5820
+ return sanitized ? sanitized.slice(0, maxReviewStatsModelLength) : void 0;
5821
+ }
5679
5822
  //#endregion
5680
5823
  //#region src/review/prior-state.ts
5681
5824
  const mainCommentMarker = "pipr:main-comment";
@@ -5701,7 +5844,8 @@ const priorReviewStateSchema = z.strictObject({
5701
5844
  version: z.literal(1),
5702
5845
  reviewedHeadSha: z.string().min(1),
5703
5846
  selectedTasks: z.array(z.string().min(1)),
5704
- findings: z.array(priorFindingRecordSchema)
5847
+ findings: z.array(priorFindingRecordSchema),
5848
+ stats: reviewStatsSchema.optional()
5705
5849
  });
5706
5850
  function buildPriorReviewState(options) {
5707
5851
  const scopedPriorState = priorReviewStateForSelectedTasks(options.priorState, options.selectedTasks);
@@ -5709,6 +5853,7 @@ function buildPriorReviewState(options) {
5709
5853
  const nextFindings = /* @__PURE__ */ new Map();
5710
5854
  const currentFindingIds = /* @__PURE__ */ new Set();
5711
5855
  const usedPriorIds = /* @__PURE__ */ new Set();
5856
+ const stats = accumulateReviewStats(scopedPriorState?.stats, options.stats);
5712
5857
  for (const finding of options.findings) {
5713
5858
  const id = selectFindingId({
5714
5859
  finding,
@@ -5741,7 +5886,8 @@ function buildPriorReviewState(options) {
5741
5886
  version: 1,
5742
5887
  reviewedHeadSha: options.reviewedHeadSha,
5743
5888
  selectedTasks: options.selectedTasks,
5744
- findings: cappedFindings([...nextFindings.values()], currentFindingIds)
5889
+ findings: cappedFindings([...nextFindings.values()], currentFindingIds),
5890
+ ...stats ? { stats } : {}
5745
5891
  };
5746
5892
  }
5747
5893
  function resolvePriorFindings(state, findingIds) {
@@ -5990,7 +6136,8 @@ const publicationMetadataSchema = z.strictObject({
5990
6136
  failedTasks: z.array(z.string().min(1)),
5991
6137
  validFindings: z.number().int().min(0),
5992
6138
  droppedFindings: z.number().int().min(0),
5993
- cappedInlineFindings: z.number().int().min(0)
6139
+ cappedInlineFindings: z.number().int().min(0),
6140
+ stats: reviewStatsSchema.optional()
5994
6141
  });
5995
6142
  const publicationPlanSchema = z.strictObject({
5996
6143
  mainComment: z.string().min(1),
@@ -6017,7 +6164,10 @@ function buildPublicationPlan(options) {
6017
6164
  event: options.event,
6018
6165
  reviewState,
6019
6166
  main: options.main,
6020
- metadata
6167
+ metadata,
6168
+ showHeader: options.showHeader ?? true,
6169
+ showFooter: options.showFooter ?? true,
6170
+ showStats: options.showStats ?? true
6021
6171
  }),
6022
6172
  mainMarker: mainCommentMarker,
6023
6173
  changeNumber: options.event.change.number,
@@ -6101,15 +6251,59 @@ function renderMainComment(options) {
6101
6251
  reviewState: options.reviewState
6102
6252
  }),
6103
6253
  "",
6104
- mainCommentTitle,
6105
- "",
6254
+ ...!options.showHeader ? [mainCommentHeaderHiddenMarker, ""] : [],
6255
+ ...options.showHeader ? [mainCommentTitle, ""] : [],
6106
6256
  ...options.metadata.validFindings > 0 ? [`**Findings:** ${options.metadata.validFindings}`, ""] : [],
6107
6257
  redactPotentialSecrets(options.main),
6108
6258
  "",
6109
- renderMainCommentAttribution(options.metadata),
6110
- ""
6259
+ ...!options.showStats || !options.metadata.stats ? [reviewStatsHiddenMarker, ""] : [],
6260
+ ...options.showStats && options.metadata.stats ? [renderReviewStats(options.metadata.stats), ""] : [],
6261
+ ...options.showFooter ? [renderMainCommentAttribution(options.metadata), ""] : [mainCommentFooterHiddenMarker, ""]
6111
6262
  ].join("\n");
6112
6263
  }
6264
+ function renderReviewStats(stats) {
6265
+ const usageSuffix = stats.usageStatus === "partial" ? " (reported)" : "";
6266
+ const usageUnavailable = stats.usageStatus === "unavailable";
6267
+ return [
6268
+ reviewStatsStartMarker,
6269
+ "<details>",
6270
+ "<summary>Review stats</summary>",
6271
+ "",
6272
+ "| Metric | Total |",
6273
+ "| --- | ---: |",
6274
+ `| Models | ${stats.models.map(formatModel).join(", ")} |`,
6275
+ `| Agent runs | ${stats.agentRuns} |`,
6276
+ `| Elapsed | ${formatDuration(stats.durationMs)} |`,
6277
+ `| Input tokens | ${usageUnavailable ? "Unavailable" : `${formatInteger(stats.inputTokens)}${usageSuffix}`} |`,
6278
+ `| Output tokens | ${usageUnavailable ? "Unavailable" : `${formatInteger(stats.outputTokens)}${usageSuffix}`} |`,
6279
+ `| Cost (USD) | ${usageUnavailable ? "Unavailable" : `${formatCost(stats.costUsd)}${usageSuffix}`} |`,
6280
+ "",
6281
+ "</details>",
6282
+ reviewStatsEndMarker
6283
+ ].join("\n");
6284
+ }
6285
+ function formatModel(model) {
6286
+ return `<code>${model.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\|/g, "&#124;")}</code>`;
6287
+ }
6288
+ function formatInteger(value) {
6289
+ return value.toLocaleString("en-US");
6290
+ }
6291
+ function formatDuration(durationMs) {
6292
+ if (durationMs < 1e3) return `${durationMs}ms`;
6293
+ const totalSeconds = durationMs / 1e3;
6294
+ if (totalSeconds < 60) return `${formatTenths(totalSeconds)}s`;
6295
+ const minutes = Math.floor(totalSeconds / 60);
6296
+ return `${minutes}m ${formatTenths(totalSeconds - minutes * 60)}s`;
6297
+ }
6298
+ function formatTenths(value) {
6299
+ return value.toFixed(1).replace(/\.0$/, "");
6300
+ }
6301
+ function formatCost(costUsd) {
6302
+ if (costUsd === 0) return "$0.00";
6303
+ if (costUsd < 1e-4) return `$${costUsd.toFixed(6)}`;
6304
+ if (costUsd < .01) return `$${costUsd.toFixed(4)}`;
6305
+ return `$${costUsd.toFixed(2)}`;
6306
+ }
6113
6307
  function renderMainCommentAttribution(metadata) {
6114
6308
  const configNotice = configVersionNotice(metadata);
6115
6309
  return `<sub>Review generated by [Pipr](${piprRepositoryUrl}) for commit \`${metadata.reviewedHeadSha.slice(0, 7)}\`.${configNotice}</sub>`;
@@ -6155,7 +6349,8 @@ function buildCommentPublishingPlan(options) {
6155
6349
  priorState: options.priorReviewState,
6156
6350
  findings: publishableInlineFindings.map((item) => item.finding),
6157
6351
  reviewedHeadSha: options.event.change.head.sha,
6158
- selectedTasks: options.metadata.selectedTasks
6352
+ selectedTasks: options.metadata.selectedTasks,
6353
+ stats: options.metadata.stats
6159
6354
  });
6160
6355
  const inlineCommentDrafts = prepareInlinePublicationItemsForPublishableFindings({
6161
6356
  publishableFindings: publishableInlineFindings,
@@ -6167,7 +6362,13 @@ function buildCommentPublishingPlan(options) {
6167
6362
  main: options.main,
6168
6363
  inlineItems: inlineCommentDrafts,
6169
6364
  maxInlineComments: options.maxInlineComments,
6170
- metadata: options.metadata,
6365
+ showHeader: options.showHeader,
6366
+ showFooter: options.showFooter,
6367
+ showStats: options.showStats,
6368
+ metadata: {
6369
+ ...options.metadata,
6370
+ ...reviewState.stats ? { stats: reviewState.stats } : {}
6371
+ },
6171
6372
  reviewState,
6172
6373
  threadActions: options.threadActions
6173
6374
  });
@@ -6266,7 +6467,8 @@ async function runInternalVerifier(options) {
6266
6467
  piExecutable: options.piExecutable,
6267
6468
  piRunner: options.piRunner,
6268
6469
  runId: options.runId,
6269
- log: options.log
6470
+ log: options.log,
6471
+ ...options.piRunSink ? { piRunSink: options.piRunSink } : {}
6270
6472
  }
6271
6473
  });
6272
6474
  return applyVerifierOutput(options, candidates, verifierOutputSchema.parse(result.value), result.providerModels);
@@ -6438,6 +6640,20 @@ function boundedVerifierText(value) {
6438
6640
  //#endregion
6439
6641
  //#region src/review/task/task-output.ts
6440
6642
  const agentInlineFindingsOutputSchema = z.custom((value) => z.looseObject({ inlineFindings: z.array(reviewFindingSchema) }).safeParse(value).success);
6643
+ const generatedReviewStatsShape = [
6644
+ /^$/,
6645
+ /^\| Metric \| Total \|$/,
6646
+ /^\| --- \| ---: \|$/,
6647
+ /^\| Models \| .+ \|$/,
6648
+ /^\| Agent runs \| \d+ \|$/,
6649
+ /^\| Elapsed \| .+ \|$/,
6650
+ /^\| Input tokens \| (?:Unavailable|[\d,]+(?: \(reported\))?) \|$/,
6651
+ /^\| Output tokens \| (?:Unavailable|[\d,]+(?: \(reported\))?) \|$/,
6652
+ /^\| Cost \(USD\) \| (?:Unavailable|\$\d+(?:\.\d+)?(?:e[+-]?\d+)?(?: \(reported\))?) \|$/,
6653
+ /^$/,
6654
+ /^<\/details>$/,
6655
+ /^<!-- pipr:stats:end -->$/
6656
+ ];
6441
6657
  function createOutputState() {
6442
6658
  return {
6443
6659
  findings: [],
@@ -6457,6 +6673,66 @@ function mergeTaskOutputs(results) {
6457
6673
  }
6458
6674
  return merged;
6459
6675
  }
6676
+ function reviewStatsForRuns(runs, durationMs) {
6677
+ if (runs.length === 0) return;
6678
+ const usage = aggregateReviewUsage(runs);
6679
+ return {
6680
+ models: collectReviewModels(runs),
6681
+ agentRuns: runs.length,
6682
+ durationMs,
6683
+ inputTokens: usage.inputTokens,
6684
+ outputTokens: usage.outputTokens,
6685
+ costUsd: usage.costUsd,
6686
+ usageStatus: usage.status
6687
+ };
6688
+ }
6689
+ function collectReviewModels(runs) {
6690
+ const models = [];
6691
+ for (const model of runs.flatMap((run) => run.models)) {
6692
+ const sanitized = sanitizeReviewStatsModel(model);
6693
+ if (sanitized && models.length < 20 && !models.includes(sanitized)) models.push(sanitized);
6694
+ }
6695
+ return models.length > 0 ? models : ["[invalid model]"];
6696
+ }
6697
+ function aggregateReviewUsage(runs) {
6698
+ let inputTokens = 0;
6699
+ let outputTokens = 0;
6700
+ let costUsd = 0;
6701
+ let reportedRuns = 0;
6702
+ let partialUsage = false;
6703
+ for (const run of runs) {
6704
+ if (!run.usage) continue;
6705
+ reportedRuns += 1;
6706
+ const input = addReportedUsage(inputTokens, run.usage.inputTokens, Number.isSafeInteger);
6707
+ const output = addReportedUsage(outputTokens, run.usage.outputTokens, Number.isSafeInteger);
6708
+ const cost = addReportedUsage(costUsd, run.usage.costUsd, Number.isFinite);
6709
+ inputTokens = input.total;
6710
+ outputTokens = output.total;
6711
+ costUsd = cost.total;
6712
+ const sumsComplete = [
6713
+ input,
6714
+ output,
6715
+ cost
6716
+ ].every((sum) => sum.complete);
6717
+ partialUsage ||= run.usage.status === "partial" || !sumsComplete;
6718
+ }
6719
+ return {
6720
+ inputTokens,
6721
+ outputTokens,
6722
+ costUsd,
6723
+ status: reportedRuns === 0 ? "unavailable" : reportedRuns < runs.length || partialUsage ? "partial" : "complete"
6724
+ };
6725
+ }
6726
+ function addReportedUsage(current, reported, isValid) {
6727
+ const next = current + reported;
6728
+ return isValid(next) ? {
6729
+ total: next,
6730
+ complete: true
6731
+ } : {
6732
+ total: current,
6733
+ complete: false
6734
+ };
6735
+ }
6460
6736
  function mergeCommentContribution(merged, comment) {
6461
6737
  if (!comment) return;
6462
6738
  assertOutputContributionAllowed(merged, "comment", comment.taskName, (existing, next) => `ctx.comment(...) may be called once per selected run; received comments from '${existing}' and '${next}'`);
@@ -6534,12 +6810,52 @@ function priorReviewForTask(priorMainComment, priorReviewState) {
6534
6810
  };
6535
6811
  }
6536
6812
  function visibleMainComment(body) {
6537
- const lines = body.split("\n").filter((line) => !line.startsWith("<!-- pipr:main-comment ") && !mainCommentAttributionPattern.test(line));
6813
+ const sourceLines = body.split("\n");
6814
+ const envelope = parseGeneratedMainCommentEnvelope(sourceLines);
6815
+ const lines = sourceLines.filter((_line, index) => {
6816
+ return !(envelope.statsRange && index >= envelope.statsRange.start && index <= envelope.statsRange.end) && index !== envelope.mainMarkerIndex && index !== envelope.headerMarkerIndex && index !== envelope.statsMarkerIndex && index !== envelope.footerIndex;
6817
+ });
6538
6818
  while (lines[0] === "") lines.shift();
6539
- if (lines[0] && mainCommentTitles.has(lines[0])) lines.shift();
6819
+ if (envelope.headerMarkerIndex < 0 && lines[0] && mainCommentTitles.has(lines[0])) lines.shift();
6540
6820
  while (lines[0] === "") lines.shift();
6541
6821
  return lines.join("\n").trim();
6542
6822
  }
6823
+ function parseGeneratedMainCommentEnvelope(lines) {
6824
+ const mainMarkerIndex = lines.findIndex((line) => line.startsWith("<!-- pipr:main-comment "));
6825
+ const headerCandidateOffset = lines.slice(mainMarkerIndex + 1).findIndex((line) => line !== "");
6826
+ const headerCandidateIndex = mainMarkerIndex + 1 + headerCandidateOffset;
6827
+ const headerMarkerIndex = mainMarkerIndex >= 0 && headerCandidateOffset >= 0 && lines[headerCandidateIndex] === "<!-- pipr:header:hidden -->" ? headerCandidateIndex : -1;
6828
+ const lastLineIndex = lines.findLastIndex((line) => line !== "");
6829
+ const lastLine = lines[lastLineIndex] ?? "";
6830
+ const footerIndex = lastLine === "<!-- pipr:footer:hidden -->" || mainCommentAttributionPattern.test(lastLine) ? lastLineIndex : -1;
6831
+ const lastContentIndex = lines.slice(0, footerIndex < 0 ? lines.length : footerIndex).findLastIndex((line) => line !== "");
6832
+ return {
6833
+ mainMarkerIndex,
6834
+ headerMarkerIndex,
6835
+ statsMarkerIndex: lines[lastContentIndex] === "<!-- pipr:stats:hidden -->" ? lastContentIndex : -1,
6836
+ statsRange: generatedReviewStatsRange(lines, footerIndex),
6837
+ footerIndex
6838
+ };
6839
+ }
6840
+ function generatedReviewStatsRange(lines, generatedFooterIndex) {
6841
+ const end = lines.slice(0, generatedFooterIndex < 0 ? lines.length : generatedFooterIndex).findLastIndex((line) => line !== "");
6842
+ if (end < 0) return;
6843
+ if (lines[end] !== "<!-- pipr:stats:end -->") return;
6844
+ if (lines[end - 1] !== "</details>") return;
6845
+ const start = lines.lastIndexOf(reviewStatsStartMarker, end - 2);
6846
+ if (start < 0) return;
6847
+ if (lines[start + 1] !== "<details>") return;
6848
+ if (lines[start + 2] !== "<summary>Review stats</summary>") return;
6849
+ if (!matchesGeneratedReviewStatsShape(lines, start, end)) return;
6850
+ return {
6851
+ start,
6852
+ end
6853
+ };
6854
+ }
6855
+ function matchesGeneratedReviewStatsShape(lines, start, end) {
6856
+ const generatedShape = lines.slice(start + 3, end + 1);
6857
+ return generatedShape.length === generatedReviewStatsShape.length && generatedReviewStatsShape.every((pattern, index) => pattern.test(generatedShape[index] ?? ""));
6858
+ }
6543
6859
  function collectInlineFindings(state, findings) {
6544
6860
  if (!findings) return;
6545
6861
  const arrayScope = state.findingScopes.get(findings);
@@ -6563,6 +6879,7 @@ function collectedReview(output, summaryBody) {
6563
6879
  //#region src/review/task/task-runtime.ts
6564
6880
  const genericTaskFailureSummary = "Task failed; see logs for details.";
6565
6881
  async function runTaskRuntime(options) {
6882
+ const runtimeStarted = Date.now();
6566
6883
  const config = parsePiprConfig(options.config);
6567
6884
  const provider = options.providerOverride ? parseProviderConfig(options.providerOverride) : resolveProvider(config, config.defaultProvider);
6568
6885
  const diffManifest = parseDiffManifest((options.diffManifestBuilder ?? buildDiffManifest)({
@@ -6615,11 +6932,15 @@ async function runTaskRuntime(options) {
6615
6932
  const loadedPriorReviewState = options.priorReviewState ?? await options.loadPriorReviewState?.();
6616
6933
  const priorMainComment = options.priorMainComment ?? await options.loadPriorMainComment?.();
6617
6934
  const priorReviewState = priorReviewStateForSelectedTasks(loadedPriorReviewState, selectedTasks);
6935
+ const piRuns = [];
6618
6936
  const runtimeOptions = {
6619
6937
  ...options,
6620
6938
  priorReviewState,
6621
6939
  priorMainComment,
6622
- runId
6940
+ runId,
6941
+ piRunSink(run) {
6942
+ piRuns.push(run);
6943
+ }
6623
6944
  };
6624
6945
  const manifestCache = /* @__PURE__ */ new Map();
6625
6946
  const taskResults = await Promise.all(tasks.map(async (task, taskOrder) => {
@@ -6703,14 +7024,19 @@ async function runTaskRuntime(options) {
6703
7024
  provider,
6704
7025
  diffManifest,
6705
7026
  priorReviewState,
6706
- runId
7027
+ runId,
7028
+ piRunSink: runtimeOptions.piRunSink
6707
7029
  });
7030
+ const stats = reviewStatsForRuns(piRuns, Date.now() - runtimeStarted);
6708
7031
  const publishing = buildCommentPublishingPlan({
6709
7032
  event: options.event,
6710
7033
  main,
6711
7034
  validated,
6712
7035
  manifest: diffManifest,
6713
7036
  maxInlineComments: config.publication.maxInlineComments,
7037
+ showHeader: config.publication.showHeader,
7038
+ showFooter: config.publication.showFooter,
7039
+ showStats: config.publication.showStats,
6714
7040
  priorReviewState: verifier.priorReviewState,
6715
7041
  threadActions: verifier.threadActions,
6716
7042
  metadata: {
@@ -6723,7 +7049,8 @@ async function runTaskRuntime(options) {
6723
7049
  selectedTasks,
6724
7050
  failedTasks: [],
6725
7051
  validFindings: validated.validFindings.length,
6726
- droppedFindings: validated.droppedFindings.length
7052
+ droppedFindings: validated.droppedFindings.length,
7053
+ ...stats ? { stats } : {}
6727
7054
  }
6728
7055
  });
6729
7056
  const publicationPlan = publishing.publicationPlan;
@@ -6782,7 +7109,8 @@ async function runSynchronizeVerifier(options) {
6782
7109
  priorReviewState: options.priorReviewState,
6783
7110
  threadContexts: await options.options.loadInlineThreadContexts?.() ?? [],
6784
7111
  mode: { kind: "synchronize" },
6785
- runId: options.runId
7112
+ runId: options.runId,
7113
+ piRunSink: options.piRunSink
6786
7114
  });
6787
7115
  }
6788
7116
  function createTaskContext(options) {
@@ -6843,7 +7171,8 @@ function createTaskContext(options) {
6843
7171
  runtime: {
6844
7172
  ...options,
6845
7173
  taskContext,
6846
- runId: options.runId
7174
+ runId: options.runId,
7175
+ piRunSink: options.piRunSink
6847
7176
  }
6848
7177
  });
6849
7178
  options.output.providerModels.push(...result.providerModels);
@@ -6904,6 +7233,9 @@ function skippedTaskRuntimeResult(options) {
6904
7233
  validated,
6905
7234
  manifest: options.diffManifest,
6906
7235
  maxInlineComments: options.config.publication.maxInlineComments,
7236
+ showHeader: options.config.publication.showHeader,
7237
+ showFooter: options.config.publication.showFooter,
7238
+ showStats: options.config.publication.showStats,
6907
7239
  metadata: {
6908
7240
  runtimeVersion,
6909
7241
  configVersion: options.versionCompatibility?.configVersion,
@@ -9167,4 +9499,4 @@ async function runActionCommandWithDependencies(options) {
9167
9499
  //#endregion
9168
9500
  export { runInspectCommand as a, PublicationError as c, supportedOfficialInitRecipes as d, piBuiltinToolNames as f, piThinkingLevels as h, runInitCommand as i, supportedOfficialInitAdapters as l, piRequiredCliFlags as m, runActionCommandWithDependencies as n, runLocalReviewCommand as o, piReadOnlyToolNames as p, runDryRunCommand as r, runValidateCommand as s, runActionCommand as t, listOfficialInitRecipes as u };
9169
9501
 
9170
- //# sourceMappingURL=commands-Btw_Ummi.mjs.map
9502
+ //# sourceMappingURL=commands-RY37Y0rG.mjs.map