@usepipr/runtime 0.4.3 → 0.5.0

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.
Files changed (32) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +8 -5
  3. package/dist/{commands-DA0Ij4Di.d.mts → commands-CF59DZXx.d.mts} +2 -2
  4. package/dist/{commands-DA0Ij4Di.d.mts.map → commands-CF59DZXx.d.mts.map} +1 -1
  5. package/dist/{commands-B9qNW4pk.mjs → commands-DgZU7tf7.mjs} +474 -334
  6. package/dist/commands-DgZU7tf7.mjs.map +1 -0
  7. package/dist/index.d.mts +16 -3
  8. package/dist/index.d.mts.map +1 -1
  9. package/dist/index.mjs +117 -15
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/internal/action-result.d.mts +4 -2
  12. package/dist/internal/action-result.d.mts.map +1 -1
  13. package/dist/internal/action-result.mjs +28 -12
  14. package/dist/internal/action-result.mjs.map +1 -1
  15. package/dist/internal/docs.mjs +1 -1
  16. package/dist/internal/pipr-result.d.mts +24 -0
  17. package/dist/internal/pipr-result.d.mts.map +1 -0
  18. package/dist/internal/pipr-result.mjs +126 -0
  19. package/dist/internal/pipr-result.mjs.map +1 -0
  20. package/dist/internal/testing.d.mts +11 -11
  21. package/dist/internal/testing.d.mts.map +1 -1
  22. package/dist/internal/testing.mjs +1 -1
  23. package/dist/main-comment-envelope-DFirHYhW.mjs +69 -0
  24. package/dist/main-comment-envelope-DFirHYhW.mjs.map +1 -0
  25. package/dist/{official-github-workflow-Bjlu-FL3.mjs → official-github-workflow-DNzV5Z70.mjs} +183 -33
  26. package/dist/official-github-workflow-DNzV5Z70.mjs.map +1 -0
  27. package/dist/{types-BAis_Lwm.d.mts → types-Dn1U3MDG.d.mts} +30 -18
  28. package/dist/types-Dn1U3MDG.d.mts.map +1 -0
  29. package/package.json +16 -8
  30. package/dist/commands-B9qNW4pk.mjs.map +0 -1
  31. package/dist/official-github-workflow-Bjlu-FL3.mjs.map +0 -1
  32. package/dist/types-BAis_Lwm.d.mts.map +0 -1
@@ -1,6 +1,7 @@
1
1
  import { c as resolveReadAtRefRequest, d as isRecord, l as unavailableReadAtRefResult, n as boundedLineSlice, r as parseManifestPath, u as createDiffRangeIndex } from "./runtime-tools-core-D8_WsbL_.mjs";
2
- import { a as officialInitRecipeWorkflowEnvSecrets, i as officialInitRecipeFiles, r as officialInitRecipeConfigTs, t as renderOfficialGithubWorkflow } from "./official-github-workflow-Bjlu-FL3.mjs";
2
+ import { a as officialInitRecipeWorkflowEnvSecrets, i as officialInitRecipeFiles, r as officialInitRecipeConfigTs, t as renderOfficialGithubWorkflow } from "./official-github-workflow-DNzV5Z70.mjs";
3
3
  import { t as isPublishableSuggestedFixSelection } from "./suggested-fix-publication-policy-B5Wwuudp.mjs";
4
+ import { a as mainCommentTitle, c as reviewStatsEndMarker, i as mainCommentHeaderHiddenMarker, l as reviewStatsHiddenMarker, n as mainCommentAttributionTextPattern, o as mainCommentTitles, r as mainCommentFooterHiddenMarker, s as piprRepositoryUrl, t as parseGeneratedMainCommentEnvelope, u as reviewStatsStartMarker } from "./main-comment-envelope-DFirHYhW.mjs";
4
5
  import { createRequire } from "node:module";
5
6
  import { chmod, chown, cp, lstat, mkdir, mkdtemp, readdir, rm } from "node:fs/promises";
6
7
  import path from "node:path";
@@ -597,7 +598,7 @@ function compareStableSemver(left, right) {
597
598
  }
598
599
  //#endregion
599
600
  //#region src/shared/version.ts
600
- const runtimeVersion = "0.4.3";
601
+ const runtimeVersion = "0.5.0";
601
602
  //#endregion
602
603
  //#region src/config/version-compat.ts
603
604
  async function resolveConfigVersionCompatibility(options) {
@@ -746,7 +747,7 @@ async function loadTypescriptForConfig(configDir) {
746
747
  packageRoot: localPackageRoot
747
748
  };
748
749
  return {
749
- ts: typescriptApi(await import("typescript")),
750
+ ts: typescriptApi(await import("typescript6")),
750
751
  packageRoot: await runtimeTypescriptPackageRoot()
751
752
  };
752
753
  }
