@weareikko/code-review 0.8.5 → 0.9.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.
@@ -166,6 +166,7 @@ var REVIEW_THREADS_QUERY = `
166
166
  pageInfo { hasNextPage endCursor }
167
167
  nodes {
168
168
  isResolved
169
+ isOutdated
169
170
  comments(first: 100) { nodes { databaseId } }
170
171
  }
171
172
  }
@@ -338,14 +339,22 @@ var GitHubClient = class {
338
339
  return parsed.data;
339
340
  }
340
341
  /**
341
- * Return the database IDs of review comments that belong to a **resolved**
342
- * review thread. GitHub's REST comment endpoints omit thread-resolution state;
343
- * it is only exposed via GraphQL `reviewThreads.isResolved`. Callers use this
344
- * set to mark normalized notes resolved so resolved threads are excluded from
345
- * summary carry-over and prior-thread context. Paginates over threads.
342
+ * Return the database IDs of review comments that belong to a **settled**
343
+ * review thread — one that is either resolved or outdated. GitHub's REST
344
+ * comment endpoints omit both states; they are only exposed via GraphQL
345
+ * `reviewThreads.isResolved` / `isOutdated`. Callers use this set to mark
346
+ * normalized notes resolved so settled threads are excluded from summary
347
+ * carry-over and prior-thread context. Paginates over threads.
348
+ *
349
+ * Outdated counts as settled because GitHub, unlike GitLab, does not
350
+ * auto-resolve a thread when the line it anchors to changes: fixing a finding
351
+ * flips the thread to outdated but leaves `isResolved` false until someone
352
+ * manually resolves it. Treating outdated as settled mirrors GitLab's
353
+ * "automatically resolve outdated diff discussions" behaviour, so a fixed
354
+ * finding stops being re-listed under "Still open from earlier reviews" (#133).
346
355
  */
347
- async listResolvedReviewCommentIds(owner, repo, pull) {
348
- const resolved = /* @__PURE__ */ new Set();
356
+ async listSettledReviewCommentIds(owner, repo, pull) {
357
+ const settled = /* @__PURE__ */ new Set();
349
358
  let cursor = null;
350
359
  let hasNext = true;
351
360
  while (hasNext) {
@@ -357,14 +366,14 @@ var GitHubClient = class {
357
366
  })).repository?.pullRequest?.reviewThreads;
358
367
  if (!threads) break;
359
368
  for (const thread of threads.nodes ?? []) {
360
- if (!thread.isResolved) continue;
361
- for (const comment of thread.comments?.nodes ?? []) if (typeof comment.databaseId === "number") resolved.add(comment.databaseId);
369
+ if (!thread.isResolved && !thread.isOutdated) continue;
370
+ for (const comment of thread.comments?.nodes ?? []) if (typeof comment.databaseId === "number") settled.add(comment.databaseId);
362
371
  }
363
372
  hasNext = threads.pageInfo?.hasNextPage ?? false;
364
373
  cursor = threads.pageInfo?.endCursor ?? null;
365
374
  if (!cursor) hasNext = false;
366
375
  }
367
- return resolved;
376
+ return settled;
368
377
  }
369
378
  };
370
379
  //#endregion
@@ -551,7 +560,7 @@ function buildSummaryBody(summary, costFooter, options = {}) {
551
560
  return `${withFooter}\n\n${buildSummaryHistoryBlock(historyEntries)}`;
552
561
  }
553
562
  function buildReviewedCommitFooter(commitSha) {
554
- return `Reviewed by ${PRODUCT_LINK} v0.8.5 for commit ${commitSha}.`;
563
+ return `Reviewed by ${PRODUCT_LINK} v0.9.0 for commit ${commitSha}.`;
555
564
  }
556
565
  function extractReviewedCommitSha(body) {
557
566
  return REVIEWED_COMMIT_FOOTER_PATTERN.exec(body)?.[1] ?? null;
@@ -1350,6 +1359,7 @@ function createDiagnosticContext(phase, config, runId, overrides = {}) {
1350
1359
  project: config.project,
1351
1360
  mr: config.mr,
1352
1361
  gitlabUrl: config.gitlabUrl,
1362
+ platform: config.platform,
1353
1363
  cwd: config.cwd,
1354
1364
  model: config.model,
1355
1365
  minSeverity: config.minSeverity,
@@ -3681,8 +3691,15 @@ async function loadReviewContext(cwd, skillNames = [], warn, options = {}) {
3681
3691
  ]);
3682
3692
  const skills = [...discovered];
3683
3693
  const discoveredNames = new Set(discovered.map((s) => s.name));
3684
- const named = await Promise.all(skillNames.filter((n) => !discoveredNames.has(n)).map((n) => loadNamedSkill(n, cwd, { refresh: options.refreshGitSkills })));
3685
- skills.push(...named);
3694
+ const named = await Promise.all(skillNames.filter((n) => !discoveredNames.has(n)).map(async (n) => {
3695
+ try {
3696
+ return await loadNamedSkill(n, cwd, { refresh: options.refreshGitSkills });
3697
+ } catch (error) {
3698
+ warn?.(`Skipping skill "${n}": ${formatError(error)}`);
3699
+ return null;
3700
+ }
3701
+ }));
3702
+ skills.push(...named.filter((s) => s !== null));
3686
3703
  return {
3687
3704
  conventions,
3688
3705
  reviewRules,
@@ -4338,54 +4355,59 @@ async function runReview(config, options) {
4338
4355
  attachTelemetry: options.attachTelemetry,
4339
4356
  verifyStaged: diskMode ? retrievableSkipped : void 0
4340
4357
  };
4358
+ const buildUsage = () => ({
4359
+ model: config.model,
4360
+ thinkingLevel: config.thinkingLevel,
4361
+ tokens: aggregated.tokens,
4362
+ cost: aggregated.cost,
4363
+ byModel: buildByModelUsage(aggregated),
4364
+ skills: context.skills.map((s) => s.name),
4365
+ sizeNotice
4366
+ });
4341
4367
  let outputText;
4342
- if (config.reviewDepth === "full") {
4343
- const { findings, summary } = await runMultiAngleFind(context, minSeverity, userPrompt, deps);
4344
- outputText = await verifyAndSynthesize(findings, summary, diff, options.commitLog, deps);
4345
- } else {
4346
- const findAgent = createAgent({
4347
- systemPrompt,
4348
- model: primary.model,
4349
- tools,
4350
- thinkingLevel: config.thinkingLevel,
4351
- getApiKey: primary.getApiKey
4352
- });
4353
- const detachTelemetry = options.attachTelemetry?.(findAgent);
4354
- let turnCount = 0;
4355
- let toolCallCount = 0;
4356
- let finalText;
4357
- try {
4358
- finalText = await runAgentToCompletion(findAgent, userPrompt, {
4359
- timeoutMs,
4360
- onAssistantMessage: (message) => accumulateUsage(aggregated, message, primary.id),
4361
- onTurnStart: (turn) => {
4362
- turnCount = turn;
4363
- logger.debug(`Turn ${turn} started`);
4364
- },
4365
- onToolStart: (toolName, args) => {
4366
- toolCallCount += 1;
4367
- logger.debug(` → ${toolName}${formatToolArgs(toolName, args)}`);
4368
- }
4368
+ try {
4369
+ if (config.reviewDepth === "full") {
4370
+ const { findings, summary } = await runMultiAngleFind(context, minSeverity, userPrompt, deps);
4371
+ outputText = await verifyAndSynthesize(findings, summary, diff, options.commitLog, deps);
4372
+ } else {
4373
+ const findAgent = createAgent({
4374
+ systemPrompt,
4375
+ model: primary.model,
4376
+ tools,
4377
+ thinkingLevel: config.thinkingLevel,
4378
+ getApiKey: primary.getApiKey
4369
4379
  });
4370
- } finally {
4371
- detachTelemetry?.();
4380
+ const detachTelemetry = options.attachTelemetry?.(findAgent);
4381
+ let turnCount = 0;
4382
+ let toolCallCount = 0;
4383
+ let finalText;
4384
+ try {
4385
+ finalText = await runAgentToCompletion(findAgent, userPrompt, {
4386
+ timeoutMs,
4387
+ onAssistantMessage: (message) => accumulateUsage(aggregated, message, primary.id),
4388
+ onTurnStart: (turn) => {
4389
+ turnCount = turn;
4390
+ logger.debug(`Turn ${turn} started`);
4391
+ },
4392
+ onToolStart: (toolName, args) => {
4393
+ toolCallCount += 1;
4394
+ logger.debug(` → ${toolName}${formatToolArgs(toolName, args)}`);
4395
+ }
4396
+ });
4397
+ } finally {
4398
+ detachTelemetry?.();
4399
+ }
4400
+ logger.debug(`Agent finished: ${turnCount} turn(s), ${toolCallCount} tool call(s)`);
4401
+ outputText = config.reviewDepth === "verify" ? await runVerifyStage(finalText, diff, options.commitLog, deps) : finalText;
4372
4402
  }
4373
- logger.debug(`Agent finished: ${turnCount} turn(s), ${toolCallCount} tool call(s)`);
4374
- outputText = config.reviewDepth === "verify" ? await runVerifyStage(finalText, diff, options.commitLog, deps) : finalText;
4403
+ } finally {
4404
+ options.onUsage?.(buildUsage());
4375
4405
  }
4376
4406
  const reviewPath = resolve(cwd, config.reviewFile);
4377
4407
  await mkdir(dirname(reviewPath), { recursive: true });
4378
4408
  await writeFile(reviewPath, outputText, "utf8");
4379
4409
  if (retrievableSkipped.length > 0) await cleanupSkippedDiffs(cwd);
4380
- return {
4381
- model: config.model,
4382
- thinkingLevel: config.thinkingLevel,
4383
- tokens: aggregated.tokens,
4384
- cost: aggregated.cost,
4385
- byModel: buildByModelUsage(aggregated),
4386
- skills: context.skills.map((s) => s.name),
4387
- sizeNotice
4388
- };
4410
+ return buildUsage();
4389
4411
  }
4390
4412
  /**
4391
4413
  * Convert the per-model usage buckets into the public {@link ModelUsage} array,
@@ -4742,13 +4764,13 @@ async function startOtelBridge(options = {}) {
4742
4764
  unit: "{token}",
4743
4765
  advice: { explicitBucketBoundaries: TOKEN_BUCKETS }
4744
4766
  });
4745
- const operationCost = meter.createHistogram("gen_ai.client.cost", {
4746
- description: "GenAI operation cost in USD",
4767
+ const operationCost = meter.createHistogram("code_review_llm_cost_usd", {
4768
+ description: "LLM cost in USD per turn, by token type (non-standard extension)",
4747
4769
  unit: "{usd}",
4748
4770
  advice: { explicitBucketBoundaries: COST_BUCKETS_USD }
4749
4771
  });
4750
- const timeToFirstToken = meter.createHistogram("gen_ai.client.time_to_first_token", {
4751
- description: "Time to first token from the LLM",
4772
+ const timeToFirstToken = meter.createHistogram("gen_ai.client.operation.time_to_first_chunk", {
4773
+ description: "Time to first chunk from the LLM",
4752
4774
  unit: "s",
4753
4775
  advice: { explicitBucketBoundaries: TTFT_BUCKETS_S }
4754
4776
  });
@@ -4788,6 +4810,9 @@ async function startOtelBridge(options = {}) {
4788
4810
  ciAttrs,
4789
4811
  ciSpanAttrs,
4790
4812
  model: ctx.model,
4813
+ dryRun: ctx.dryRun,
4814
+ platform: ctx.platform,
4815
+ serverAddress: hostOf(ctx.gitlabUrl),
4791
4816
  rootSpanCtx
4792
4817
  });
4793
4818
  emitReviewStartedLog(logger, ctx, ciAttrs, ciSpanAttrs, rootSpanCtx);
@@ -4819,50 +4844,55 @@ async function startOtelBridge(options = {}) {
4819
4844
  entry.span.end();
4820
4845
  entry.closed = true;
4821
4846
  const status = resolveRunStatus(ctx, isError);
4822
- const projectPath = ciAttrs["gitlab.project_path"] ?? "";
4847
+ const projectPath = ciAttrs["vcs.repository.name"] ?? "";
4848
+ const vcs = vcsAttrs(ctx.platform, ctx.gitlabUrl);
4823
4849
  if (typeof ctx.durationMs === "number") reviewPhaseDuration.record(ctx.durationMs / 1e3, {
4824
4850
  ...REVIEW_SERVICE_ATTRS,
4825
- "gitlab.project_path": projectPath,
4826
- "gitlab_review.phase": ctx.phase,
4827
- "gitlab_review.status": status
4851
+ ...vcs,
4852
+ "vcs.repository.name": projectPath,
4853
+ "code_review.phase": ctx.phase,
4854
+ "code_review.status": status
4828
4855
  });
4829
4856
  if (ctx.phase === ROOT_PHASE) {
4830
4857
  const meta = runMeta.get(ctx.runId);
4831
- const pipelineSource = ciAttrs["gitlab.pipeline_source"] ?? "";
4858
+ const pipelineSource = ciAttrs["cicd.pipeline.source"] ?? "";
4832
4859
  const runMetricBase = {
4833
4860
  ...REVIEW_SERVICE_ATTRS,
4834
- "gitlab.project_path": projectPath,
4835
- "gitlab_review.dry_run": ctx.dryRun
4861
+ ...vcs,
4862
+ "vcs.repository.name": projectPath,
4863
+ "code_review.dry_run": ctx.dryRun,
4864
+ ...ctx.firstReview !== void 0 ? { "code_review.first_review": ctx.firstReview } : {}
4836
4865
  };
4837
4866
  const usage = meta?.usage ?? ctx.usage;
4838
4867
  const runModelAttrs = genAiModelAttrs(void 0, splitModel(usage?.model ?? "").modelId);
4839
4868
  reviewRunsTotal.add(1, {
4840
4869
  ...runMetricBase,
4841
- "gitlab.pipeline_source": pipelineSource,
4842
- "gitlab_review.status": status
4870
+ "cicd.pipeline.source": pipelineSource,
4871
+ "code_review.status": status
4843
4872
  });
4844
4873
  if (isError) reviewErrorsTotal.add(1, {
4845
4874
  ...runMetricBase,
4846
- "gitlab_review.status": status,
4875
+ "code_review.status": status,
4847
4876
  "error.type": errorTypeOf(ctx)
4848
4877
  });
4849
4878
  if (typeof ctx.durationMs === "number") reviewRunDuration.record(ctx.durationMs / 1e3, {
4850
4879
  ...runMetricBase,
4851
4880
  ...runModelAttrs,
4852
- "gitlab.pipeline_source": pipelineSource,
4853
- "gitlab_review.status": status
4881
+ "cicd.pipeline.source": pipelineSource,
4882
+ "code_review.status": status
4854
4883
  });
4855
4884
  const totalCostUsd = usage?.cost.total;
4856
4885
  if (totalCostUsd !== void 0) reviewTotalCost.record(totalCostUsd, {
4857
4886
  ...runMetricBase,
4858
4887
  ...runModelAttrs,
4859
- "gitlab_review.status": status
4888
+ "code_review.status": status
4860
4889
  });
4861
4890
  if (usage) {
4862
4891
  const tokenAttrs = {
4863
4892
  ...REVIEW_SERVICE_ATTRS,
4864
4893
  ...runModelAttrs,
4865
- "gitlab.project_path": projectPath
4894
+ ...vcs,
4895
+ "vcs.repository.name": projectPath
4866
4896
  };
4867
4897
  for (const [field, type] of [
4868
4898
  ["input", "input"],
@@ -4878,7 +4908,7 @@ async function startOtelBridge(options = {}) {
4878
4908
  if (bySeverity) {
4879
4909
  for (const [severity, count] of Object.entries(bySeverity)) if (count && count > 0) reviewCommentsTotal.add(count, {
4880
4910
  ...runMetricBase,
4881
- "gitlab_review.comment.severity": severity
4911
+ "code_review.comment.severity": severity
4882
4912
  });
4883
4913
  } else {
4884
4914
  const posted = ctx.posted ?? 0;
@@ -4924,16 +4954,17 @@ async function startOtelBridge(options = {}) {
4924
4954
  context: meta?.rootSpanCtx,
4925
4955
  attributes: {
4926
4956
  "service.name": SERVICE_NAME,
4927
- "event.name": "gitlab_review.comment",
4928
- "gitlab_review.run_id": runId,
4929
- "gitlab_review.comment.file": comment.file,
4930
- "gitlab_review.comment.line": comment.line,
4931
- "gitlab_review.comment.severity": comment.severity,
4932
- "gitlab_review.comment.is_duplicate": duplicate,
4957
+ "event.name": "code_review.comment",
4958
+ "code_review.run_id": runId,
4959
+ "code_review.comment.file": comment.file,
4960
+ "code_review.comment.line": comment.line,
4961
+ "code_review.comment.severity": comment.severity,
4962
+ "code_review.comment.is_duplicate": duplicate,
4933
4963
  ...meta && {
4934
- "gitlab.project_id": meta.project,
4935
- "gitlab.mr_iid": meta.mr,
4936
- "gitlab.server_url": meta.gitlabUrl,
4964
+ "vcs.repository.id": meta.project,
4965
+ "vcs.change.id": meta.mr,
4966
+ ...meta.platform ? { "vcs.provider.name": meta.platform } : {},
4967
+ ...meta.serverAddress ? { "server.address": meta.serverAddress } : {},
4937
4968
  ...meta.ciAttrs,
4938
4969
  ...meta.ciSpanAttrs
4939
4970
  }
@@ -4948,20 +4979,30 @@ async function startOtelBridge(options = {}) {
4948
4979
  ciAttrs,
4949
4980
  runId,
4950
4981
  configuredModel: runMeta.get(runId)?.model,
4982
+ dryRun: runMeta.get(runId)?.dryRun,
4983
+ platform: runMeta.get(runId)?.platform,
4984
+ serverAddress: runMeta.get(runId)?.serverAddress,
4951
4985
  captureContent
4952
4986
  });
4953
4987
  }
4954
4988
  };
4955
4989
  }
4956
4990
  /**
4957
- * Builds the dynamic `gen_ai.system` / `gen_ai.request.model` metric labels
4958
- * shared by the per-turn and per-phase GenAI metric emitters. Owning these in
4959
- * one place keeps the two emission sites from drifting into separate Prometheus
4960
- * series (the double-count `recordGenAiMetrics` documents).
4991
+ * Builds the dynamic `gen_ai.provider.name` / `gen_ai.request.model` metric
4992
+ * labels shared by the per-turn and per-phase GenAI metric emitters. Owning
4993
+ * these in one place keeps the two emission sites from drifting into separate
4994
+ * Prometheus series (the double-count `recordGenAiMetrics` documents).
4995
+ *
4996
+ * `gen_ai.provider.name` is the current GenAI-semconv discriminator; the
4997
+ * deprecated `gen_ai.system` is emitted alongside it during the transition so
4998
+ * backends still keyed on the old attribute keep working.
4961
4999
  */
4962
5000
  function genAiModelAttrs(provider, modelId) {
4963
5001
  return {
4964
- ...provider ? { "gen_ai.system": provider } : {},
5002
+ ...provider ? {
5003
+ "gen_ai.provider.name": provider,
5004
+ "gen_ai.system": provider
5005
+ } : {},
4965
5006
  ...modelId ? { "gen_ai.request.model": modelId } : {}
4966
5007
  };
4967
5008
  }
@@ -5017,9 +5058,9 @@ function recordTurnUsage(span, u, metricAttrs, tokenUsage, operationCost) {
5017
5058
  ...metricAttrs,
5018
5059
  "gen_ai.token.type": "cache_creation"
5019
5060
  });
5020
- span.setAttribute("gen_ai.usage.cost.input_usd", u.cost.input);
5021
- span.setAttribute("gen_ai.usage.cost.output_usd", u.cost.output);
5022
- span.setAttribute("gen_ai.usage.cost.total_usd", u.cost.total);
5061
+ span.setAttribute("code_review.cost.input_usd", u.cost.input);
5062
+ span.setAttribute("code_review.cost.output_usd", u.cost.output);
5063
+ span.setAttribute("code_review.cost.total_usd", u.cost.total);
5023
5064
  }
5024
5065
  }
5025
5066
  /**
@@ -5083,12 +5124,15 @@ function extractOutputMessages(msg) {
5083
5124
  }]);
5084
5125
  }
5085
5126
  function buildAgentSubscriber(tracer, tokenUsage, operationCost, timeToFirstToken, reviewerSpanCtx, options = {}) {
5086
- const { ciAttrs = {}, runId, configuredModel, captureContent = false } = options;
5127
+ const { ciAttrs = {}, runId, configuredModel, dryRun, platform, serverAddress, captureContent = false } = options;
5087
5128
  const configuredProvider = configuredModel ? splitModel(configuredModel).provider : void 0;
5088
5129
  const baseMetricAttrs = {
5089
5130
  "gen_ai.operation.name": "invoke_agent",
5090
5131
  ...REVIEW_SERVICE_ATTRS,
5091
- ...ciAttrs
5132
+ ...ciAttrs,
5133
+ ...dryRun !== void 0 ? { "code_review.dry_run": dryRun } : {},
5134
+ ...platform ? { "vcs.provider.name": platform } : {},
5135
+ ...serverAddress ? { "server.address": serverAddress } : {}
5092
5136
  };
5093
5137
  return (agent) => {
5094
5138
  let currentTurn;
@@ -5122,13 +5166,16 @@ function buildAgentSubscriber(tracer, tokenUsage, operationCost, timeToFirstToke
5122
5166
  ...baseMetricAttrs,
5123
5167
  ...genAiModelAttrs(provider, modelId)
5124
5168
  };
5125
- if (provider) span.setAttribute("gen_ai.system", provider);
5169
+ if (provider) {
5170
+ span.setAttribute("gen_ai.provider.name", provider);
5171
+ span.setAttribute("gen_ai.system", provider);
5172
+ }
5126
5173
  if (modelId) span.setAttribute("gen_ai.response.model", modelId);
5127
5174
  if (msg.stopReason) span.setAttribute("gen_ai.response.stop_reason", msg.stopReason);
5128
5175
  if (firstTokenMs !== void 0) {
5129
5176
  const ttftS = (firstTokenMs - startMs) / 1e3;
5130
5177
  timeToFirstToken.record(ttftS, metricAttrs);
5131
- span.setAttribute("gen_ai.client.time_to_first_token_s", ttftS);
5178
+ span.setAttribute("gen_ai.client.operation.time_to_first_chunk_s", ttftS);
5132
5179
  }
5133
5180
  if (msg.usage) recordTurnUsage(span, msg.usage, metricAttrs, tokenUsage, operationCost);
5134
5181
  if (captureContent) {
@@ -5199,7 +5246,7 @@ async function loadDefaultRuntime() {
5199
5246
  const [sdkNode, resources, semconv] = modules;
5200
5247
  const serviceResource = resources.resourceFromAttributes({
5201
5248
  [semconv.ATTR_SERVICE_NAME ?? "service.name"]: SERVICE_NAME,
5202
- [semconv.ATTR_SERVICE_VERSION ?? "service.version"]: "0.8.5"
5249
+ [semconv.ATTR_SERVICE_VERSION ?? "service.version"]: "0.9.0"
5203
5250
  });
5204
5251
  process.env.OTEL_METRICS_EXPORTER = process.env.OTEL_METRICS_EXPORTER ?? "otlp";
5205
5252
  process.env.OTEL_LOGS_EXPORTER = process.env.OTEL_LOGS_EXPORTER ?? "otlp";
@@ -5221,14 +5268,14 @@ function emitReviewStartedLog(logger, ctx, ciAttrs, ciSpanAttrs, rootSpanCtx) {
5221
5268
  context: rootSpanCtx,
5222
5269
  attributes: {
5223
5270
  "service.name": SERVICE_NAME,
5224
- "event.name": "gitlab_review.started",
5225
- "gitlab.project_id": ctx.project,
5226
- "gitlab.mr_iid": ctx.mr,
5227
- "gitlab.server_url": ctx.gitlabUrl,
5271
+ "event.name": "code_review.started",
5272
+ "vcs.repository.id": ctx.project,
5273
+ "vcs.change.id": ctx.mr,
5274
+ ...vcsAttrs(ctx.platform, ctx.gitlabUrl),
5228
5275
  ...ciAttrs,
5229
5276
  ...ciSpanAttrs,
5230
- "gitlab_review.run_id": ctx.runId,
5231
- "gitlab_review.dry_run": ctx.dryRun,
5277
+ "code_review.run_id": ctx.runId,
5278
+ "code_review.dry_run": ctx.dryRun,
5232
5279
  ...modelId !== void 0 && { "gen_ai.request.model": modelId }
5233
5280
  }
5234
5281
  });
@@ -5248,10 +5295,10 @@ function emitReviewCompletedLog(logger, ctx, meta, isError) {
5248
5295
  context: meta?.rootSpanCtx,
5249
5296
  attributes: {
5250
5297
  "service.name": SERVICE_NAME,
5251
- "event.name": isError ? "gitlab_review.failed" : "gitlab_review.completed",
5252
- "gitlab.project_id": ctx.project,
5253
- "gitlab.mr_iid": ctx.mr,
5254
- "gitlab.server_url": ctx.gitlabUrl,
5298
+ "event.name": isError ? "code_review.failed" : "code_review.completed",
5299
+ "vcs.repository.id": ctx.project,
5300
+ "vcs.change.id": ctx.mr,
5301
+ ...vcsAttrs(ctx.platform, ctx.gitlabUrl),
5255
5302
  ...meta?.ciAttrs,
5256
5303
  ...meta?.ciSpanAttrs,
5257
5304
  ...isError && {
@@ -5259,15 +5306,15 @@ function emitReviewCompletedLog(logger, ctx, meta, isError) {
5259
5306
  ...ctx.errorInfo && { "error.message": ctx.errorInfo.message },
5260
5307
  ...typeof ctx.errorInfo?.status === "number" && { "http.response.status_code": ctx.errorInfo.status }
5261
5308
  },
5262
- "gitlab_review.run_id": ctx.runId,
5263
- "gitlab_review.duration_ms": ctx.durationMs ?? 0,
5264
- "gitlab_review.dry_run": ctx.dryRun,
5265
- "gitlab_review.comments.generated": ctx.generated ?? 0,
5266
- "gitlab_review.comments.new": ctx.newComments ?? 0,
5267
- "gitlab_review.comments.duplicate": ctx.duplicateComments ?? 0,
5268
- "gitlab_review.comments.posted": ctx.posted ?? 0,
5309
+ "code_review.run_id": ctx.runId,
5310
+ "code_review.duration_ms": ctx.durationMs ?? 0,
5311
+ "code_review.dry_run": ctx.dryRun,
5312
+ "code_review.comments.generated": ctx.generated ?? 0,
5313
+ "code_review.comments.new": ctx.newComments ?? 0,
5314
+ "code_review.comments.duplicate": ctx.duplicateComments ?? 0,
5315
+ "code_review.comments.posted": ctx.posted ?? 0,
5269
5316
  ...modelId !== void 0 && { "gen_ai.request.model": modelId },
5270
- ...cost !== void 0 && { "gen_ai.usage.cost.total_usd": cost },
5317
+ ...cost !== void 0 && { "code_review.cost.total_usd": cost },
5271
5318
  ...usage?.tokens.input !== void 0 && { "gen_ai.usage.input_tokens": usage.tokens.input + (usage.tokens.cacheRead ?? 0) },
5272
5319
  ...usage?.tokens.cacheRead && {
5273
5320
  "gen_ai.usage.input_tokens.cached": usage.tokens.cacheRead,
@@ -5286,39 +5333,39 @@ function spanNameFor(phase) {
5286
5333
  /** Creates the review-level OTel metric instruments on the given meter. */
5287
5334
  function createReviewInstruments(meter) {
5288
5335
  return {
5289
- reviewRunsTotal: meter.createCounter("gitlab_review_runs_total", { description: "Total number of code-review runs, labelled by terminal status" }),
5290
- reviewErrorsTotal: meter.createCounter("gitlab_review_errors_total", { description: "Total number of failed code-review runs, labelled by error type" }),
5336
+ reviewRunsTotal: meter.createCounter("code_review_runs_total", { description: "Total number of code-review runs, labelled by terminal status" }),
5337
+ reviewErrorsTotal: meter.createCounter("code_review_errors_total", { description: "Total number of failed code-review runs, labelled by error type" }),
5291
5338
  reviewLlmTokens: {
5292
- input: meter.createCounter("gitlab_review_llm_input_tokens_total", {
5339
+ input: meter.createCounter("code_review_llm_input_tokens_total", {
5293
5340
  description: "Total non-cached LLM input tokens consumed across code-review runs",
5294
5341
  unit: "{token}"
5295
5342
  }),
5296
- output: meter.createCounter("gitlab_review_llm_output_tokens_total", {
5343
+ output: meter.createCounter("code_review_llm_output_tokens_total", {
5297
5344
  description: "Total LLM output tokens generated across code-review runs",
5298
5345
  unit: "{token}"
5299
5346
  }),
5300
- cache_read: meter.createCounter("gitlab_review_llm_cache_read_tokens_total", {
5347
+ cache_read: meter.createCounter("code_review_llm_cache_read_tokens_total", {
5301
5348
  description: "Total LLM cache-read input tokens across code-review runs",
5302
5349
  unit: "{token}"
5303
5350
  }),
5304
- cache_creation: meter.createCounter("gitlab_review_llm_cache_creation_tokens_total", {
5351
+ cache_creation: meter.createCounter("code_review_llm_cache_creation_tokens_total", {
5305
5352
  description: "Total LLM cache-creation input tokens across code-review runs",
5306
5353
  unit: "{token}"
5307
5354
  })
5308
5355
  },
5309
- reviewRunDuration: meter.createHistogram("gitlab_review_run_duration_seconds", {
5356
+ reviewRunDuration: meter.createHistogram("code_review_run_duration_seconds", {
5310
5357
  description: "Duration of a complete code-review run",
5311
5358
  unit: "s",
5312
5359
  advice: { explicitBucketBoundaries: REVIEW_RUN_DURATION_BUCKETS_S }
5313
5360
  }),
5314
- reviewTotalCost: meter.createHistogram("gitlab_review_total_cost_usd", {
5361
+ reviewTotalCost: meter.createHistogram("code_review_total_cost_usd", {
5315
5362
  description: "Total LLM cost in USD for a complete code-review run",
5316
5363
  unit: "{usd}",
5317
5364
  advice: { explicitBucketBoundaries: REVIEW_TOTAL_COST_BUCKETS_USD }
5318
5365
  }),
5319
- reviewCommentsTotal: meter.createCounter("gitlab_review_comments_total", { description: "Total number of MR comments posted by code-review" }),
5320
- reviewDraftsPublishedTotal: meter.createCounter("gitlab_review_drafts_published_total", { description: "Total number of draft notes published by code-review" }),
5321
- reviewPhaseDuration: meter.createHistogram("gitlab_review_phase_duration_seconds", {
5366
+ reviewCommentsTotal: meter.createCounter("code_review_comments_total", { description: "Total number of MR comments posted by code-review" }),
5367
+ reviewDraftsPublishedTotal: meter.createCounter("code_review_drafts_published_total", { description: "Total number of draft notes published by code-review" }),
5368
+ reviewPhaseDuration: meter.createHistogram("code_review_phase_duration_seconds", {
5322
5369
  description: "Duration of individual code-review workflow phases",
5323
5370
  unit: "s",
5324
5371
  advice: { explicitBucketBoundaries: REVIEW_PHASE_DURATION_BUCKETS_S }
@@ -5342,7 +5389,7 @@ function errorTypeOf(ctx) {
5342
5389
  return base;
5343
5390
  }
5344
5391
  /**
5345
- * Derives the `gitlab_review.status` label used by review-level OTel metrics.
5392
+ * Derives the `code_review.status` label used by review-level OTel metrics.
5346
5393
  * Distinguishes timeouts (AbortError / ETIMEDOUT) from generic errors so
5347
5394
  * Grafana alerts can treat deadline-exceeded runs separately.
5348
5395
  */
@@ -5353,55 +5400,91 @@ function resolveRunStatus(ctx, isError) {
5353
5400
  return "error";
5354
5401
  }
5355
5402
  /**
5356
- * Extracts GitLab CI environment variables that add project/pipeline context
5357
- * to every metric, span, and log record. Only populated when running inside a
5358
- * GitLab CI pipeline; callers spread the result so missing vars add nothing.
5403
+ * Extracts low-cardinality CI project/pipeline context (repository, owner, base
5404
+ * branch, pipeline trigger) as platform-neutral `vcs.*` / `cicd.*` attributes
5405
+ * added to every metric, span, and log record. Sourced from GitLab CI or GitHub
5406
+ * Actions variables — a run is one or the other, so `??` picks whichever is set;
5407
+ * callers spread the result so missing vars add nothing.
5359
5408
  */
5360
5409
  function buildCiAttrs(env) {
5361
5410
  const attrs = {};
5362
- if (env.CI_PROJECT_PATH) attrs["gitlab.project_path"] = env.CI_PROJECT_PATH;
5363
- if (env.CI_PROJECT_NAMESPACE) attrs["gitlab.project_namespace"] = env.CI_PROJECT_NAMESPACE;
5364
- if (env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME) attrs["gitlab.mr_target_branch"] = env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME;
5365
- if (env.CI_PIPELINE_SOURCE) attrs["gitlab.pipeline_source"] = env.CI_PIPELINE_SOURCE;
5411
+ const repository = env.CI_PROJECT_PATH ?? env.GITHUB_REPOSITORY;
5412
+ if (repository) attrs["vcs.repository.name"] = repository;
5413
+ const owner = env.CI_PROJECT_NAMESPACE ?? env.GITHUB_REPOSITORY_OWNER;
5414
+ if (owner) attrs["vcs.owner.name"] = owner;
5415
+ const baseRef = env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME ?? env.GITHUB_BASE_REF;
5416
+ if (baseRef) attrs["vcs.ref.base.name"] = baseRef;
5417
+ const pipelineSource = env.CI_PIPELINE_SOURCE ?? env.GITHUB_EVENT_NAME;
5418
+ if (pipelineSource) attrs["cicd.pipeline.source"] = pipelineSource;
5366
5419
  return attrs;
5367
5420
  }
5368
5421
  /**
5369
- * Extracts high-cardinality GitLab CI identifiers that should appear on spans
5370
- * and log records but NOT on metric data points (to avoid label explosion in
5371
- * Prometheus/Mimir). Spread results via `ciSpanAttrs` stored in RunMeta.
5422
+ * Extracts high-cardinality CI identifiers that should appear on spans and log
5423
+ * records but NOT on metric data points (to avoid label explosion in
5424
+ * Prometheus/Mimir). Sourced from GitLab CI or GitHub Actions. GitHub exposes no
5425
+ * per-job run id in the environment, so the job's config-key (`GITHUB_JOB`) is
5426
+ * used for the task identifier. Spread via `ciSpanAttrs` stored in RunMeta.
5372
5427
  */
5373
5428
  function buildCiSpanAttrs(env) {
5374
5429
  const attrs = {};
5375
- if (env.CI_JOB_ID) attrs["gitlab.ci_job_id"] = env.CI_JOB_ID;
5376
- if (env.CI_PIPELINE_ID) attrs["gitlab.ci_pipeline_id"] = env.CI_PIPELINE_ID;
5430
+ const taskRunId = env.CI_JOB_ID ?? env.GITHUB_JOB;
5431
+ if (taskRunId) attrs["cicd.pipeline.task.run.id"] = taskRunId;
5432
+ const pipelineRunId = env.CI_PIPELINE_ID ?? env.GITHUB_RUN_ID;
5433
+ if (pipelineRunId) attrs["cicd.pipeline.run.id"] = pipelineRunId;
5434
+ const repositoryUrl = env.CI_PROJECT_URL ?? (env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY ? `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}` : void 0);
5435
+ if (repositoryUrl) attrs["vcs.repository.url.full"] = repositoryUrl;
5377
5436
  return attrs;
5378
5437
  }
5438
+ /** Host of a server URL, for the low-cardinality `server.address` instance label. */
5439
+ function hostOf(url) {
5440
+ if (!url) return void 0;
5441
+ try {
5442
+ return new URL(url).host;
5443
+ } catch {
5444
+ return;
5445
+ }
5446
+ }
5447
+ /**
5448
+ * Low-cardinality VCS discriminators added to every metric, span, and log:
5449
+ * `vcs.provider.name` (gitlab | github) to filter by platform, and
5450
+ * `server.address` (the instance host) to distinguish multiple GitLab/GitHub
5451
+ * instances (e.g. gitlab.com vs a self-hosted gitlab.example.com).
5452
+ */
5453
+ function vcsAttrs(platform, serverUrl) {
5454
+ const host = hostOf(serverUrl);
5455
+ return {
5456
+ ...platform ? { "vcs.provider.name": platform } : {},
5457
+ ...host ? { "server.address": host } : {}
5458
+ };
5459
+ }
5379
5460
  function baseAttributes(ctx) {
5461
+ const host = hostOf(ctx.gitlabUrl);
5380
5462
  return {
5381
- "gitlab_review.run_id": ctx.runId,
5463
+ "code_review.run_id": ctx.runId,
5382
5464
  "gen_ai.conversation.id": ctx.runId,
5383
- "gitlab_review.phase": ctx.phase,
5384
- "gitlab.project_id": ctx.project,
5385
- "gitlab.mr_iid": ctx.mr,
5386
- "gitlab.server_url": ctx.gitlabUrl,
5387
- "gitlab_review.dry_run": ctx.dryRun,
5388
- "gitlab_review.no_post": ctx.noPost,
5389
- "gitlab_review.min_severity": ctx.minSeverity
5465
+ "code_review.phase": ctx.phase,
5466
+ "vcs.repository.id": ctx.project,
5467
+ "vcs.change.id": ctx.mr,
5468
+ ...ctx.platform ? { "vcs.provider.name": ctx.platform } : {},
5469
+ ...host ? { "server.address": host } : {},
5470
+ "code_review.dry_run": ctx.dryRun,
5471
+ "code_review.no_post": ctx.noPost,
5472
+ "code_review.min_severity": ctx.minSeverity
5390
5473
  };
5391
5474
  }
5392
5475
  var NUMERIC_RESULT_ATTRIBUTES = [
5393
- ["durationMs", "gitlab_review.duration_ms"],
5394
- ["generated", "gitlab_review.comments.generated"],
5395
- ["newComments", "gitlab_review.comments.new"],
5396
- ["duplicateComments", "gitlab_review.comments.duplicate"],
5397
- ["posted", "gitlab_review.comments.posted"],
5398
- ["draftsPublished", "gitlab_review.drafts.published"],
5399
- ["draftsCreated", "gitlab_review.drafts.created"],
5400
- ["summaryNoteId", "gitlab_review.summary.note_id"],
5401
- ["warnings", "gitlab_review.warnings"],
5402
- ["draftsAbandoned", "gitlab_review.drafts.abandoned"],
5403
- ["draftsDeletedPrePublish", "gitlab_review.drafts.deleted_pre_publish"],
5404
- ["draftsPublishFailed", "gitlab_review.drafts.publish_failed"],
5476
+ ["durationMs", "code_review.duration_ms"],
5477
+ ["generated", "code_review.comments.generated"],
5478
+ ["newComments", "code_review.comments.new"],
5479
+ ["duplicateComments", "code_review.comments.duplicate"],
5480
+ ["posted", "code_review.comments.posted"],
5481
+ ["draftsPublished", "code_review.drafts.published"],
5482
+ ["draftsCreated", "code_review.drafts.created"],
5483
+ ["summaryNoteId", "code_review.summary.note_id"],
5484
+ ["warnings", "code_review.warnings"],
5485
+ ["draftsAbandoned", "code_review.drafts.abandoned"],
5486
+ ["draftsDeletedPrePublish", "code_review.drafts.deleted_pre_publish"],
5487
+ ["draftsPublishFailed", "code_review.drafts.publish_failed"],
5405
5488
  ["diffFilesChanged", "diff.files_changed"],
5406
5489
  ["diffLinesAdded", "diff.lines_added"],
5407
5490
  ["diffLinesRemoved", "diff.lines_removed"],
@@ -5409,7 +5492,7 @@ var NUMERIC_RESULT_ATTRIBUTES = [
5409
5492
  ["httpResponseBodySize", "http.response.body.size"]
5410
5493
  ];
5411
5494
  var STRING_RESULT_ATTRIBUTES = [
5412
- ["summaryAction", "gitlab_review.summary.action"],
5495
+ ["summaryAction", "code_review.summary.action"],
5413
5496
  ["httpRequestMethod", "http.request.method"],
5414
5497
  ["httpUrl", "url.full"],
5415
5498
  ["serverAddress", "server.address"]
@@ -5426,7 +5509,10 @@ function applyResultAttributes(span, ctx) {
5426
5509
  }
5427
5510
  function applyGenAiAttributes(span, ctx) {
5428
5511
  const { provider, modelId } = splitModel(ctx.model ?? "");
5429
- if (provider) span.setAttribute("gen_ai.system", provider);
5512
+ if (provider) {
5513
+ span.setAttribute("gen_ai.provider.name", provider);
5514
+ span.setAttribute("gen_ai.system", provider);
5515
+ }
5430
5516
  if (modelId) {
5431
5517
  span.setAttribute("gen_ai.request.model", modelId);
5432
5518
  span.setAttribute("gen_ai.response.model", modelId);
@@ -5436,16 +5522,16 @@ function applyGenAiAttributes(span, ctx) {
5436
5522
  const usage = ctx.usage;
5437
5523
  if (!usage) return;
5438
5524
  setTokenUsageSpanAttributes(span, usage.tokens);
5439
- span.setAttribute("gen_ai.usage.cost.input_usd", usage.cost.input);
5440
- span.setAttribute("gen_ai.usage.cost.output_usd", usage.cost.output);
5441
- span.setAttribute("gen_ai.usage.cost.cache_read_usd", usage.cost.cacheRead);
5442
- span.setAttribute("gen_ai.usage.cost.cache_creation_usd", usage.cost.cacheWrite);
5443
- span.setAttribute("gen_ai.usage.cost.total_usd", usage.cost.total);
5525
+ span.setAttribute("code_review.cost.input_usd", usage.cost.input);
5526
+ span.setAttribute("code_review.cost.output_usd", usage.cost.output);
5527
+ span.setAttribute("code_review.cost.cache_read_usd", usage.cost.cacheRead);
5528
+ span.setAttribute("code_review.cost.cache_creation_usd", usage.cost.cacheWrite);
5529
+ span.setAttribute("code_review.cost.total_usd", usage.cost.total);
5444
5530
  }
5445
5531
  /**
5446
5532
  * Records `gen_ai.client.operation.duration` for the `reviewer.run` phase.
5447
5533
  *
5448
- * Token usage (`gen_ai.client.token.usage`) and cost (`gen_ai.client.cost`) are
5534
+ * Token usage (`gen_ai.client.token.usage`) and cost (`code_review_llm_cost_usd`) are
5449
5535
  * intentionally NOT recorded here. They are emitted per-turn by
5450
5536
  * `buildAgentSubscriber` from the live agent event stream. Keeping a single
5451
5537
  * emission point for each metric prevents the double-count that previously
@@ -5458,7 +5544,9 @@ function recordGenAiMetrics(durationHist, ctx, isError, ciAttrs = {}) {
5458
5544
  "gen_ai.operation.name": "invoke_agent",
5459
5545
  ...REVIEW_SERVICE_ATTRS,
5460
5546
  ...ciAttrs,
5461
- ...genAiModelAttrs(provider, modelId)
5547
+ ...vcsAttrs(ctx.platform, ctx.gitlabUrl),
5548
+ ...genAiModelAttrs(provider, modelId),
5549
+ "code_review.dry_run": ctx.dryRun
5462
5550
  };
5463
5551
  if (isError) attrs["error.type"] = errorTypeOf(ctx);
5464
5552
  if (typeof ctx.durationMs === "number") durationHist.record(ctx.durationMs / 1e3, attrs);
@@ -5631,7 +5719,7 @@ function boldCommentTitle(body) {
5631
5719
  */
5632
5720
  function buildCommentBody(body, commitSha, confidence) {
5633
5721
  const confidenceLine = `_Confidence: ${confidence}._`;
5634
- const footer = `<sub>Reviewed by ${PRODUCT_LINK} v0.8.5 for commit ${commitSha}.</sub>`;
5722
+ const footer = `<sub>Reviewed by ${PRODUCT_LINK} v0.9.0 for commit ${commitSha}.</sub>`;
5635
5723
  return `${boldCommentTitle(body.trim())}\n\n${confidenceLine}\n\n---\n\n${footer}`;
5636
5724
  }
5637
5725
  function buildPayload(comment, body, refs, resolved) {
@@ -5795,11 +5883,14 @@ function reviewCommentPosition(comment) {
5795
5883
  * comments that render identically on GitHub, so `extractExistingFingerprints`,
5796
5884
  * `findExistingSummaryNote`, and the reviewed-commit scan all work as-is.
5797
5885
  *
5798
- * `resolvedCommentIds` carries the database ids of comments in resolved review
5799
- * threads (from the GraphQL `reviewThreads` query, since REST omits resolution),
5800
- * so each note gets a `resolved` flag mirroring GitLab's per-note field.
5886
+ * `settledCommentIds` carries the database ids of comments in settled review
5887
+ * threads — resolved or outdated (from the GraphQL `reviewThreads` query, since
5888
+ * REST omits both). Each such note gets a `resolved` flag mirroring GitLab's
5889
+ * per-note field: an outdated GitHub thread maps to `resolved: true` because
5890
+ * GitHub, unlike GitLab, does not auto-resolve a thread when its anchored line
5891
+ * changes, so treating outdated as resolved matches GitLab's behaviour (#133).
5801
5892
  */
5802
- function normalizeGitHubDiscussions(reviewComments, issueComments, resolvedCommentIds = /* @__PURE__ */ new Set()) {
5893
+ function normalizeGitHubDiscussions(reviewComments, issueComments, settledCommentIds = /* @__PURE__ */ new Set()) {
5803
5894
  const threads = /* @__PURE__ */ new Map();
5804
5895
  const order = [];
5805
5896
  for (const comment of reviewComments) {
@@ -5813,7 +5904,7 @@ function normalizeGitHubDiscussions(reviewComments, issueComments, resolvedComme
5813
5904
  notes.push({
5814
5905
  id: comment.id,
5815
5906
  body: comment.body ?? "",
5816
- resolved: resolvedCommentIds.has(comment.id),
5907
+ resolved: settledCommentIds.has(comment.id),
5817
5908
  position: reviewCommentPosition(comment)
5818
5909
  });
5819
5910
  }
@@ -5880,12 +5971,12 @@ var GitHubPlatform = class {
5880
5971
  return refs;
5881
5972
  }
5882
5973
  async getDiscussions() {
5883
- const [reviewComments, issueComments, resolvedCommentIds] = await Promise.all([
5974
+ const [reviewComments, issueComments, settledCommentIds] = await Promise.all([
5884
5975
  this.client.listReviewComments(this.owner, this.repo, this.pull),
5885
5976
  this.client.listIssueComments(this.owner, this.repo, this.pull),
5886
- this.client.listResolvedReviewCommentIds(this.owner, this.repo, this.pull)
5977
+ this.client.listSettledReviewCommentIds(this.owner, this.repo, this.pull)
5887
5978
  ]);
5888
- return normalizeGitHubDiscussions(reviewComments, issueComments, resolvedCommentIds);
5979
+ return normalizeGitHubDiscussions(reviewComments, issueComments, settledCommentIds);
5889
5980
  }
5890
5981
  buildComments(comments, diff, refs, existingFingerprints) {
5891
5982
  this.commitId = refs.head_sha;
@@ -6423,6 +6514,7 @@ async function run(config, bridges) {
6423
6514
  const refs = await tracedRead("scm.get_latest_version", () => platform.getRefs());
6424
6515
  const initialDiscussions = await tracedRead("scm.get_discussions", () => platform.getDiscussions());
6425
6516
  const reviewedCommitSha = findExistingReviewedCommitSha(initialDiscussions);
6517
+ runContext.firstReview = findExistingSummaryNote(initialDiscussions) === null;
6426
6518
  if (!config.forceReview && !config.dryRun && !config.noPost && reviewedCommitSha === refs.head_sha) {
6427
6519
  const usage = zeroReviewUsage(config.model, config.thinkingLevel);
6428
6520
  runContext.usage = usage;
@@ -6465,27 +6557,36 @@ async function run(config, bridges) {
6465
6557
  if (priorThreads.length > 0) logger.info(`Found ${priorThreads.length} prior thread(s) with developer replies — including as context.`);
6466
6558
  logger.info("Running review...");
6467
6559
  let usage;
6560
+ let partialUsage;
6468
6561
  try {
6469
6562
  usage = await traceDiagnosticPhase("reviewer.run", config, runId, async (context) => {
6470
- const result = await runReview(config, {
6471
- cwd: config.cwd,
6472
- diff,
6473
- commitLog,
6474
- priorThreads,
6475
- intent: {
6476
- title: mr.title,
6477
- description: mr.description
6478
- },
6479
- logger,
6480
- attachTelemetry: bridges?.otel?.createAgentTelemetry(runId),
6481
- sinceRef: config.inputMode === "commits" && reviewedCommitSha && reviewedCommitSha !== refs.head_sha ? reviewedCommitSha : void 0
6482
- });
6483
- context.usage = result;
6484
- return result;
6563
+ try {
6564
+ const result = await runReview(config, {
6565
+ cwd: config.cwd,
6566
+ diff,
6567
+ commitLog,
6568
+ priorThreads,
6569
+ intent: {
6570
+ title: mr.title,
6571
+ description: mr.description
6572
+ },
6573
+ logger,
6574
+ attachTelemetry: bridges?.otel?.createAgentTelemetry(runId),
6575
+ onUsage: (u) => {
6576
+ partialUsage = u;
6577
+ },
6578
+ sinceRef: config.inputMode === "commits" && reviewedCommitSha && reviewedCommitSha !== refs.head_sha ? reviewedCommitSha : void 0
6579
+ });
6580
+ context.usage = result;
6581
+ return result;
6582
+ } catch (error) {
6583
+ if (partialUsage) context.usage = partialUsage;
6584
+ throw error;
6585
+ }
6485
6586
  });
6486
6587
  } catch (error) {
6487
6588
  if (!isQuotaExceededError(error)) throw error;
6488
- const skipUsage = zeroReviewUsage(config.model, config.thinkingLevel);
6589
+ const skipUsage = partialUsage ?? zeroReviewUsage(config.model, config.thinkingLevel);
6489
6590
  runContext.usage = skipUsage;
6490
6591
  runContext.generated = 0;
6491
6592
  runContext.newComments = 0;
@@ -6725,10 +6826,10 @@ async function main(argv = process.argv.slice(2)) {
6725
6826
  return;
6726
6827
  }
6727
6828
  if (argv.includes("--version") || argv.includes("-v")) {
6728
- console.log("0.8.5");
6829
+ console.log("0.9.0");
6729
6830
  return;
6730
6831
  }
6731
- process.stderr.write(`[code-review] @weareikko/code-review v0.8.5\n`);
6832
+ process.stderr.write(`[code-review] @weareikko/code-review v0.9.0\n`);
6732
6833
  assertNodeVersion();
6733
6834
  applyCodeReviewEnvPrefix();
6734
6835
  applyDefaultCacheRetention();
@@ -6751,4 +6852,4 @@ if (isDirectRun()) main().catch((error) => {
6751
6852
  //#endregion
6752
6853
  export { normalizeBody as $, SUMMARY_HISTORY_END as A, buildSummaryHistoryEntries as B, createDiagnosticContext as C, traceDiagnosticPhase as D, traceDiagnostic as E, SUMMARY_MARKER as F, findExistingSummaryNoteId as G, extractSummaryHistoryEntries as H, buildArchivedSummaryEntry as I, upsertSummaryNote as J, stripSummaryHistory as K, buildReviewedCommitFooter as L, SUMMARY_HISTORY_ENTRY_START as M, SUMMARY_HISTORY_LIMIT as N, normalizeSeverity as O, SUMMARY_HISTORY_START as P, fingerprints as Q, buildSizeNoticeBlock as R, DIAGNOSTIC_CHANNEL_PREFIX as S, diagnosticChannels as T, findExistingReviewedCommitSha as U, extractReviewedCommitSha as V, findExistingSummaryNote as W, extractDiffHunkContext as X, appendFingerprintMarkers as Y, extractExistingFingerprints as Z, resolveNpmSkillDir as _, main as a, parseReviewMarkdownWithWarnings as b, buildGeneratedComments as c, startOtelBridge as d, sha256 as et, filterDiff as f, parseSkillSpec as g, loadNamedSkill as h, formatUsageLine as i, SUMMARY_HISTORY_ENTRY_END as j, toGitLabReviewSeverity as k, buildPayload as l, gitSkillCacheKey as m, formatPerModelUsage as n, run as o, runReview as p, stripSummaryMarker as q, formatSkillsFooter as r, withHttpStamping as s, countPostedBySeverity as t, isOtelEnabled as u, resolveSkillCacheDir as v, createDiagnosticRunId as w, DIAGNOSTIC_CHANNEL_NAMES as x, parseReviewMarkdown as y, buildSummaryBody as z };
6753
6854
 
6754
- //# sourceMappingURL=cli-DmzA9PxS.js.map
6855
+ //# sourceMappingURL=cli-DK0kteLL.js.map