@jphutchins/code-review 0.1.0-alpha.67 → 0.1.0-alpha.69

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/README.md CHANGED
@@ -38,10 +38,10 @@ npx @jphutchins/code-review <subcommand>
38
38
  | Subcommand | What it does |
39
39
  | --- | --- |
40
40
  | `post` | Post a complete review from findings + envelope + diff — the one-call path. The findings are listed in the sticky summary; `--inline` renders the in-diff ones as comments on the diff instead. Also renders the review into `$GITHUB_STEP_SUMMARY`: the sticky is overwritten each round, the run summary keeps each round as it stood |
41
- | `gather` | Resolve the PR from the CI head SHA and gather the review inputs (diff with git-diff fallback, PR context, prior bot review, the implementer's answers (`Review-Response:` lines and verdict tables), failing-job logs) into the directory named by `--out-dir` (default: the current directory; the shipped workflows use `$RUNNER_TEMP/gather`) |
41
+ | `gather` | Resolve the PR from the CI head SHA and gather the review inputs (diff with git-diff fallback, PR context, prior bot review, the implementer's answers (`Review-Response:` lines and verdict tables), the CI run's job logs — the failing jobs', up to 20, on a failure, the jobs that ran, up to 64, on a pass) into the directory named by `--out-dir` (default: the current directory; the shipped workflows use `$RUNNER_TEMP/gather`) |
42
42
  | `parse-command` | Resolve a PR's head from its number and parse a ChatOps trigger comment (`/code-review [24m] [$1.00] <instructions>`) into review overrides — the on-demand comment trigger |
43
43
  | `react` | Add/remove a GitHub comment reaction — the ChatOps acknowledgement (👀 on receipt, 🚀 on completion) |
44
- | `await-ci` | Wait for a PR head's CI run to conclude and emit its real conclusion + run id — so an on-demand comment review routes on the CI result (success → full, failure → mechanic) instead of reviewing blind |
44
+ | `await-ci` | Wait for a PR head's CI run to conclude and emit its real conclusion, run id and run attempt — so an on-demand comment review routes on the CI result (success → full, failure → mechanic) instead of reviewing blind |
45
45
  | `check-scope` | Validate + normalize the workflow's `scope` input (the languages/inputs the project accepts) and print the normalized list for the review prompt, or nothing when empty (the reviewer then infers scope from the README's first paragraph); a structurally malformed value is rejected rather than spliced in as-is |
46
46
  | `render` | Render the sticky-comment markdown from findings + usage + prices |
47
47
  | `inline` | Build the GitHub reviews `comments[]` payload from findings + diff (in-diff validation; strays demote to the summary) |
package/dist/index.js CHANGED
@@ -574,7 +574,7 @@ var incompleteFindings = (summary) => ({
574
574
  var isIncompleteFindings = (findings) => findings.verdict === "error" && findings.findings.length === 0;
575
575
 
576
576
  // src/released.ts
577
- var RELEASED = "2026-10-10";
577
+ var RELEASED = "2026-10-11";
578
578
 
579
579
  // src/cost.ts
580
580
  var defaultWarn = (message) => {
@@ -2373,6 +2373,7 @@ var render = (input) => {
2373
2373
  answeredReRaiseNote: input.answeredReRaiseNote ?? "",
2374
2374
  contested: contestedBudget.kept,
2375
2375
  unfinished: shownUnfinished(input.findings, isFullReviewRound),
2376
+ unfinishedCaveat: unfinished.length > 0 ? ` \u2014 ${String(unfinished.length)} planned check${unfinished.length === 1 ? "" : "s"} went unfinished` : "",
2376
2377
  reviewUrl: input.reviewUrl ?? null,
2377
2378
  formatTokens: (n) => Number.isFinite(n) && n >= 0 ? n.toLocaleString("en-US") : "\u2014",
2378
2379
  // N/A (never a false $0.00) when no real price map was provided — real tokens, no rates to
@@ -2788,7 +2789,7 @@ var timeClause = (i) => i.elapsedMs === null ? null : i.wallMs !== null && i.wal
2788
2789
  var directive = (phase, draftPath, isSubagent) => {
2789
2790
  if (isSubagent)
2790
2791
  return phase.kind === "hard" ? `Budget nearly exhausted \u2014 STOP all new investigation now and report the findings you have back to the main agent in your reply, naming each check you planned but did not finish. Do not write ${draftPath} yourself; only the main agent writes it.` : `Wind down investigation and report the findings you have back to the main agent in your reply, naming each check you planned but did not finish \u2014 do not write ${draftPath} yourself; the main agent writes it, and you may run out of budget otherwise.`;
2791
- return phase.kind === "hard" ? `Budget nearly exhausted \u2014 STOP all new investigation now. Do not wait for subagents still running in the background. Write your COMPLETE findings from what you already have to ${draftPath}, listing in its \`unfinished\` field, when its schema has one, every check you or a subagent planned but did not finish, and run \`code-review validate ${draftPath} --explain\` until it passes (--explain prints the exact schema when the shape is wrong). Other tools are blocked until that draft is written.` : `Wind down investigation and write your COMPLETE findings to ${draftPath} now, listing in its \`unfinished\` field, when its schema has one, every check you or a subagent planned but did not finish, then run \`code-review validate ${draftPath} --explain\` (it prints the exact schema if the shape is wrong) \u2014 fold in the subagent reports you already have rather than waiting on stragglers; you may run out of budget before you finish otherwise.`;
2792
+ return phase.kind === "hard" ? `Budget nearly exhausted \u2014 STOP all new investigation now. Do not wait for subagents still running in the background. Write your COMPLETE findings from what you already have to ${draftPath}, listing in its \`unfinished\` field, when its schema has one, every check you or a subagent planned but did not finish (never one a CI job already ran and passed), and run \`code-review validate ${draftPath} --explain\` until it passes (--explain prints the exact schema when the shape is wrong). Other tools are blocked until that draft is written.` : `Wind down investigation and write your COMPLETE findings to ${draftPath} now, listing in its \`unfinished\` field, when its schema has one, every check you or a subagent planned but did not finish (never one a CI job already ran and passed), then run \`code-review validate ${draftPath} --explain\` (it prints the exact schema if the shape is wrong) \u2014 fold in the subagent reports you already have rather than waiting on stragglers; you may run out of budget before you finish otherwise.`;
2792
2793
  };
2793
2794
  var budgetMessage = (i, phase, draftPath, isSubagent) => {
2794
2795
  const status = [spendClause(i), timeClause(i)].filter((c) => c !== null).join(" \xB7 ");
@@ -5058,14 +5059,17 @@ var react = async (input, ghApi = runGhApi) => {
5058
5059
  await removeReactions(input.repo, input.commentId, input.remove, ghApi);
5059
5060
  }
5060
5061
  };
5061
- var RunCodec = t.type({
5062
- id: t.number,
5063
- name: t.union([t.string, t.null]),
5064
- status: t.union([t.string, t.null]),
5065
- conclusion: t.union([t.string, t.null]),
5066
- run_number: t.number
5067
- });
5068
- var RUN_JQ = ".workflow_runs[] | {id: .id, name: .name, status: .status, conclusion: .conclusion, run_number: .run_number}";
5062
+ var RunCodec = t.intersection([
5063
+ t.type({
5064
+ id: t.number,
5065
+ name: t.union([t.string, t.null]),
5066
+ status: t.union([t.string, t.null]),
5067
+ conclusion: t.union([t.string, t.null]),
5068
+ run_number: t.number
5069
+ }),
5070
+ t.partial({ run_attempt: t.union([t.number, t.null]) })
5071
+ ]);
5072
+ var RUN_JQ = ".workflow_runs[] | {id: .id, name: .name, status: .status, conclusion: .conclusion, run_number: .run_number, run_attempt: .run_attempt}";
5069
5073
  var resolveCiRun = async (repo, headSha, workflowName, ghApi) => {
5070
5074
  const endpoint = `repos/${repo}/actions/runs?head_sha=${headSha}&per_page=100`;
5071
5075
  const rows = parseJsonl(await ghApi([endpoint, "--paginate", "--jq", RUN_JQ]));
@@ -5085,7 +5089,12 @@ var resolveCiRun = async (repo, headSha, workflowName, ghApi) => {
5085
5089
  null
5086
5090
  );
5087
5091
  return {
5088
- run: latest === null ? null : { id: latest.id, status: latest.status ?? "unknown", conclusion: latest.conclusion },
5092
+ run: latest === null ? null : {
5093
+ id: latest.id,
5094
+ status: latest.status ?? "unknown",
5095
+ conclusion: latest.conclusion,
5096
+ attempt: latest.run_attempt ?? null
5097
+ },
5089
5098
  seenNames: [...new Set(runs.flatMap((r) => r.name === null ? [] : [r.name]))]
5090
5099
  };
5091
5100
  };
@@ -5104,7 +5113,12 @@ var awaitCiConclusion = async (repo, headSha, options, deps = { ghApi: runGhApi,
5104
5113
  const poll = async (lastSeenNames, lastRunId) => {
5105
5114
  const { run: run2, seenNames } = await safeResolve();
5106
5115
  if (run2 !== null && run2.status === "completed" && run2.conclusion !== null)
5107
- return { kind: "concluded", conclusion: run2.conclusion, runId: run2.id };
5116
+ return {
5117
+ kind: "concluded",
5118
+ conclusion: run2.conclusion,
5119
+ runId: run2.id,
5120
+ runAttempt: run2.attempt
5121
+ };
5108
5122
  const runId = run2 === null ? lastRunId : run2.id;
5109
5123
  const names = seenNames.length > 0 ? seenNames : lastSeenNames;
5110
5124
  if (deps.elapsedMs() >= options.timeoutMs)
@@ -5122,6 +5136,7 @@ var monotonicElapsed = () => {
5122
5136
  var renderCiOutputs = (outcome) => outcome.kind === "concluded" ? `ci_settled=true
5123
5137
  ci_conclusion=${outcome.conclusion}
5124
5138
  ci_run_id=${String(outcome.runId)}
5139
+ ci_run_attempt=${outcome.runAttempt === null ? "" : String(outcome.runAttempt)}
5125
5140
  ` : `ci_settled=false
5126
5141
  ci_run_id=${outcome.runId === null ? "" : String(outcome.runId)}
5127
5142
  `;
@@ -5137,6 +5152,8 @@ stacked=${String(result.stacked)}
5137
5152
  base_sha=${result.baseSha}
5138
5153
  staged_job_logs=${String(result.stagedJobLogs)}
5139
5154
  failing_jobs=${String(result.failingJobs)}
5155
+ ci_jobs_passed=${result.passedJobs === null ? "" : String(result.passedJobs.passed)}
5156
+ ci_jobs_ran=${result.passedJobs === null ? "" : String(result.passedJobs.ran)}
5140
5157
  `;
5141
5158
  }
5142
5159
  };
@@ -5174,14 +5191,25 @@ var IssueCommentCodec = t.intersection([
5174
5191
  author_association: t.union([t.string, t.null])
5175
5192
  })
5176
5193
  ]);
5177
- var JobCodec = t.type({
5178
- id: t.number,
5179
- // A null name (an API edge) degrades to a nameless entry rather than aborting the whole gather —
5180
- // the identity the reproduction steer needs is best-effort, never load-bearing.
5181
- name: t.union([t.string, t.null]),
5182
- conclusion: t.union([t.string, t.null])
5183
- });
5184
- var JOBS_JQ = ".jobs[] | {id: .id, name: .name, conclusion: .conclusion}";
5194
+ var JobCodec = t.intersection([
5195
+ t.type({
5196
+ id: t.number,
5197
+ // A null name (an API edge) degrades to a nameless entry rather than aborting the whole gather —
5198
+ // the identity the reproduction steer needs is best-effort, never load-bearing.
5199
+ name: t.union([t.string, t.null]),
5200
+ conclusion: t.union([t.string, t.null])
5201
+ }),
5202
+ t.partial({ steps: t.array(t.string) })
5203
+ ]);
5204
+ var JOBS_JQ = ".jobs[] | {id: .id, name: .name, conclusion: .conclusion, steps: [.steps[]? | .name? | strings]}";
5205
+ var parseRunAttempt = (raw) => {
5206
+ if (raw === void 0 || raw === "") return void 0;
5207
+ if (!/^[1-9][0-9]*$/.test(raw)) {
5208
+ throw new Error(`--run-attempt must be a positive integer, got ${JSON.stringify(raw)}`);
5209
+ }
5210
+ return raw;
5211
+ };
5212
+ var jobsEndpoint = (repo, runId, runAttempt) => runAttempt === void 0 ? `repos/${repo}/actions/runs/${runId}/jobs` : `repos/${repo}/actions/runs/${runId}/attempts/${runAttempt}/jobs`;
5185
5213
  var fetchPrMeta = async (repo, prNumber, ghApi) => {
5186
5214
  const stdout = await ghApi([
5187
5215
  `repos/${repo}/pulls/${String(prNumber)}`,
@@ -5331,35 +5359,46 @@ var reviewsFrom = (reviews, botLogin) => boundedHuman(reviews, botLogin, "review
5331
5359
  body: clip(r.body)
5332
5360
  }));
5333
5361
  var MAX_STAGED_JOB_LOGS = 20;
5334
- var downloadFailingJobLogs = async (repo, runId, outDir, ghApi) => {
5335
- const rows = parseJsonl(
5336
- await ghApi([`repos/${repo}/actions/runs/${runId}/jobs`, "--paginate", "--jq", JOBS_JQ])
5362
+ var stageJobLog = async (repo, jobId, outDir, ghApi) => {
5363
+ try {
5364
+ const log = await ghApi([`repos/${repo}/actions/jobs/${String(jobId)}/logs`]);
5365
+ if (log.trim() === "no log available" || log.trim() === "") {
5366
+ process.stderr.write(
5367
+ `Warning: job ${String(jobId)} has no log available (placeholder body) \u2014 not staged
5368
+ `
5369
+ );
5370
+ return null;
5371
+ }
5372
+ const file = `job_${String(jobId)}.log`;
5373
+ writeFileSync(join(outDir, file), log);
5374
+ return file;
5375
+ } catch (err) {
5376
+ process.stderr.write(
5377
+ `Warning: failed to download logs for job ${String(jobId)}: ${errMsg(err)} \u2014 continuing with the logs retrieved so far
5378
+ `
5379
+ );
5380
+ return null;
5381
+ }
5382
+ };
5383
+ var listRunJobs = async (repo, runId, runAttempt, ghApi) => {
5384
+ const decoded = t.array(JobCodec).decode(
5385
+ parseJsonl(
5386
+ await ghApi([jobsEndpoint(repo, runId, runAttempt), "--paginate", "--jq", JOBS_JQ])
5387
+ )
5337
5388
  );
5338
- const decoded = t.array(JobCodec).decode(rows);
5339
5389
  if (decoded._tag === "Left") {
5340
5390
  throw new Error(`Jobs list for run ${runId} did not match the expected shape`);
5341
5391
  }
5342
- const failing = decoded.right.filter((j) => j.conclusion === "failure");
5392
+ return decoded.right;
5393
+ };
5394
+ var downloadFailingJobLogs = async (repo, runId, runAttempt, outDir, ghApi) => {
5395
+ const failing = (await listRunJobs(repo, runId, runAttempt, ghApi)).filter(
5396
+ (j) => j.conclusion === "failure"
5397
+ );
5343
5398
  const selected = failing.slice(0, MAX_STAGED_JOB_LOGS);
5344
5399
  let staged = 0;
5345
5400
  for (const job of selected) {
5346
- try {
5347
- const log = await ghApi([`repos/${repo}/actions/jobs/${String(job.id)}/logs`]);
5348
- if (log.trim() === "no log available" || log.trim() === "") {
5349
- process.stderr.write(
5350
- `Warning: job ${String(job.id)} has no log available (placeholder body) \u2014 not staged
5351
- `
5352
- );
5353
- continue;
5354
- }
5355
- writeFileSync(join(outDir, `job_${String(job.id)}.log`), log);
5356
- staged += 1;
5357
- } catch (err) {
5358
- process.stderr.write(
5359
- `Warning: failed to download logs for job ${String(job.id)}: ${errMsg(err)} \u2014 continuing with the logs retrieved so far
5360
- `
5361
- );
5362
- }
5401
+ if (await stageJobLog(repo, job.id, outDir, ghApi) !== null) staged += 1;
5363
5402
  }
5364
5403
  writeFileSync(
5365
5404
  join(outDir, "failed_jobs.json"),
@@ -5387,6 +5426,65 @@ var downloadFailingJobLogs = async (repo, runId, outDir, ghApi) => {
5387
5426
  }
5388
5427
  return { staged, failing: failing.length };
5389
5428
  };
5429
+ var MAX_STAGED_PASSED_JOB_LOGS = 64;
5430
+ var PASSED_LOG_CONCURRENCY = 8;
5431
+ var stagePassedRun = async (repo, runId, runAttempt, outDir, ghApi) => {
5432
+ try {
5433
+ const jobs = await listRunJobs(repo, runId, runAttempt, ghApi);
5434
+ const writeIndex = (logs2) => {
5435
+ writeFileSync(
5436
+ join(outDir, "ci_jobs.json"),
5437
+ `${JSON.stringify(
5438
+ jobs.map((j) => ({
5439
+ name: j.name,
5440
+ conclusion: j.conclusion,
5441
+ steps: j.steps ?? [],
5442
+ log: logs2.get(j.id) ?? null
5443
+ })),
5444
+ null,
5445
+ 2
5446
+ )}
5447
+ `
5448
+ );
5449
+ };
5450
+ writeIndex(/* @__PURE__ */ new Map());
5451
+ const ran = jobs.filter((j) => j.conclusion !== "skipped");
5452
+ const selected = ran.slice(0, MAX_STAGED_PASSED_JOB_LOGS);
5453
+ const logs = await Array.from(
5454
+ { length: Math.ceil(selected.length / PASSED_LOG_CONCURRENCY) },
5455
+ (_, i) => selected.slice(i * PASSED_LOG_CONCURRENCY, (i + 1) * PASSED_LOG_CONCURRENCY)
5456
+ ).reduce(async (staged, batch) => {
5457
+ const prior = await staged;
5458
+ const files = await Promise.all(
5459
+ batch.map(async (job) => [job.id, await stageJobLog(repo, job.id, outDir, ghApi)])
5460
+ );
5461
+ return new Map([
5462
+ ...prior,
5463
+ ...files.flatMap(([id, file]) => file === null ? [] : [[id, file]])
5464
+ ]);
5465
+ }, Promise.resolve(/* @__PURE__ */ new Map()));
5466
+ writeIndex(logs);
5467
+ if (ran.length > selected.length) {
5468
+ process.stderr.write(
5469
+ `::warning::${annotationSafe(`${String(ran.length)} jobs ran in the passed run ${runId}; logs were staged for the first ${String(selected.length)} only`)}
5470
+ `
5471
+ );
5472
+ }
5473
+ return {
5474
+ staged: logs.size,
5475
+ passed: {
5476
+ passed: jobs.filter((j) => j.conclusion === "success").length,
5477
+ ran: ran.length
5478
+ }
5479
+ };
5480
+ } catch (err) {
5481
+ process.stderr.write(
5482
+ `::warning::${annotationSafe(`the passed CI run's jobs could not be staged (${errMsg(err)}) \u2014 the review knows CI passed, not what its jobs reported`)}
5483
+ `
5484
+ );
5485
+ return { staged: 0, passed: null };
5486
+ }
5487
+ };
5390
5488
  var gather = async (input, ghApi = runGhApi, gitRun = runGit, readArtifact = ghArtifactReader) => {
5391
5489
  const candidates = await fetchPrCandidates(input.repo, input.headSha, ghApi);
5392
5490
  const resolution = resolvePr(candidates, input.headBranch);
@@ -5490,7 +5588,8 @@ var gather = async (input, ghApi = runGhApi, gitRun = runGit, readArtifact = ghA
5490
5588
  reviews: reviews === null ? [] : reviewsFrom(reviews, input.botLogin)
5491
5589
  })
5492
5590
  );
5493
- const jobLogs = input.conclusion === "failure" ? await downloadFailingJobLogs(input.repo, input.runId, input.outDir, ghApi) : { staged: 0, failing: 0 };
5591
+ const jobLogs = input.conclusion === "failure" ? await downloadFailingJobLogs(input.repo, input.runId, input.runAttempt, input.outDir, ghApi) : { staged: 0, failing: 0 };
5592
+ const passedRun = input.conclusion === "success" ? await stagePassedRun(input.repo, input.runId, input.runAttempt, input.outDir, ghApi) : { staged: 0, passed: null };
5494
5593
  return {
5495
5594
  kind: "gathered",
5496
5595
  pr: prNumber,
@@ -5498,8 +5597,9 @@ var gather = async (input, ghApi = runGhApi, gitRun = runGit, readArtifact = ghA
5498
5597
  diffSize: Buffer.byteLength(prDiff, "utf8"),
5499
5598
  stacked,
5500
5599
  baseSha: meta.base_sha,
5501
- stagedJobLogs: jobLogs.staged,
5502
- failingJobs: jobLogs.failing
5600
+ stagedJobLogs: jobLogs.staged + passedRun.staged,
5601
+ failingJobs: jobLogs.failing,
5602
+ passedJobs: passedRun.passed
5503
5603
  };
5504
5604
  };
5505
5605
 
@@ -7081,7 +7181,7 @@ var stopGateCmd = defineCommand({
7081
7181
  var gatherCmd = defineCommand({
7082
7182
  meta: {
7083
7183
  name: "gather",
7084
- description: "Resolve the PR from the CI head SHA and gather review inputs (diff with git-diff fallback, PR context, prior bot review, untrusted PR conversation to triage, failing-job logs and the failing jobs' identities in failed_jobs.json) as files for the review agent"
7184
+ description: "Resolve the PR from the CI head SHA and gather review inputs (diff with git-diff fallback, PR context, prior bot review, untrusted PR conversation to triage, the CI run's job logs \u2014 the failing jobs' with their identities in failed_jobs.json on a failure, the jobs that ran, up to 64, indexed in ci_jobs.json on a pass) as files for the review agent"
7085
7185
  },
7086
7186
  args: {
7087
7187
  repo: { type: "string", description: "Repository (owner/name)", required: true },
@@ -7101,12 +7201,16 @@ var gatherCmd = defineCommand({
7101
7201
  },
7102
7202
  "run-id": {
7103
7203
  type: "string",
7104
- description: "CI run id (from workflow_run.id); its failing jobs' logs are downloaded on failure",
7204
+ description: "CI run id (from workflow_run.id); its job logs are downloaded \u2014 the failing jobs' on failure, the jobs that ran (up to 64) on success",
7105
7205
  required: true
7106
7206
  },
7207
+ "run-attempt": {
7208
+ type: "string",
7209
+ description: "The CI run's attempt that triggered the review (from workflow_run.run_attempt); its jobs' logs are the ones staged, even after a re-run starts a newer attempt. Empty or omitted: the run's latest attempt"
7210
+ },
7107
7211
  conclusion: {
7108
7212
  type: "string",
7109
- description: "CI conclusion (e.g. success | failure); failure triggers failing-job log download",
7213
+ description: "CI conclusion (e.g. success | failure); failure stages the failing jobs' logs, success the logs of the jobs that ran (up to 64) and ci_jobs.json",
7110
7214
  required: true
7111
7215
  },
7112
7216
  "bot-login": {
@@ -7125,6 +7229,7 @@ var gatherCmd = defineCommand({
7125
7229
  headBranch: args["head-branch"],
7126
7230
  defaultBranch: args["default-branch"],
7127
7231
  runId: args["run-id"],
7232
+ runAttempt: parseRunAttempt(args["run-attempt"]),
7128
7233
  conclusion: args.conclusion,
7129
7234
  botLogin: args["bot-login"] || "github-actions[bot]",
7130
7235
  outDir: args["out-dir"] ? resolve(args["out-dir"]) : process.cwd()
@@ -7522,7 +7627,7 @@ var reactCmd = defineCommand({
7522
7627
  var awaitCiCmd = defineCommand({
7523
7628
  meta: {
7524
7629
  name: "await-ci",
7525
- description: "Wait for the PR head's CI workflow run to conclude, then emit its REAL conclusion + run id to $GITHUB_OUTPUT (ci_settled, ci_conclusion, ci_run_id). The on-demand comment trigger uses this so it routes on the same CI result the CI-completion trigger would \u2014 success \u2192 full review, failure \u2192 mechanic with that run's logs \u2014 instead of reviewing blind. Polls until the run completes or the timeout elapses; ci_settled=false \u21D2 no conclusive result (caller should decline to review, not guess)."
7630
+ description: "Wait for the PR head's CI workflow run to conclude, then emit its REAL conclusion, run id and run attempt to $GITHUB_OUTPUT (ci_settled, ci_conclusion, ci_run_id, ci_run_attempt). The on-demand comment trigger uses this so it routes on the same CI result the CI-completion trigger would \u2014 success \u2192 full review, failure \u2192 mechanic with that run's logs \u2014 instead of reviewing blind. Polls until the run completes or the timeout elapses; ci_settled=false \u21D2 no conclusive result (caller should decline to review, not guess)."
7526
7631
  },
7527
7632
  args: {
7528
7633
  repo: { type: "string", description: "Repository (owner/name)", required: true },