@@ -759,7 +760,7 @@ function typescriptApi(module) {
759
760
  async function runtimeTypescriptPackageRoot() {
760
761
  try {
761
762
  const require = createRequire(import.meta.url);
762
- const packageRoot = path.dirname(require.resolve("typescript/package.json"));
763
+ const packageRoot = path.dirname(require.resolve("typescript6/package.json"));
763
764
  return await fileExists(path.join(packageRoot, "lib", "typescript.js")) ? packageRoot : void 0;
764
765
  } catch {
765
766
  return;
@@ -935,8 +936,8 @@ const supportedOfficialInitAdapters = [
935
936
  "azure-devops",
936
937
  "bitbucket"
937
938
  ];
938
- const defaultGitLabImageRef = "ghcr.io/somus/pipr:v0.4.3";
939
- const defaultSdkVersion = "0.4.3";
939
+ const defaultGitLabImageRef = "ghcr.io/somus/pipr:v0.5.0";
940
+ const defaultSdkVersion = "0.5.0";
940
941
  function resolveOfficialInitAdapters(adapters) {
941
942
  if (adapters === void 0) return ["github"];
942
943
  if (adapters.length === 0) return [];
@@ -1155,6 +1156,14 @@ async function maybeLstat(filePath) {
1155
1156
  }
1156
1157
  //#endregion
1157
1158
  //#region src/diff/git.ts
1159
+ var GitOutputLimitError = class extends Error {
1160
+ limitBytes;
1161
+ constructor(limitBytes) {
1162
+ super(`git output exceeded ${limitBytes} byte limit`);
1163
+ this.limitBytes = limitBytes;
1164
+ this.name = "GitOutputLimitError";
1165
+ }
1166
+ };
1158
1167
  function runGit$1(args, cwd, maxBuffer) {
1159
1168
  const result = Bun.spawnSync(["git", ...args], {
1160
1169
  cwd,
@@ -1163,6 +1172,7 @@ function runGit$1(args, cwd, maxBuffer) {
1163
1172
  stderr: "pipe",
1164
1173
  stdout: "pipe"
1165
1174
  });
1175
+ if (result.exitedDueToMaxBuffer && maxBuffer !== void 0) throw new GitOutputLimitError(maxBuffer);
1166
1176
  if (result.exitCode !== 0) {
1167
1177
  const failure = result.stderr?.toString().trim() || "unknown error";
1168
1178
  throw new Error(`git ${args.join(" ")} failed: ${failure}`);
@@ -1175,6 +1185,7 @@ const lockFilePattern = /(^|\/)(bun\.lock|package-lock\.json|pnpm-lock\.yaml|yar
1175
1185
  const generatedPattern = /(^|\/)(dist|build|coverage|vendor)\//;
1176
1186
  const maxInlineChangedLines = 1e3;
1177
1187
  const maxCommentableRangeLines = 5e3;
1188
+ const maxAggregatePatchBytes = 16 * 1024 * 1024;
1178
1189
  function buildDiffManifest(options) {
1179
1190
  const mergeBaseSha = runGit$1([
1180
1191
  "merge-base",
@@ -1188,7 +1199,7 @@ function buildDiffManifest(options) {
1188
1199
  "--find-renames"
1189
1200
  ], mergeBaseSha, diffHead), options.cwd));
1190
1201
  const diffStats = getDiffStats(options.cwd, mergeBaseSha, diffHead);
1191
- const rawPatch = parseRawPatch(runGit$1(buildUnifiedDiffArgs(mergeBaseSha, diffHead, getPreExcludedFiles(files, diffStats)), options.cwd));
1202
+ const rawPatch = loadRawPatch(buildUnifiedDiffArgs(mergeBaseSha, diffHead, getPreExcludedFiles(files, diffStats)), options.cwd);
1192
1203
  const parsedDiff = parseUnifiedDiff(rawPatch.patch, rawPatch.filePaths);
1193
1204
  for (const file of files) {
1194
1205
  const stats = diffStats.get(file.path);
@@ -1212,6 +1223,14 @@ function buildDiffManifest(options) {
1212
1223
  files
1213
1224
  });
1214
1225
  }
1226
+ function loadRawPatch(args, cwd) {
1227
+ try {
1228
+ return parseRawPatch(runGit$1(args, cwd, maxAggregatePatchBytes));
1229
+ } catch (error) {
1230
+ if (error instanceof GitOutputLimitError) throw new Error(`Diff Manifest construction exceeded aggregate patch limit before parsing; limit=${error.limitBytes} bytes`);
1231
+ throw error;
1232
+ }
1233
+ }
1215
1234
  function parseNameStatus(output) {
1216
1235
  const fields = output.split("\0");
1217
1236
  const files = [];
@@ -3409,8 +3428,7 @@ function resolveProvider(config, providerId) {
3409
3428
  return provider;
3410
3429
  }
3411
3430
  function createAgentRunContext(runtime) {
3412
- const runId = runtime.runId ?? runtime.taskContext?.run.id;
3413
- if (!runId) throw new Error("runId is required for stable review run identity");
3431
+ const run = runtime.run;
3414
3432
  const repositorySlugParts = runtime.event.repository.slug.split("/");
3415
3433
  const repository = {
3416
3434
  root: runtime.workspace,
@@ -3427,13 +3445,13 @@ function createAgentRunContext(runtime) {
3427
3445
  const platform = { id: runtime.event.platform.id };
3428
3446
  return {
3429
3447
  prompt: {
3430
- runId,
3448
+ run,
3431
3449
  repository,
3432
3450
  change,
3433
3451
  platform
3434
3452
  },
3435
3453
  tools: {
3436
- run: { id: runId },
3454
+ run,
3437
3455
  repository,
3438
3456
  change,
3439
3457
  platform
@@ -3696,25 +3714,6 @@ function buildRepairPrompt(options) {
3696
3714
  options.prompt
3697
3715
  ].join("\n\n");
3698
3716
  }
3699
- //#endregion
3700
- //#region src/review/comment-branding.ts
3701
- const piprLogoUrl = "https://pipr.run/images/pipr/pipr-mark.svg";
3702
- const piprRepositoryUrl = "https://github.com/somus/pipr";
3703
- const mainCommentTitle = `# <img src="${piprLogoUrl}" width="22" height="22" alt=""> Pipr Review`;
3704
- const mainCommentTitles = /* @__PURE__ */ new Set([
3705
- "# pipr Review",
3706
- "# Pipr Review",
3707
- mainCommentTitle
3708
- ]);
3709
- const mainCommentHeaderHiddenMarker = "<!-- pipr:header:hidden -->";
3710
- const mainCommentFooterHiddenMarker = "<!-- pipr:footer:hidden -->";
3711
- const reviewStatsStartMarker = "<!-- pipr:stats:start -->";
3712
- const reviewStatsEndMarker = "<!-- pipr:stats:end -->";
3713
- const reviewStatsHiddenMarker = "<!-- pipr:stats:hidden -->";
3714
- const escapedRepositoryUrl = piprRepositoryUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3715
- const mainCommentAttributionSource = `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+\\))\\.)?`;
3716
- const mainCommentAttributionTextPattern = new RegExp(`^${mainCommentAttributionSource}$`);
3717
- const mainCommentAttributionPattern = new RegExp(`^<sub>${mainCommentAttributionSource}</sub>$`);
3718
3717
  const maxReviewStatsModelLength = 200;
3719
3718
  const reviewStatsModelSchema = z.string().min(1).max(maxReviewStatsModelLength).transform((model) => sanitizeReviewStatsModel(model) ?? "[invalid model]");
3720
3719
  const reviewStatsSchema = z.strictObject({
@@ -4601,7 +4600,7 @@ async function runInternalVerifier(options) {
4601
4600
  env: options.env,
4602
4601
  piExecutable: options.piExecutable,
4603
4602
  piRunner: options.piRunner,
4604
- runId: options.runId,
4603
+ run: options.run,
4605
4604
  log: options.log,
4606
4605
  ...options.piRunSink ? { piRunSink: options.piRunSink } : {}
4607
4606
  }
@@ -4619,7 +4618,7 @@ async function runInternalVerifier(options) {
4619
4618
  function verifierInput(options, prior, candidates) {
4620
4619
  return {
4621
4620
  manifest: options.diffManifest,
4622
- runId: options.runId,
4621
+ runId: options.run.id,
4623
4622
  mode: options.mode.kind,
4624
4623
  reviewedHeadSha: prior.reviewedHeadSha,
4625
4624
  currentHeadSha: options.event.change.head.sha,
@@ -4720,7 +4719,6 @@ function verifierResponseBody(response) {
4720
4719
  }
4721
4720
  function internalVerifierAgent(provider, config) {
4722
4721
  return {
4723
- kind: "pipr.agent",
4724
4722
  name: "pipr-internal-verifier",
4725
4723
  definition: {
4726
4724
  model: modelProfile(provider),
@@ -4739,8 +4737,8 @@ function internalVerifierAgent(provider, config) {
4739
4737
  "Do not repeat user-supplied text unless needed to explain the verdict.",
4740
4738
  config.publication.autoResolve.instructions
4741
4739
  ].filter(Boolean).join("\n"),
4742
- prompt: (input) => {
4743
- const { manifest: _manifest, ...verifierPromptInput } = input;
4740
+ prompt: (value) => {
4741
+ const { manifest: _manifest, ...verifierPromptInput } = value;
4744
4742
  return JSON.stringify(verifierPromptInput, null, 2);
4745
4743
  },
4746
4744
  tools: [],
@@ -4749,15 +4747,6 @@ function internalVerifierAgent(provider, config) {
4749
4747
  invalidOutput: 1,
4750
4748
  transientFailure: 0
4751
4749
  }
4752
- },
4753
- extend(patch) {
4754
- return {
4755
- ...this,
4756
- definition: {
4757
- ...this.definition,
4758
- ...patch
4759
- }
4760
- };
4761
4750
  }
4762
4751
  };
4763
4752
  }
@@ -4779,20 +4768,6 @@ function boundedVerifierText(value) {
4779
4768
  //#endregion
4780
4769
  //#region src/review/task/task-output.ts
4781
4770
  const agentInlineFindingsOutputSchema = z.custom((value) => z.looseObject({ inlineFindings: z.array(reviewFindingSchema) }).safeParse(value).success);
4782
- const generatedReviewStatsShape = [
4783
- /^$/,
4784
- /^\| Metric \| Total \|$/,
4785
- /^\| --- \| ---: \|$/,
4786
- /^\| Models \| .+ \|$/,
4787
- /^\| Agent runs \| \d+ \|$/,
4788
- /^\| Elapsed \| .+ \|$/,
4789
- /^\| Input tokens \| (?:Unavailable|[\d,]+(?: \(reported\))?) \|$/,
4790
- /^\| Output tokens \| (?:Unavailable|[\d,]+(?: \(reported\))?) \|$/,
4791
- /^\| Cost \(USD\) \| (?:Unavailable|\$\d+(?:\.\d+)?(?:e[+-]?\d+)?(?: \(reported\))?) \|$/,
4792
- /^$/,
4793
- /^<\/details>$/,
4794
- /^<!-- pipr:stats:end -->$/
4795
- ];
4796
4771
  function createOutputState() {
4797
4772
  return {
4798
4773
  findings: [],
@@ -4959,42 +4934,6 @@ function visibleMainComment(body) {
4959
4934
  while (lines[0] === "") lines.shift();
4960
4935
  return lines.join("\n").trim();
4961
4936
  }
4962
- function parseGeneratedMainCommentEnvelope(lines) {
4963
- const mainMarkerIndex = lines.findIndex((line) => line.startsWith("<!-- pipr:main-comment "));
4964
- const headerCandidateOffset = lines.slice(mainMarkerIndex + 1).findIndex((line) => line !== "");
4965
- const headerCandidateIndex = mainMarkerIndex + 1 + headerCandidateOffset;
4966
- const headerMarkerIndex = mainMarkerIndex >= 0 && headerCandidateOffset >= 0 && lines[headerCandidateIndex] === "<!-- pipr:header:hidden -->" ? headerCandidateIndex : -1;
4967
- const lastLineIndex = lines.findLastIndex((line) => line !== "");
4968
- const lastLine = lines[lastLineIndex] ?? "";
4969
- const footerIndex = lastLine === "<!-- pipr:footer:hidden -->" || mainCommentAttributionPattern.test(lastLine) ? lastLineIndex : -1;
4970
- const lastContentIndex = lines.slice(0, footerIndex < 0 ? lines.length : footerIndex).findLastIndex((line) => line !== "");
4971
- return {
4972
- mainMarkerIndex,
4973
- headerMarkerIndex,
4974
- statsMarkerIndex: lines[lastContentIndex] === "<!-- pipr:stats:hidden -->" ? lastContentIndex : -1,
4975
- statsRange: generatedReviewStatsRange(lines, footerIndex),
4976
- footerIndex
4977
- };
4978
- }
4979
- function generatedReviewStatsRange(lines, generatedFooterIndex) {
4980
- const end = lines.slice(0, generatedFooterIndex < 0 ? lines.length : generatedFooterIndex).findLastIndex((line) => line !== "");
4981
- if (end < 0) return;
4982
- if (lines[end] !== "<!-- pipr:stats:end -->") return;
4983
- if (lines[end - 1] !== "</details>") return;
4984
- const start = lines.lastIndexOf(reviewStatsStartMarker, end - 2);
4985
- if (start < 0) return;
4986
- if (lines[start + 1] !== "<details>") return;
4987
- if (lines[start + 2] !== "<summary>Review stats</summary>") return;
4988
- if (!matchesGeneratedReviewStatsShape(lines, start, end)) return;
4989
- return {
4990
- start,
4991
- end
4992
- };
4993
- }
4994
- function matchesGeneratedReviewStatsShape(lines, start, end) {
4995
- const generatedShape = lines.slice(start + 3, end + 1);
4996
- return generatedShape.length === generatedReviewStatsShape.length && generatedReviewStatsShape.every((pattern, index) => pattern.test(generatedShape[index] ?? ""));
4997
- }
4998
4937
  function collectInlineFindings(state, findings) {
4999
4938
  if (!findings) return;
5000
4939
  const arrayScope = state.findingScopes.get(findings);
@@ -5020,7 +4959,7 @@ const genericTaskFailureSummary = "Task failed; see logs for details.";
5020
4959
  async function runTaskRuntime(options) {
5021
4960
  const runtimeStarted = Date.now();
5022
4961
  const config = parsePiprConfig(options.config);
5023
- const provider = options.providerOverride ? parseProviderConfig(options.providerOverride) : resolveProvider(config, config.defaultProvider);
4962
+ const provider = taskRuntimeProvider(options, config);
5024
4963
  const diffManifest = parseDiffManifest((options.diffManifestBuilder ?? buildDiffManifest)({
5025
4964
  cwd: options.workspace,
5026
4965
  baseSha: options.event.change.base.sha,
@@ -5068,6 +5007,10 @@ async function runTaskRuntime(options) {
5068
5007
  trustedConfigHash: options.trustedConfigHash,
5069
5008
  commandInvocation: options.commandInvocation
5070
5009
  });
5010
+ const run = Object.freeze({
5011
+ id: runId,
5012
+ trigger: taskRunTrigger(options)
5013
+ });
5071
5014
  const loadedPriorReviewState = options.priorReviewState ?? await options.loadPriorReviewState?.();
5072
5015
  const priorMainComment = options.priorMainComment ?? await options.loadPriorMainComment?.();
5073
5016
  const priorReviewState = priorReviewStateForSelectedTasks(loadedPriorReviewState, selectedTasks);
@@ -5076,62 +5019,23 @@ async function runTaskRuntime(options) {
5076
5019
  ...options,
5077
5020
  priorReviewState,
5078
5021
  priorMainComment,
5079
- runId,
5022
+ run,
5080
5023
  piRunSink(run) {
5081
5024
  piRuns.push(run);
5082
5025
  }
5083
5026
  };
5084
5027
  const manifestCache = /* @__PURE__ */ new Map();
5085
- const taskResults = await Promise.all(tasks.map(async (task, taskOrder) => {
5086
- const output = createOutputState();
5087
- const started = Date.now();
5088
- options.log?.info("task start", {
5089
- task: task.name,
5090
- order: taskOrder
5091
- });
5092
- try {
5093
- await task.handler(createTaskContext({
5094
- ...runtimeOptions,
5095
- config,
5096
- provider,
5097
- diffManifest,
5098
- manifestCache,
5099
- output,
5100
- taskName: task.name,
5101
- taskOrder
5102
- }), task.name === options.taskName ? options.taskInput : void 0);
5103
- options.log?.info("task ok", {
5104
- task: task.name,
5105
- durationMs: Date.now() - started,
5106
- findings: output.findings.length,
5107
- providerModels: output.providerModels,
5108
- repairAttempted: output.repairAttempted
5109
- });
5110
- return {
5111
- taskName: task.name,
5112
- output
5113
- };
5114
- } catch (error) {
5115
- const check = {
5116
- conclusion: "failure",
5117
- summary: genericTaskFailureSummary
5118
- };
5119
- options.log?.error("task failed", {
5120
- task: task.name,
5121
- durationMs: Date.now() - started,
5122
- error: error instanceof Error ? error.message : String(error)
5123
- });
5124
- if (options.log?.debugEnabled && error instanceof Error && error.stack) options.log.text("debug", "error stack", error.stack);
5125
- return {
5126
- taskName: task.name,
5127
- output: {
5128
- ...output,
5129
- check
5130
- },
5131
- error
5132
- };
5028
+ const taskResults = await executeSelectedTasks({
5029
+ tasks,
5030
+ runtimeOptions: options,
5031
+ context: {
5032
+ ...runtimeOptions,
5033
+ config,
5034
+ provider,
5035
+ diffManifest,
5036
+ manifestCache
5133
5037
  }
5134
- }));
5038
+ });
5135
5039
  const taskChecks = taskResults.map((result) => runtimeTaskCheckResult(result.taskName, result.output.check ?? { conclusion: "success" }));
5136
5040
  const failedTask = taskResults.find((result) => result.error !== void 0);
5137
5041
  if (failedTask) {
@@ -5144,11 +5048,20 @@ async function runTaskRuntime(options) {
5144
5048
  providerModels: output.providerModels,
5145
5049
  repairAttempted: output.repairAttempted
5146
5050
  });
5051
+ const commandDurationMs = Date.now() - runtimeStarted;
5147
5052
  const commandResponse = commandResponseResultFromOutput({
5148
5053
  provider,
5149
5054
  diffManifest,
5150
5055
  output,
5151
5056
  taskChecks,
5057
+ run: runSummary({
5058
+ options,
5059
+ run,
5060
+ selectedTasks,
5061
+ durationMs: commandDurationMs,
5062
+ models: output.providerModels.length > 0 ? uniq(output.providerModels) : [provider.model],
5063
+ stats: reviewStatsForRuns(piRuns, commandDurationMs)
5064
+ }),
5152
5065
  commandInvocation: options.commandInvocation,
5153
5066
  secretRedactor: options.secretRedactor
5154
5067
  });
@@ -5157,7 +5070,7 @@ async function runTaskRuntime(options) {
5157
5070
  return commandResponse;
5158
5071
  }
5159
5072
  assertReviewCommentOutput(output, options.commandInvocation !== void 0);
5160
- const main = typeof output.comment.value === "string" ? output.comment.value : output.comment.value.main ?? "Review completed.";
5073
+ const main = reviewMainComment(output);
5161
5074
  const validated = validateReviewResult(collectedReview(output, main), diffManifest, {
5162
5075
  expectedHeadSha: options.event.change.head.sha,
5163
5076
  pathScopeForFinding: (_finding, index) => output.findings[index]?.paths
@@ -5168,10 +5081,12 @@ async function runTaskRuntime(options) {
5168
5081
  provider,
5169
5082
  diffManifest,
5170
5083
  priorReviewState,
5171
- runId,
5084
+ run,
5172
5085
  piRunSink: runtimeOptions.piRunSink
5173
5086
  });
5174
- const stats = reviewStatsForRuns(piRuns, Date.now() - runtimeStarted);
5087
+ const durationMs = Date.now() - runtimeStarted;
5088
+ const stats = reviewStatsForRuns(piRuns, durationMs);
5089
+ const models = reviewProviderModels(output, verifier.providerModels, provider.model);
5175
5090
  const redactedPublication = redactReviewPublication({
5176
5091
  main,
5177
5092
  validated,
@@ -5197,7 +5112,7 @@ async function runTaskRuntime(options) {
5197
5112
  trustedConfigSha: options.trustedConfigSha,
5198
5113
  trustedConfigHash: options.trustedConfigHash,
5199
5114
  reviewedHeadSha: options.event.change.head.sha,
5200
- providerModels: output.providerModels.length + verifier.providerModels.length > 0 ? uniq([...output.providerModels, ...verifier.providerModels]) : [provider.model],
5115
+ providerModels: models,
5201
5116
  selectedTasks,
5202
5117
  failedTasks: [],
5203
5118
  validFindings: validated.validFindings.length,
@@ -5215,6 +5130,14 @@ async function runTaskRuntime(options) {
5215
5130
  });
5216
5131
  return {
5217
5132
  kind: "review",
5133
+ run: runSummary({
5134
+ options,
5135
+ run,
5136
+ selectedTasks,
5137
+ durationMs,
5138
+ models,
5139
+ stats
5140
+ }),
5218
5141
  provider,
5219
5142
  diffManifest,
5220
5143
  review: redactedPublication.validated.review,
@@ -5226,6 +5149,67 @@ async function runTaskRuntime(options) {
5226
5149
  repairAttempted: output.repairAttempted
5227
5150
  };
5228
5151
  }
5152
+ function taskRuntimeProvider(options, config) {
5153
+ return options.providerOverride ? parseProviderConfig(options.providerOverride) : resolveProvider(config, config.defaultProvider);
5154
+ }
5155
+ function taskRunTrigger(options) {
5156
+ if (options.runTrigger) return options.runTrigger;
5157
+ return options.commandInvocation ? "command" : "change-request";
5158
+ }
5159
+ function reviewMainComment(output) {
5160
+ return typeof output.comment.value === "string" ? output.comment.value : output.comment.value.main ?? "Review completed.";
5161
+ }
5162
+ function reviewProviderModels(output, verifierModels, fallbackModel) {
5163
+ return output.providerModels.length + verifierModels.length > 0 ? uniq([...output.providerModels, ...verifierModels]) : [fallbackModel];
5164
+ }
5165
+ async function executeSelectedTasks(options) {
5166
+ return Promise.all(options.tasks.map(async (task, taskOrder) => {
5167
+ const output = createOutputState();
5168
+ const started = Date.now();
5169
+ options.runtimeOptions.log?.info("task start", {
5170
+ task: task.name,
5171
+ order: taskOrder
5172
+ });
5173
+ try {
5174
+ await task.handler(createTaskContext({
5175
+ ...options.context,
5176
+ output,
5177
+ taskName: task.name,
5178
+ taskOrder
5179
+ }), task.name === options.runtimeOptions.taskName ? options.runtimeOptions.taskInput : void 0);
5180
+ options.runtimeOptions.log?.info("task ok", {
5181
+ task: task.name,
5182
+ durationMs: Date.now() - started,
5183
+ findings: output.findings.length,
5184
+ providerModels: output.providerModels,
5185
+ repairAttempted: output.repairAttempted
5186
+ });
5187
+ return {
5188
+ taskName: task.name,
5189
+ output
5190
+ };
5191
+ } catch (error) {
5192
+ const check = {
5193
+ conclusion: "failure",
5194
+ summary: genericTaskFailureSummary
5195
+ };
5196
+ options.runtimeOptions.log?.error("task failed", {
5197
+ task: task.name,
5198
+ durationMs: Date.now() - started,
5199
+ error: error instanceof Error ? error.message : String(error)
5200
+ });
5201
+ if (options.runtimeOptions.log?.debugEnabled && error instanceof Error && error.stack) options.runtimeOptions.log.text("debug", "error stack", error.stack);
5202
+ return {
5203
+ taskName: task.name,
5204
+ output: {
5205
+ ...output,
5206
+ check
5207
+ },
5208
+ error
5209
+ };
5210
+ }
5211
+ }));
5212
+ }
5229
5213
  function publishFailedRunTaskChecks(options, taskChecks) {
5230
5214
  const redacted = redactCommandPublication({
5231
5215
  body: "",
@@ -5234,6 +5218,22 @@ function publishFailedRunTaskChecks(options, taskChecks) {
5234
5218
  });
5235
5219
  publishTaskChecks(options.checkSink, redacted.taskChecks);
5236
5220
  }
5221
+ function runSummary(options) {
5222
+ const { agentRuns = 0, inputTokens = 0, outputTokens = 0, costUsd = 0, usageStatus = "unavailable" } = options.stats ?? {};
5223
+ return {
5224
+ ...options.run,
5225
+ baseSha: options.options.event.change.base.sha,
5226
+ headSha: options.options.event.change.head.sha,
5227
+ tasks: options.selectedTasks,
5228
+ durationMs: options.durationMs,
5229
+ models: options.models,
5230
+ agentRuns,
5231
+ inputTokens,
5232
+ outputTokens,
5233
+ costUsd,
5234
+ usageStatus
5235
+ };
5236
+ }
5237
5237
  function commandResponseResultFromOutput(options) {
5238
5238
  const commandResponse = options.output.commandResponse;
5239
5239
  if (!commandResponse) return;
@@ -5270,7 +5270,7 @@ async function runSynchronizeVerifier(options) {
5270
5270
  priorReviewState: options.priorReviewState,
5271
5271
  threadContexts: await options.options.loadInlineThreadContexts?.() ?? [],
5272
5272
  mode: { kind: "synchronize" },
5273
- runId: options.runId,
5273
+ run: options.run,
5274
5274
  piRunSink: options.piRunSink
5275
5275
  });
5276
5276
  }
@@ -5278,7 +5278,7 @@ function createTaskContext(options) {
5278
5278
  const repositorySlugParts = options.event.repository.slug.split("/");
5279
5279
  let taskContext;
5280
5280
  taskContext = {
5281
- run: { id: options.runId },
5281
+ run: options.run,
5282
5282
  repository: {
5283
5283
  root: options.workspace,
5284
5284
  owner: repositorySlugParts.length > 1 ? repositorySlugParts[0] : void 0,
@@ -5307,9 +5307,6 @@ function createTaskContext(options) {
5307
5307
  previousPath: file.previousPath,
5308
5308
  status: file.status
5309
5309
  }));
5310
- },
5311
- async currentHeadSha() {
5312
- return options.event.change.head.sha;
5313
5310
  }
5314
5311
  },
5315
5312
  platform: { id: options.event.platform.id },
@@ -5326,13 +5323,13 @@ function createTaskContext(options) {
5326
5323
  },
5327
5324
  pi: { async run(agent, input, runOptions) {
5328
5325
  const result = await runReviewAgent({
5329
- agent,
5326
+ agent: options.plan.resolveAgent(agent),
5330
5327
  input,
5331
5328
  runOptions,
5332
5329
  runtime: {
5333
5330
  ...options,
5334
5331
  taskContext,
5335
- runId: options.runId,
5332
+ run: options.run,
5336
5333
  piRunSink: options.piRunSink
5337
5334
  }
5338
5335
  });
@@ -5371,6 +5368,7 @@ function commandResponseRuntimeResult(options) {
5371
5368
  });
5372
5369
  return {
5373
5370
  kind: "command-response",
5371
+ run: options.run,
5374
5372
  provider: options.provider,
5375
5373
  diffManifest: options.diffManifest,
5376
5374
  taskChecks: redacted.taskChecks,
@@ -5461,6 +5459,134 @@ function createKnownSecretRedactor(options) {
5461
5459
  return createSecretRedactor(sensitiveEnvironmentValues(options?.env ?? process.env));
5462
5460
  }
5463
5461
  //#endregion
5462
+ //#region src/review/inline-publication-policy.ts
5463
+ function inlinePublicationDecision(options) {
5464
+ if (options.existing.markers.has(options.marker) || hasExistingInlinePublicationLocation(options.existing.locations, options.location)) return "skip";
5465
+ return "post";
5466
+ }
5467
+ function hasExistingInlinePublicationLocation(existing, location) {
5468
+ return existing.some((comment) => {
5469
+ if (comment.path !== location.path || comment.commitId !== location.commitId || comment.side !== location.side) return false;
5470
+ return comment.startLine <= location.endLine && location.startLine <= comment.endLine;
5471
+ });
5472
+ }
5473
+ //#endregion
5474
+ //#region src/review/publication-result.ts
5475
+ /** Error thrown when publication fails after producing partial result metadata. */
5476
+ var PublicationError = class extends Error {
5477
+ result;
5478
+ constructor(message, result) {
5479
+ super(message);
5480
+ this.result = result;
5481
+ }
5482
+ };
5483
+ //#endregion
5484
+ //#region src/hosts/publication.ts
5485
+ async function publishUnseenInlineItems(options) {
5486
+ const existing = new Set(extractInlineFindingMarkerRecords(options.existingBodies).map((record) => `${record.id}:${record.head}`));
5487
+ const errors = [];
5488
+ let posted = 0;
5489
+ let skipped = 0;
5490
+ for (const item of options.items) {
5491
+ const marker = `${item.findingId}:${item.reviewedHeadSha}`;
5492
+ const location = options.location?.(item);
5493
+ if (existing.has(marker) || location !== void 0 && inlinePublicationDecision({
5494
+ marker,
5495
+ location,
5496
+ existing: {
5497
+ markers: existing,
5498
+ locations: options.existingLocations ?? []
5499
+ }
5500
+ }) === "skip") {
5501
+ skipped += 1;
5502
+ continue;
5503
+ }
5504
+ try {
5505
+ await options.publish(item);
5506
+ posted += 1;
5507
+ existing.add(marker);
5508
+ if (location) options.existingLocations?.push(location);
5509
+ } catch (error) {
5510
+ errors.push(error instanceof Error ? error.message : String(error));
5511
+ }
5512
+ }
5513
+ return {
5514
+ posted,
5515
+ skipped,
5516
+ errors
5517
+ };
5518
+ }
5519
+ function nativeInlineLocation(options) {
5520
+ const rightSide = options.rightEnd !== void 0;
5521
+ const endLine = rightSide ? options.rightEnd : options.leftEnd;
5522
+ if (endLine === void 0) return void 0;
5523
+ return {
5524
+ path: rightSide ? options.rightPath : options.leftPath,
5525
+ commitId: options.commitId,
5526
+ side: rightSide ? "RIGHT" : "LEFT",
5527
+ startLine: (rightSide ? options.rightStart : options.leftStart) ?? endLine,
5528
+ endLine
5529
+ };
5530
+ }
5531
+ function commandResponseBody(options) {
5532
+ const marker = `<!-- pipr:command-response change=${options.changeNumber} source=${options.sourceCommentId} command=${options.commandName} -->`;
5533
+ return {
5534
+ marker,
5535
+ body: [
5536
+ marker,
5537
+ "",
5538
+ options.body,
5539
+ ""
5540
+ ].join("\n")
5541
+ };
5542
+ }
5543
+ function commandStatusText(options) {
5544
+ return [
5545
+ `<!-- pipr:command-state state=${options.state} head=${options.reviewedHeadSha} -->`,
5546
+ "",
5547
+ {
5548
+ accepted: "Pipr accepted this command.",
5549
+ running: "Pipr is running this command.",
5550
+ completed: "Pipr completed this command.",
5551
+ failed: "Pipr could not complete this command; see the run log for details.",
5552
+ superseded: "Pipr stopped because the change request head was updated."
5553
+ }[options.state]
5554
+ ].join("\n");
5555
+ }
5556
+ function threadActionReply(action) {
5557
+ const marker = action.kind === "resolve" ? renderResolvedFindingMarker(action.findingId, action.findingHeadSha) : renderVerifierResponseMarker(action.findingId, action.responseKey);
5558
+ return {
5559
+ marker,
5560
+ body: [
5561
+ marker,
5562
+ "",
5563
+ action.body.replaceAll("<!--", "&lt;!--")
5564
+ ].join("\n")
5565
+ };
5566
+ }
5567
+ function completeHostPublication(options) {
5568
+ const partial = {
5569
+ inlineComments: {
5570
+ posted: options.inline.posted,
5571
+ skipped: options.inline.skipped,
5572
+ failed: options.inline.errors.length
5573
+ },
5574
+ metadata: {
5575
+ ...options.metadata,
5576
+ inlinePublicationErrors: options.inline.errors,
5577
+ inlineResolutionErrors: options.resolutionErrors
5578
+ }
5579
+ };
5580
+ if (options.inline.errors.length > 0) throw new PublicationError(`${options.provider} inline comment publication failed`, partial);
5581
+ return {
5582
+ mainComment: {
5583
+ action: options.mainAction,
5584
+ id: options.mainId
5585
+ },
5586
+ ...partial
5587
+ };
5588
+ }
5589
+ //#endregion
5464
5590
  //#region src/hosts/http.ts
5465
5591
  var CodeHostHttpError = class extends Error {
5466
5592
  status;
@@ -6032,121 +6158,6 @@ function azureRepositoryUrl(organization, project, repository) {
6032
6158
  return `https://dev.azure.com/${encodeURIComponent(organization)}/${encodeURIComponent(project)}/_git/${encodeURIComponent(repository)}`;
6033
6159
  }
6034
6160
  //#endregion
6035
- //#region src/review/inline-publication-policy.ts
6036
- function inlinePublicationDecision(options) {
6037
- if (options.existing.markers.has(options.marker) || hasExistingInlinePublicationLocation(options.existing.locations, options.location)) return "skip";
6038
- return "post";
6039
- }
6040
- function hasExistingInlinePublicationLocation(existing, location) {
6041
- return existing.some((comment) => {
6042
- if (comment.path !== location.path || comment.commitId !== location.commitId || comment.side !== location.side) return false;
6043
- return comment.startLine <= location.endLine && location.startLine <= comment.endLine;
6044
- });
6045
- }
6046
- //#endregion
6047
- //#region src/review/publication-result.ts
6048
- /** Error thrown when publication fails after producing partial result metadata. */
6049
- var PublicationError = class extends Error {
6050
- result;
6051
- constructor(message, result) {
6052
- super(message);
6053
- this.result = result;
6054
- }
6055
- };
6056
- //#endregion
6057
- //#region src/hosts/publication.ts
6058
- async function publishUnseenInlineItems(options) {
6059
- const existing = new Set(extractInlineFindingMarkerRecords(options.existingBodies).map((record) => `${record.id}:${record.head}`));
6060
- const errors = [];
6061
- let posted = 0;
6062
- let skipped = 0;
6063
- for (const item of options.items) {
6064
- const marker = `${item.findingId}:${item.reviewedHeadSha}`;
6065
- const location = options.location?.(item);
6066
- if (existing.has(marker) || location !== void 0 && inlinePublicationDecision({
6067
- marker,
6068
- location,
6069
- existing: {
6070
- markers: existing,
6071
- locations: options.existingLocations ?? []
6072
- }
6073
- }) === "skip") {
6074
- skipped += 1;
6075
- continue;
6076
- }
6077
- try {
6078
- await options.publish(item);
6079
- posted += 1;
6080
- existing.add(marker);
6081
- if (location) options.existingLocations?.push(location);
6082
- } catch (error) {
6083
- errors.push(error instanceof Error ? error.message : String(error));
6084
- }
6085
- }
6086
- return {
6087
- posted,
6088
- skipped,
6089
- errors
6090
- };
6091
- }
6092
- function nativeInlineLocation(options) {
6093
- const rightSide = options.rightEnd !== void 0;
6094
- const endLine = rightSide ? options.rightEnd : options.leftEnd;
6095
- if (endLine === void 0) return void 0;
6096
- return {
6097
- path: rightSide ? options.rightPath : options.leftPath,
6098
- commitId: options.commitId,
6099
- side: rightSide ? "RIGHT" : "LEFT",
6100
- startLine: (rightSide ? options.rightStart : options.leftStart) ?? endLine,
6101
- endLine
6102
- };
6103
- }
6104
- function commandResponseBody(options) {
6105
- const marker = `<!-- pipr:command-response change=${options.changeNumber} source=${options.sourceCommentId} command=${options.commandName} -->`;
6106
- return {
6107
- marker,
6108
- body: [
6109
- marker,
6110
- "",
6111
- options.body,
6112
- ""
6113
- ].join("\n")
6114
- };
6115
- }
6116
- function threadActionReply(action) {
6117
- const marker = action.kind === "resolve" ? renderResolvedFindingMarker(action.findingId, action.findingHeadSha) : renderVerifierResponseMarker(action.findingId, action.responseKey);
6118
- return {
6119
- marker,
6120
- body: [
6121
- marker,
6122
- "",
6123
- action.body.replaceAll("<!--", "&lt;!--")
6124
- ].join("\n")
6125
- };
6126
- }
6127
- function completeHostPublication(options) {
6128
- const partial = {
6129
- inlineComments: {
6130
- posted: options.inline.posted,
6131
- skipped: options.inline.skipped,
6132
- failed: options.inline.errors.length
6133
- },
6134
- metadata: {
6135
- ...options.metadata,
6136
- inlinePublicationErrors: options.inline.errors,
6137
- inlineResolutionErrors: options.resolutionErrors
6138
- }
6139
- };
6140
- if (options.inline.errors.length > 0) throw new PublicationError(`${options.provider} inline comment publication failed`, partial);
6141
- return {
6142
- mainComment: {
6143
- action: options.mainAction,
6144
- id: options.mainId
6145
- },
6146
- ...partial
6147
- };
6148
- }
6149
- //#endregion
6150
6161
  //#region src/hosts/azure-devops/publication.ts
6151
6162
  async function publishAzureDevOpsPlan(options) {
6152
6163
  const coordinates = azureCoordinates$1(options.change);
@@ -6226,8 +6237,11 @@ async function publishAzureDevOpsCommandResponse(options) {
6226
6237
  commandName: options.commandName,
6227
6238
  body: options.body
6228
6239
  });
6229
- await currentPullRequest(options.client, options.change);
6230
- const { owner, threads } = await loadAzureWriteState(options.client, options.change);
6240
+ if (!options.allowHeadDrift) await currentPullRequest(options.client, options.change);
6241
+ const { owner, threads } = options.allowHeadDrift ? {
6242
+ owner: await authenticatedAzureOwner(options.client),
6243
+ threads: await options.client.listThreads(coordinates.repositoryId, options.change.change.number)
6244
+ } : await loadAzureWriteState(options.client, options.change);
6231
6245
  const existing = ownedRootThread(threads, owner.uniqueName, response.marker);
6232
6246
  const comment = existing ? await options.client.updateComment(coordinates.repositoryId, options.change.change.number, existing.id, existing.comments[0]?.id ?? "", response.body) : (await options.client.createThread(coordinates.repositoryId, options.change.change.number, unpositionedThread(response.body))).comments[0];
6233
6247
  if (!comment) throw new Error("Azure DevOps did not return the command response comment");
@@ -6564,6 +6578,12 @@ function createAzureDevOpsHostAdapter(options = {}) {
6564
6578
  client,
6565
6579
  ...args
6566
6580
  }),
6581
+ publishCommandStatus: (args) => publishAzureDevOpsCommandResponse({
6582
+ client,
6583
+ ...args,
6584
+ body: commandStatusText(args),
6585
+ allowHeadDrift: true
6586
+ }),
6567
6587
  publishThreadActions: (args) => publishAzureDevOpsThreadActions({
6568
6588
  client,
6569
6589
  ...args
@@ -7054,8 +7074,11 @@ async function publishBitbucketCommandResponse(options) {
7054
7074
  commandName: options.commandName,
7055
7075
  body: options.body
7056
7076
  });
7057
- await assertCurrentEndpoints(options.client, options.change);
7058
- const { owner, comments } = await loadBitbucketWriteState(options.client, options.change);
7077
+ if (!options.allowHeadDrift) await assertCurrentEndpoints(options.client, options.change);
7078
+ const { owner, comments } = options.allowHeadDrift ? {
7079
+ owner: await authenticatedBitbucketOwner(options.client),
7080
+ comments: await options.client.listComments(options.change.change.number)
7081
+ } : await loadBitbucketWriteState(options.client, options.change);
7059
7082
  const existing = comments.find((comment) => comment.user?.uuid === owner.uuid && normalizeBitbucketMarkdown(comment.content.raw).includes(response.marker));
7060
7083
  const responseBody = renderBitbucketMarkdown(response.body);
7061
7084
  const comment = existing ? await options.client.updateComment(options.change.change.number, existing.id, responseBody) : await options.client.createComment(options.change.change.number, { content: { raw: responseBody } });
@@ -7241,6 +7264,12 @@ function createBitbucketHostAdapter(options = {}) {
7241
7264
  client,
7242
7265
  ...args
7243
7266
  }),
7267
+ publishCommandStatus: (args) => publishBitbucketCommandResponse({
7268
+ client,
7269
+ ...args,
7270
+ body: commandStatusText(args),
7271
+ allowHeadDrift: true
7272
+ }),
7244
7273
  publishThreadActions: (args) => publishBitbucketThreadActions({
7245
7274
  client,
7246
7275
  ...args
@@ -7895,32 +7924,26 @@ function findMainComment(comments, marker, changeNumber, ownerLogin) {
7895
7924
  }
7896
7925
  //#endregion
7897
7926
  //#region src/hosts/github/publication-command-response.ts
7898
- const commandResponseMarker = "pipr:command-response";
7899
7927
  async function publishGitHubCommandResponse(options) {
7900
- await assertCurrentHeadSha(options.client, options.change, options.change.change.head.sha);
7928
+ if (!options.allowHeadDrift) await assertCurrentHeadSha(options.client, options.change, options.change.change.head.sha);
7901
7929
  const ownerLogin = await options.client.getAuthenticatedUserLogin();
7902
- const marker = renderCommandResponseMarker({
7930
+ const response = commandResponseBody({
7903
7931
  changeNumber: options.change.change.number,
7904
- sourceCommentId: options.sourceCommentId,
7905
- commandName: options.commandName
7932
+ sourceCommentId: String(options.sourceCommentId),
7933
+ commandName: options.commandName,
7934
+ body: options.body
7906
7935
  });
7907
- const body = [
7908
- marker,
7909
- "",
7910
- options.body,
7911
- ""
7912
- ].join("\n");
7913
7936
  const comments = await options.client.listIssueComments({
7914
7937
  repo: options.change.repository.slug,
7915
7938
  issueNumber: options.change.change.number
7916
7939
  });
7917
- await assertCurrentHeadSha(options.client, options.change, options.change.change.head.sha);
7918
- const existing = findCommandResponseComment(comments, marker, ownerLogin);
7940
+ if (!options.allowHeadDrift) await assertCurrentHeadSha(options.client, options.change, options.change.change.head.sha);
7941
+ const existing = findCommandResponseComment(comments, response.marker, ownerLogin);
7919
7942
  if (existing) {
7920
7943
  const updated = await options.client.updateIssueComment({
7921
7944
  repo: options.change.repository.slug,
7922
7945
  commentId: existing.id,
7923
- body
7946
+ body: response.body
7924
7947
  });
7925
7948
  return {
7926
7949
  action: "updated",
@@ -7930,7 +7953,7 @@ async function publishGitHubCommandResponse(options) {
7930
7953
  const created = await options.client.createIssueComment({
7931
7954
  repo: options.change.repository.slug,
7932
7955
  issueNumber: options.change.change.number,
7933
- body
7956
+ body: response.body
7934
7957
  });
7935
7958
  return {
7936
7959
  action: "created",
@@ -7940,9 +7963,6 @@ async function publishGitHubCommandResponse(options) {
7940
7963
  function findCommandResponseComment(comments, marker, ownerLogin) {
7941
7964
  return findOwnedIssueComment(comments, ownerLogin, (firstLine) => firstLine === marker);
7942
7965
  }
7943
- function renderCommandResponseMarker(options) {
7944
- return `<!-- ${commandResponseMarker} change=${options.changeNumber} source=${options.sourceCommentId} command=${options.commandName} -->`;
7945
- }
7946
7966
  //#endregion
7947
7967
  //#region src/hosts/github/publication-prior-state.ts
7948
7968
  async function loadGitHubPriorReviewState(options) {
@@ -8486,6 +8506,16 @@ function createGitHubHostAdapter(options = {}) {
8486
8506
  body: options.body
8487
8507
  });
8488
8508
  },
8509
+ publishCommandStatus(options) {
8510
+ return publishGitHubCommandResponse({
8511
+ client: publicationClient,
8512
+ change: options.change,
8513
+ sourceCommentId: Number(options.sourceCommentId),
8514
+ commandName: options.commandName,
8515
+ body: commandStatusText(options),
8516
+ allowHeadDrift: true
8517
+ });
8518
+ },
8489
8519
  publishThreadActions(options) {
8490
8520
  return publishGitHubThreadActions({
8491
8521
  client: publicationClient,
@@ -8988,7 +9018,7 @@ async function publishGitLabCommandResponse(options) {
8988
9018
  body: options.body
8989
9019
  });
8990
9020
  const existing = ownedNote(await options.client.listNotes(projectId, options.change.change.number), owner.username, response.marker);
8991
- await assertCurrentHead(options.client, projectId, options.change);
9021
+ if (!options.allowHeadDrift) await assertCurrentHead(options.client, projectId, options.change);
8992
9022
  const note = existing ? await options.client.updateNote(projectId, options.change.change.number, existing.id, response.body) : await options.client.createNote(projectId, options.change.change.number, response.body);
8993
9023
  return {
8994
9024
  action: existing ? "updated" : "created",
@@ -9178,6 +9208,12 @@ function createGitLabHostAdapter(options = {}) {
9178
9208
  client,
9179
9209
  ...args
9180
9210
  }),
9211
+ publishCommandStatus: (args) => publishGitLabCommandResponse({
9212
+ client,
9213
+ ...args,
9214
+ body: commandStatusText(args),
9215
+ allowHeadDrift: true
9216
+ }),
9181
9217
  publishThreadActions: (args) => publishGitLabThreadActions({
9182
9218
  client,
9183
9219
  ...args
@@ -9541,6 +9577,7 @@ async function runTrustedReviewAndPublish(options) {
9541
9577
  await finalizeRuntimeChecks(checks, {});
9542
9578
  return {
9543
9579
  kind: "command-response",
9580
+ run: review.run,
9544
9581
  response: {
9545
9582
  commandName: review.commandResponse.commandName,
9546
9583
  body: review.commandResponse.body
@@ -9814,6 +9851,11 @@ function runnableIssueCommentCommand(comment, dryRun) {
9814
9851
  };
9815
9852
  }
9816
9853
  async function dispatchIssueCommentCommand(options, adapter, prepared, log) {
9854
+ const runnable = await resolveRunnableCommand(adapter, prepared, log);
9855
+ if (runnable.kind !== "matched") return runnable;
9856
+ return await runCommandLifecycle(options, adapter, prepared, runnable.invocation, log);
9857
+ }
9858
+ async function resolveRunnableCommand(adapter, prepared, log) {
9817
9859
  const requiredPermission = prepared.resolution.kind === "matched" ? prepared.resolution.invocation.requiredPermission : prepared.resolution.requiredPermission;
9818
9860
  const permission = await logPhase(log, "check command permission", async () => adapter.permissions.getRepositoryPermission({
9819
9861
  change: prepared.event,
@@ -9850,36 +9892,93 @@ async function dispatchIssueCommentCommand(options, adapter, prepared, log) {
9850
9892
  kind: "ignored",
9851
9893
  reason: "command dispatch did not resolve to a runnable task"
9852
9894
  };
9853
- await prepareTrustedHeadCheckout(options, adapter, prepared.trustedRuntime.settings.config, prepared.event, log);
9854
- const dispatch = dispatchRuntimeEntry({
9855
- kind: "change-request",
9856
- plan: prepared.trustedRuntime.plan,
9857
- event: prepared.event,
9858
- taskName: parsedResolution.invocation.taskName
9895
+ return parsedResolution;
9896
+ }
9897
+ async function runCommandLifecycle(options, adapter, prepared, invocation, log) {
9898
+ const status = adapter.publication?.publishCommandStatus;
9899
+ if (!status) throw new Error("command status publication is not available for this code host");
9900
+ const statusOptions = {
9901
+ change: prepared.event,
9902
+ sourceCommentId: prepared.comment.commentId,
9903
+ commandName: invocation.commandName,
9904
+ reviewedHeadSha: prepared.event.change.head.sha
9905
+ };
9906
+ await status({
9907
+ ...statusOptions,
9908
+ state: "accepted"
9859
9909
  });
9860
- return await issueCommentCommandResult({
9861
- adapter,
9862
- completed: await runTrustedReviewAndPublish({
9863
- options,
9910
+ try {
9911
+ await prepareTrustedHeadCheckout(options, adapter, prepared.trustedRuntime.settings.config, prepared.event, log);
9912
+ const dispatch = dispatchRuntimeEntry({
9913
+ kind: "change-request",
9914
+ plan: prepared.trustedRuntime.plan,
9915
+ event: prepared.event,
9916
+ taskName: invocation.taskName
9917
+ });
9918
+ await status({
9919
+ ...statusOptions,
9920
+ state: "running"
9921
+ });
9922
+ const result = await issueCommentCommandResult({
9864
9923
  adapter,
9865
- trustedRuntime: prepared.trustedRuntime,
9924
+ completed: await runTrustedReviewAndPublish({
9925
+ options,
9926
+ adapter,
9927
+ trustedRuntime: prepared.trustedRuntime,
9928
+ event: prepared.event,
9929
+ taskName: invocation.taskName,
9930
+ taskInput: invocation.inputs,
9931
+ selectedTasks: dispatch.kind === "change-request" ? dispatch.tasks : [],
9932
+ commandInvocation: {
9933
+ name: invocation.commandName,
9934
+ line: invocation.line,
9935
+ arguments: invocation.arguments,
9936
+ sourceCommentId: prepared.comment.commentId
9937
+ },
9938
+ log
9939
+ }),
9866
9940
  event: prepared.event,
9867
- taskName: parsedResolution.invocation.taskName,
9868
- taskInput: parsedResolution.invocation.inputs,
9869
- selectedTasks: dispatch.kind === "change-request" ? dispatch.tasks : [],
9870
- commandInvocation: {
9871
- name: parsedResolution.invocation.commandName,
9872
- line: parsedResolution.invocation.line,
9873
- arguments: parsedResolution.invocation.arguments,
9874
- sourceCommentId: prepared.comment.commentId
9875
- },
9941
+ commandName: invocation.commandName,
9942
+ sourceCommentId: prepared.comment.commentId,
9943
+ configSource: prepared.trustedRuntime.settings.source
9944
+ });
9945
+ if (result.kind === "review") await status({
9946
+ ...statusOptions,
9947
+ state: "completed"
9948
+ });
9949
+ return result;
9950
+ } catch (error) {
9951
+ await publishCommandFailureStatus({
9952
+ adapter,
9953
+ prepared,
9954
+ status,
9955
+ statusOptions,
9956
+ error,
9876
9957
  log
9877
- }),
9878
- event: prepared.event,
9879
- commandName: parsedResolution.invocation.commandName,
9880
- sourceCommentId: prepared.comment.commentId,
9881
- configSource: prepared.trustedRuntime.settings.source
9882
- });
9958
+ });
9959
+ throw error;
9960
+ }
9961
+ }
9962
+ async function publishCommandFailureStatus(options) {
9963
+ try {
9964
+ const state = (await options.adapter.events.loadChangeRequest({
9965
+ repository: options.prepared.comment.repository,
9966
+ changeNumber: options.prepared.comment.changeNumber,
9967
+ workspace: options.prepared.comment.workspace,
9968
+ eventName: options.prepared.comment.eventName,
9969
+ action: options.prepared.comment.action,
9970
+ rawAction: options.prepared.comment.rawAction
9971
+ })).change.head.sha === options.prepared.event.change.head.sha ? "failed" : "superseded";
9972
+ await options.status({
9973
+ ...options.statusOptions,
9974
+ state
9975
+ });
9976
+ } catch (statusError) {
9977
+ options.log.warning("command terminal status publication failed", {
9978
+ error: statusError instanceof Error ? statusError.message : String(statusError),
9979
+ originalError: options.error instanceof Error ? options.error.message : String(options.error)
9980
+ });
9981
+ }
9883
9982
  }
9884
9983
  async function issueCommentCommandResult(options) {
9885
9984
  if (options.completed.kind === "skipped") return {
@@ -9913,6 +10012,7 @@ async function publishCommandResponseHostRunResult(options) {
9913
10012
  });
9914
10013
  return {
9915
10014
  kind: "command-response",
10015
+ run: options.completed.run,
9916
10016
  event: options.event,
9917
10017
  command: options.completed.response.commandName,
9918
10018
  configSource: options.configSource,
@@ -9950,6 +10050,7 @@ async function runReviewCommentReplyHostRunCommand(options, adapter, log, reply)
9950
10050
  });
9951
10051
  return {
9952
10052
  kind: "verifier",
10053
+ run: result.run,
9953
10054
  event: prepared.event,
9954
10055
  configSource: prepared.trustedRuntime.settings.source,
9955
10056
  errors: publication?.errors ?? []
@@ -10027,6 +10128,23 @@ async function runReviewCommentVerifier(options, adapter, prepared, log) {
10027
10128
  const config = trustedRuntime.settings.config;
10028
10129
  const provider = resolveProvider(config, config.defaultProvider);
10029
10130
  const verifierProvider = resolveProvider(config, config.publication.autoResolve.model ?? config.defaultProvider);
10131
+ const started = Date.now();
10132
+ const piRuns = [];
10133
+ const runId = stableReviewRunId({
10134
+ event,
10135
+ selectedTasks: ["pipr-internal-verifier"],
10136
+ trustedConfigSha: trustedRuntime.trustedConfigSha,
10137
+ trustedConfigHash: trustedRuntime.trustedConfigHash,
10138
+ verifierInvocation: {
10139
+ mode: "user-reply",
10140
+ commentId: reply.commentId,
10141
+ parentCommentId: reply.parentCommentId
10142
+ }
10143
+ });
10144
+ const runContext = Object.freeze({
10145
+ id: runId,
10146
+ trigger: "verifier"
10147
+ });
10030
10148
  const threadContexts = await adapter.comments?.loadInlineThreadContexts?.({ change: event }) ?? [];
10031
10149
  log.notice("verifier start", {
10032
10150
  mode: "user-reply",
@@ -10061,26 +10179,47 @@ async function runReviewCommentVerifier(options, adapter, prepared, log) {
10061
10179
  },
10062
10180
  respondWhenStillValid: config.publication.autoResolve.userReplies.respondWhenStillValid
10063
10181
  },
10064
- runId: stableReviewRunId({
10065
- event,
10066
- selectedTasks: ["pipr-internal-verifier"],
10067
- trustedConfigSha: trustedRuntime.trustedConfigSha,
10068
- trustedConfigHash: trustedRuntime.trustedConfigHash,
10069
- verifierInvocation: {
10070
- mode: "user-reply",
10071
- commentId: reply.commentId,
10072
- parentCommentId: reply.parentCommentId
10073
- }
10074
- })
10182
+ run: runContext,
10183
+ piRunSink(run) {
10184
+ piRuns.push(run);
10185
+ }
10186
+ });
10187
+ const durationMs = Date.now() - started;
10188
+ const stats = reviewStatsForRuns(piRuns, durationMs);
10189
+ const run = verifierRunSummary({
10190
+ event,
10191
+ run: runContext,
10192
+ durationMs,
10193
+ providerModels: result.providerModels,
10194
+ fallbackModel: verifierProvider.model,
10195
+ stats
10075
10196
  });
10076
10197
  return {
10077
10198
  ...result,
10199
+ run,
10078
10200
  threadActions: redactThreadActions({
10079
10201
  threadActions: result.threadActions,
10080
10202
  redactor: options.secretRedactor
10081
10203
  })
10082
10204
  };
10083
10205
  }
10206
+ function verifierRunSummary(options) {
10207
+ const { agentRuns = 0, inputTokens = 0, outputTokens = 0, costUsd = 0, usageStatus = "unavailable" } = options.stats ?? {};
10208
+ const models = options.providerModels.length ? options.providerModels : [options.fallbackModel];
10209
+ return {
10210
+ ...options.run,
10211
+ baseSha: options.event.change.base.sha,
10212
+ headSha: options.event.change.head.sha,
10213
+ tasks: ["pipr-internal-verifier"],
10214
+ durationMs: options.durationMs,
10215
+ models,
10216
+ agentRuns,
10217
+ inputTokens,
10218
+ outputTokens,
10219
+ costUsd,
10220
+ usageStatus
10221
+ };
10222
+ }
10084
10223
  function runnableReviewCommentReply(reply) {
10085
10224
  if (reply.action !== "created") return {
10086
10225
  kind: "ignored",
@@ -10213,7 +10352,8 @@ async function runLocalReviewCommand(options) {
10213
10352
  includeWorkingTree: true
10214
10353
  }) : void 0,
10215
10354
  log,
10216
- taskLog: options.taskLog
10355
+ taskLog: options.taskLog,
10356
+ runTrigger: "local"
10217
10357
  });
10218
10358
  if (result.kind === "command-response") throw new Error("command response result is only supported for issue_comment commands");
10219
10359
  log?.notice("local review complete", {
@@ -10265,6 +10405,6 @@ async function runHostRunCommandWithDependencies(options) {
10265
10405
  });
10266
10406
  }
10267
10407
  //#endregion
10268
- export { piReadOnlyToolNames as _, runInspectCommand as a, createGitLabClient as c, PublicationError as d, createAzureDevOpsClient as f, piBuiltinToolNames as g, supportedOfficialInitAdapters as h, runInitCommand as i, createGitHubHostAdapter as l, createKnownSecretRedactor as m, runHostRunCommand as n, runLocalReviewCommand as o, azureOrganizationFromUrl as p, runHostRunCommandWithDependencies as r, runValidateCommand as s, runDryRunCommand as t, createBitbucketClient as u, piRequiredCliFlags as v, piThinkingLevels as y };
10408
+ export { piReadOnlyToolNames as _, runInspectCommand as a, createGitLabClient as c, createAzureDevOpsClient as d, azureOrganizationFromUrl as f, piBuiltinToolNames as g, supportedOfficialInitAdapters as h, runInitCommand as i, createGitHubHostAdapter as l, createKnownSecretRedactor as m, runHostRunCommand as n, runLocalReviewCommand as o, PublicationError as p, runHostRunCommandWithDependencies as r, runValidateCommand as s, runDryRunCommand as t, createBitbucketClient as u, piRequiredCliFlags as v, piThinkingLevels as y };
10269
10409
 
10270
- //# sourceMappingURL=commands-B9qNW4pk.mjs.map
10410
+ //# sourceMappingURL=commands-DgZU7tf7.mjs.map