@doist/doistbot-cli 1.0.5 → 1.0.6

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 (53) hide show
  1. package/dist/actions/auth.js +55 -21
  2. package/dist/actions/doctor.js +13 -6
  3. package/dist/actions/review.js +75 -10
  4. package/dist/auth.js +51 -6
  5. package/dist/config.js +34 -5
  6. package/dist/env.js +1 -0
  7. package/dist/runtime.js +1 -1
  8. package/dist/terminal.js +2 -0
  9. package/package.json +4 -4
  10. package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +4 -8
  11. package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +8 -20
  12. package/sandbox/dist/core/check-run.js +16 -0
  13. package/sandbox/dist/core/datadog-metrics.js +6 -52
  14. package/sandbox/dist/core/logger-span-processor.js +78 -0
  15. package/sandbox/dist/core/pi.js +257 -176
  16. package/sandbox/dist/core/tracing-setup.js +15 -0
  17. package/sandbox/dist/core/tracing.js +71 -0
  18. package/sandbox/dist/main.js +4 -1
  19. package/sandbox/dist/providers/credentials.js +12 -2
  20. package/sandbox/dist/providers/gemini.js +5 -1
  21. package/sandbox/dist/providers/helpers.js +12 -17
  22. package/sandbox/dist/providers/index.js +4 -0
  23. package/sandbox/dist/providers/openrouter-pi.js +25 -0
  24. package/sandbox/dist/providers/pi-review.js +12 -1
  25. package/sandbox/dist/tasks/chat/chat.js +161 -101
  26. package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +210 -185
  27. package/sandbox/dist/tasks/issue-summarize/model.js +1 -1
  28. package/sandbox/dist/tasks/issue-triage/clone.js +40 -0
  29. package/sandbox/dist/tasks/issue-triage/context.js +25 -1
  30. package/sandbox/dist/tasks/issue-triage/fix-attempt.js +31 -8
  31. package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +154 -101
  32. package/sandbox/dist/tasks/issue-triage/fix-loop/runners.js +15 -6
  33. package/sandbox/dist/tasks/issue-triage/model.js +120 -36
  34. package/sandbox/dist/tasks/issue-triage/output.js +27 -0
  35. package/sandbox/dist/tasks/issue-triage/pr-creator.js +3 -2
  36. package/sandbox/dist/tasks/issue-triage/resolution.js +26 -0
  37. package/sandbox/dist/tasks/issue-triage/routing.js +9 -9
  38. package/sandbox/dist/tasks/issue-triage/side-effects.js +9 -0
  39. package/sandbox/dist/tasks/issue-triage/triage.js +288 -418
  40. package/sandbox/dist/tasks/review/check-run.js +2 -1
  41. package/sandbox/dist/tasks/review/conversation-context.js +2 -1
  42. package/sandbox/dist/tasks/review/engines/dedupe.js +5 -4
  43. package/sandbox/dist/tasks/review/engines/multi-focus.js +40 -9
  44. package/sandbox/dist/tasks/review/engines/shared.js +8 -6
  45. package/sandbox/dist/tasks/review/github-comment-format.js +64 -10
  46. package/sandbox/dist/tasks/review/local.js +2 -4
  47. package/sandbox/dist/tasks/review/review-summary.js +45 -7
  48. package/sandbox/dist/tasks/review/review.js +38 -24
  49. package/sandbox/dist/tasks/review/runner.js +23 -2
  50. package/sandbox/dist/tasks/review/summary-model.js +164 -17
  51. package/sandbox/dist/tasks/review/thinking-level.js +4 -0
  52. package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +4 -8
  53. package/sandbox/node_modules/doistbot-repo-config/dist/index.js +8 -20
@@ -1,26 +1,27 @@
1
1
  import { performance } from 'node:perf_hooks';
2
2
  import path from 'path';
3
+ import { SpanStatusCode } from '@opentelemetry/api';
3
4
  import { recordTaskMetrics } from '../../core/datadog-metrics.js';
4
5
  import { logger } from '../../core/logging.js';
5
- import { logPiProgressEvent } from '../../core/pi-progress-logging.js';
6
6
  import { loadPromptTemplateFile } from '../../core/prompt-template.js';
7
7
  import { removeIssueCommentReactionIfPresent } from '../../core/reactions.js';
8
- import { resolveIssueRepositories } from '../../core/repository-resolver.js';
9
8
  import { REPOS_SUBDIR, WORKSPACE_DIR } from '../../core/shared.js';
10
- import { parseIssueTriageContext } from './context.js';
9
+ import { withPhase } from '../../core/tracing.js';
10
+ import { cloneTriageRepositories } from './clone.js';
11
+ import { loadTriageContext, parseIssueTriageContext } from './context.js';
11
12
  import { postAutoFixFailureComment, shouldPostAutoFixFailureComment } from './fix-attempt.js';
12
13
  import { runConfiguredFix } from './fix-dispatch.js';
13
14
  import { invokeTriageModel } from './model.js';
14
- import { buildTriageFailureMessage, buildUnresolvedLabelsOutput, ensureStructuredTriageOutput, parseFixAssessment, REQUIRED_HEADINGS, } from './output.js';
15
+ import { buildTriageFailureMessage, buildUnresolvedLabelsOutput, REQUIRED_HEADINGS, validateTriageOutput, } from './output.js';
15
16
  import { fillTemplate, loadPromptTemplate } from './prompt.js';
16
17
  import { allRepositoriesAllow, createRepoConfigLoader, isAutoFixEnabled, isAutoTriageEnabled, } from './repo-config.js';
17
- import { buildHeroRoutingComment, postHeroRoutingComment, shouldPostHeroRoutingComment, } from './routing.js';
18
- import { cloneRepository, createGitHubClients, extractLabelNames, fetchIssue, fetchIssueComments, getIssueUrl, prepareWorkspaceReposDirectory, TRIAGE_MARKER, } from './shared.js';
19
- import { deleteAutoTriagePlaceholder, postIssueComment } from './side-effects.js';
18
+ import { resolveTriageScope } from './resolution.js';
19
+ import { buildHeroRoutingComment, postTriageRouting, shouldPostHeroRoutingComment, } from './routing.js';
20
+ import { createGitHubClients, getIssueUrl, prepareWorkspaceReposDirectory } from './shared.js';
21
+ import { deleteAutoTriagePlaceholder, postTriageOutput } from './side-effects.js';
20
22
  const MAX_ISSUE_BODY_CHARS = 8_000;
21
23
  const MAX_COMMENT_BODY_CHARS = 2_000;
22
24
  const MAX_PROMPT_COMMENTS = 120;
23
- const VERBOSE_PI_PROGRESS_LOGS = process.env.DOISTBOT_VERBOSE_PI_PROGRESS === 'true';
24
25
  const TRIAGE_PROVIDER = 'openai';
25
26
  function truncateText(text, maxChars) {
26
27
  if (text.length <= maxChars) {
@@ -58,27 +59,6 @@ function formatRepositoriesForPrompt(repositories) {
58
59
  .map(({ repository, localPath }) => `- ${repository.fullName} (local path: ${localPath})`)
59
60
  .join('\n');
60
61
  }
61
- function logTriageModelProgress(ctx, event) {
62
- const meta = {
63
- task: 'issue-triage',
64
- issueNumber: ctx.issueNumber,
65
- };
66
- logPiProgressEvent({
67
- event,
68
- messages: {
69
- agentStart: 'Issue triage model agent started',
70
- agentEnd: 'Issue triage model agent finished',
71
- thinking: 'Issue triage model still thinking',
72
- responding: 'Issue triage model streaming response',
73
- toolStart: 'Issue triage model tool started',
74
- toolUpdate: 'Issue triage model tool still running',
75
- toolEnd: 'Issue triage model tool finished',
76
- },
77
- meta,
78
- logResponding: VERBOSE_PI_PROGRESS_LOGS,
79
- logTools: VERBOSE_PI_PROGRESS_LOGS,
80
- });
81
- }
82
62
  function buildPrompt({ ctx, issue, comments, labelNames, clonedRepositories, decisionPath, enableAutoFix, }) {
83
63
  const template = loadPromptTemplate();
84
64
  const issueBody = truncateText(formatCommentBody(issue.body ?? '(No issue description provided.)'), MAX_ISSUE_BODY_CHARS);
@@ -105,22 +85,6 @@ function buildPrompt({ ctx, issue, comments, labelNames, clonedRepositories, dec
105
85
  : '',
106
86
  });
107
87
  }
108
- async function postTriageComment({ octokit, ctx, body, }) {
109
- logger.info('Issue triage comment post starting', {
110
- task: 'issue-triage',
111
- issueNumber: ctx.issueNumber,
112
- bodyChars: body.length,
113
- editingPlaceholder: Boolean(ctx.placeholderCommentId),
114
- });
115
- await postIssueComment(ctx, octokit, body, {
116
- marker: TRIAGE_MARKER,
117
- placeholderCommentId: ctx.placeholderCommentId,
118
- });
119
- logger.info('Issue triage comment post succeeded', {
120
- task: 'issue-triage',
121
- issueNumber: ctx.issueNumber,
122
- });
123
- }
124
88
  export async function runIssueTriage(ctx) {
125
89
  if (ctx.triggerType === 'auto') {
126
90
  return runAutoIssueTriage(ctx);
@@ -152,425 +116,331 @@ async function runIssueTriagePipeline(ctx, mode) {
152
116
  repository: `${ctx.owner}/${ctx.repo}`,
153
117
  issueNumber: ctx.issueNumber,
154
118
  });
155
- logger.info('Issue triage task started', {
119
+ // The root withPhase wraps the entire run. OTel sets this span active, so
120
+ // every nested withPhase / startToolSpan parents under it automatically.
121
+ // withPhase handles the span lifecycle + ERROR status; the inner try/catch
122
+ // keeps the existing failure log and cleanup side effects.
123
+ return withPhase('triage', {
156
124
  task: 'issue-triage',
157
- dryRun: ctx.dryRun,
158
- issueNumber: ctx.issueNumber,
159
- commentId: ctx.commentId,
125
+ 'gen_ai.operation.name': 'invoke_agent',
126
+ 'gen_ai.agent.name': 'doistbot.triage',
127
+ dry_run: ctx.dryRun,
128
+ 'issue.number': ctx.issueNumber,
129
+ 'comment.id': ctx.commentId,
160
130
  trigger: ctx.triggerType,
161
- enableAutoFixOverride: ctx.enableAutoFixOverride,
162
- });
163
- const { app, readOctokit, writeOctokit } = await createGitHubClients(ctx);
164
- async function postFinalTriageComment(body) {
165
- await postTriageComment({
166
- octokit: writeOctokit,
167
- ctx,
168
- body,
169
- });
170
- triageCommentPosted = true;
171
- }
172
- try {
173
- logger.info('Fetching issue triage context from GitHub', {
174
- task: 'issue-triage',
175
- issueNumber: ctx.issueNumber,
176
- });
177
- const [issue, comments] = await Promise.all([
178
- fetchIssue(readOctokit, {
179
- owner: ctx.owner,
180
- repo: ctx.repo,
181
- issueNumber: ctx.issueNumber,
182
- }),
183
- fetchIssueComments(readOctokit, {
131
+ enable_auto_fix_override: ctx.enableAutoFixOverride,
132
+ }, async (rootSpan) => {
133
+ const { app, readOctokit, writeOctokit } = await createGitHubClients(ctx);
134
+ // postTriageOutput adds TRIAGE_MARKER (and edits the auto-triage
135
+ // placeholder) internally — callers pass marker-free bodies.
136
+ async function postFinalTriageComment(body) {
137
+ await postTriageOutput({ octokit: writeOctokit, ctx, body });
138
+ triageCommentPosted = true;
139
+ }
140
+ try {
141
+ const { issue, comments, labelNames } = await loadTriageContext({
142
+ octokit: readOctokit,
184
143
  owner: ctx.owner,
185
144
  repo: ctx.repo,
186
145
  issueNumber: ctx.issueNumber,
187
- }),
188
- ]);
189
- const labelNames = extractLabelNames(issue.labels);
190
- logger.info('Issue triage context loaded', {
191
- task: 'issue-triage',
192
- issueNumber: ctx.issueNumber,
193
- labelsCount: labelNames.length,
194
- commentsCount: comments.length,
195
- });
196
- const resolution = resolveIssueRepositories(labelNames);
197
- logger.info('Issue triage label resolution completed', {
198
- task: 'issue-triage',
199
- issueNumber: ctx.issueNumber,
200
- status: resolution.status,
201
- reason: resolution.reason,
202
- confidence: resolution.confidence,
203
- decisionPath: resolution.decisionPath,
204
- parsedProducts: resolution.parsedLabels.productLabels,
205
- parsedApps: resolution.parsedLabels.appLabels,
206
- parsedTeams: resolution.parsedLabels.teamLabels,
207
- unknownProductLabels: resolution.parsedLabels.unknownProductLabels,
208
- unknownAppLabels: resolution.parsedLabels.unknownAppLabels,
209
- unknownTeamLabels: resolution.parsedLabels.unknownTeamLabels,
210
- });
211
- logger.info('triage.repo_resolution', {
212
- task: 'issue-triage',
213
- trigger: ctx.triggerType,
214
- status: resolution.status,
215
- confidence: resolution.confidence,
216
- });
217
- if (resolution.status === 'unresolved') {
218
- const unresolvedBody = buildUnresolvedLabelsOutput({
219
- reason: resolution.reason,
220
- productLabels: resolution.parsedLabels.productLabels,
221
- appLabels: resolution.parsedLabels.appLabels,
222
- teamLabels: resolution.parsedLabels.teamLabels,
223
146
  });
224
- await postFinalTriageComment(unresolvedBody);
225
- if (mode.routeUnresolvedLabels) {
226
- await postHeroRoutingComment({
227
- octokit: writeOctokit,
228
- ctx,
229
- body: buildHeroRoutingComment({
230
- labels: labelNames,
231
- triageOutput: unresolvedBody,
232
- }),
147
+ const resolution = await resolveTriageScope({
148
+ triggerType: ctx.triggerType,
149
+ labelNames,
150
+ });
151
+ if (resolution.status === 'unresolved') {
152
+ const unresolvedBody = buildUnresolvedLabelsOutput({
153
+ reason: resolution.reason,
154
+ productLabels: resolution.parsedLabels.productLabels,
155
+ appLabels: resolution.parsedLabels.appLabels,
156
+ teamLabels: resolution.parsedLabels.teamLabels,
233
157
  });
158
+ await postFinalTriageComment(unresolvedBody);
159
+ if (mode.routeUnresolvedLabels) {
160
+ await postTriageRouting({
161
+ octokit: writeOctokit,
162
+ ctx,
163
+ body: buildHeroRoutingComment({
164
+ labels: labelNames,
165
+ triageOutput: unresolvedBody,
166
+ }),
167
+ });
168
+ }
169
+ triageDurationMs = performance.now() - startedAt;
170
+ return;
234
171
  }
235
- triageDurationMs = performance.now() - startedAt;
236
- return;
237
- }
238
- logger.info('Issue triage repository resolution succeeded', {
239
- task: 'issue-triage',
240
- issueNumber: ctx.issueNumber,
241
- resolvedReposCount: resolution.repositories.length,
242
- resolvedRepos: resolution.repositories.map((repository) => repository.fullName),
243
- confidence: resolution.confidence,
244
- });
245
- const getCachedRepoConfig = createRepoConfigLoader({
246
- octokit: readOctokit,
247
- logger,
248
- });
249
- if (ctx.triggerType === 'auto') {
250
- const enableAutoTriage = await allRepositoriesAllow({
251
- repositories: resolution.repositories,
252
- loadRepoConfig: getCachedRepoConfig,
253
- isEnabled: isAutoTriageEnabled,
254
- feature: 'auto_triage',
172
+ const getCachedRepoConfig = createRepoConfigLoader({
173
+ octokit: readOctokit,
255
174
  logger,
256
175
  });
257
- if (!enableAutoTriage) {
258
- shouldRecordTriageMetric = false;
259
- logger.info('Auto issue triage skipped: a resolved repository disabled auto triage', {
260
- task: 'issue-triage',
261
- issueNumber: ctx.issueNumber,
262
- resolvedRepos: resolution.repositories.map((repository) => repository.fullName),
176
+ if (ctx.triggerType === 'auto') {
177
+ const enableAutoTriage = await allRepositoriesAllow({
178
+ repositories: resolution.repositories,
179
+ loadRepoConfig: getCachedRepoConfig,
180
+ isEnabled: isAutoTriageEnabled,
181
+ feature: 'auto_triage',
182
+ logger,
263
183
  });
264
- await deleteAutoTriagePlaceholder({
265
- octokit: writeOctokit,
266
- ctx,
267
- reason: 'auto_triage_disabled',
184
+ if (!enableAutoTriage) {
185
+ shouldRecordTriageMetric = false;
186
+ logger.info('Auto issue triage skipped: a resolved repository disabled auto triage', {
187
+ task: 'issue-triage',
188
+ issueNumber: ctx.issueNumber,
189
+ resolvedRepos: resolution.repositories.map((repository) => repository.fullName),
190
+ });
191
+ await deleteAutoTriagePlaceholder({
192
+ octokit: writeOctokit,
193
+ ctx,
194
+ reason: 'auto_triage_disabled',
195
+ });
196
+ return;
197
+ }
198
+ }
199
+ let enableAutoFix = ctx.enableAutoFixOverride ?? true;
200
+ if (ctx.enableAutoFixOverride === undefined) {
201
+ enableAutoFix = await allRepositoriesAllow({
202
+ repositories: resolution.repositories,
203
+ loadRepoConfig: getCachedRepoConfig,
204
+ isEnabled: isAutoFixEnabled,
205
+ feature: 'auto_fix',
206
+ logger,
268
207
  });
269
- return;
270
208
  }
271
- }
272
- let enableAutoFix = ctx.enableAutoFixOverride ?? true;
273
- if (ctx.enableAutoFixOverride === undefined) {
274
- enableAutoFix = await allRepositoriesAllow({
209
+ const reposDir = prepareWorkspaceReposDirectory();
210
+ const { clonedRepositories, failedRepositories } = await cloneTriageRepositories({
211
+ app,
212
+ reposDir,
275
213
  repositories: resolution.repositories,
276
- loadRepoConfig: getCachedRepoConfig,
277
- isEnabled: isAutoFixEnabled,
278
- feature: 'auto_fix',
279
- logger,
214
+ fallbackInstallationId: ctx.installationId,
280
215
  });
281
- }
282
- const reposDir = prepareWorkspaceReposDirectory();
283
- const clonedRepositories = [];
284
- const failedRepositories = [];
285
- logger.info('Issue triage clone phase started', {
286
- task: 'issue-triage',
287
- issue_number: ctx.issueNumber,
288
- });
289
- for (const repository of resolution.repositories) {
290
- try {
291
- const cloneResult = await cloneRepository({
292
- app,
293
- repository,
294
- reposDir,
295
- fallbackInstallationId: ctx.installationId,
216
+ if (clonedRepositories.length === 0) {
217
+ await postFinalTriageComment(buildTriageFailureMessage('Repository cloning failed for all resolved repositories.'));
218
+ // Non-throwing failure: mark the root span so the run is
219
+ // not reported as ok in Datadog. withPhase never forces OK,
220
+ // so this survives to the root phase.end.
221
+ rootSpan.setStatus({
222
+ code: SpanStatusCode.ERROR,
223
+ message: 'Repository cloning failed for all resolved repositories.',
296
224
  });
297
- clonedRepositories.push(cloneResult);
225
+ triageDurationMs = performance.now() - startedAt;
226
+ return;
298
227
  }
299
- catch (error) {
300
- failedRepositories.push({
301
- repository: repository.fullName,
302
- error: error instanceof Error ? error.message : String(error),
303
- });
304
- logger.warn('Issue triage clone failed', {
228
+ if (failedRepositories.length > 0) {
229
+ logger.warn('Issue triage running in degraded mode after partial clone failures', {
305
230
  task: 'issue-triage',
306
- repository: repository.fullName,
307
- error: error instanceof Error ? error.message : String(error),
231
+ issueNumber: ctx.issueNumber,
232
+ cloneSucceeded: clonedRepositories.length,
233
+ cloneFailed: failedRepositories.length,
234
+ failedRepositories,
308
235
  });
309
236
  }
310
- }
311
- logger.info('Issue triage clone phase completed', {
312
- task: 'issue-triage',
313
- issueNumber: ctx.issueNumber,
314
- cloneSucceeded: clonedRepositories.length,
315
- cloneFailed: failedRepositories.length,
316
- failedRepositories,
317
- });
318
- if (clonedRepositories.length === 0) {
319
- const fallbackComment = `${TRIAGE_MARKER}\n\n${buildTriageFailureMessage('Repository cloning failed for all resolved repositories.')}`;
320
- await postFinalTriageComment(fallbackComment);
321
- triageDurationMs = performance.now() - startedAt;
322
- return;
323
- }
324
- if (failedRepositories.length > 0) {
325
- logger.warn('Issue triage running in degraded mode after partial clone failures', {
326
- task: 'issue-triage',
327
- issueNumber: ctx.issueNumber,
328
- cloneSucceeded: clonedRepositories.length,
329
- cloneFailed: failedRepositories.length,
237
+ const issueInstallationAuth = (await app.octokit.auth({
238
+ type: 'installation',
239
+ installationId: ctx.installationId,
240
+ }));
241
+ const prompt = buildPrompt({
242
+ ctx,
243
+ issue,
244
+ comments,
245
+ labelNames,
246
+ clonedRepositories,
247
+ decisionPath: resolution.decisionPath,
248
+ enableAutoFix,
330
249
  });
331
- }
332
- const issueInstallationAuth = (await app.octokit.auth({
333
- type: 'installation',
334
- installationId: ctx.installationId,
335
- }));
336
- const prompt = buildPrompt({
337
- ctx,
338
- issue,
339
- comments,
340
- labelNames,
341
- clonedRepositories,
342
- decisionPath: resolution.decisionPath,
343
- enableAutoFix,
344
- });
345
- logger.info('Issue triage model invocation starting', {
346
- task: 'issue-triage',
347
- issueNumber: ctx.issueNumber,
348
- provider: TRIAGE_PROVIDER,
349
- promptChars: prompt.length,
350
- repositoriesAnalyzed: clonedRepositories.length,
351
- });
352
- const triageResult = await invokeTriageModel({
353
- prompt,
354
- apiKey: ctx.openaiApiKey,
355
- cwd: WORKSPACE_DIR,
356
- extraEnv: {
357
- GH_TOKEN: issueInstallationAuth.token,
358
- },
359
- thinkingLevel: 'high',
360
- onProgress: (event) => {
361
- logTriageModelProgress(ctx, event);
362
- },
363
- });
364
- if (!triageResult.ok) {
365
- logger.error('Issue triage model invocation failed', {
366
- task: 'issue-triage',
367
- issueNumber: ctx.issueNumber,
368
- provider: TRIAGE_PROVIDER,
369
- timedOut: triageResult.timedOut,
370
- durationMs: triageResult.durationMs,
371
- error: triageResult.error,
250
+ const triageResult = await invokeTriageModel({
251
+ prompt,
252
+ apiKey: ctx.openaiApiKey,
253
+ cwd: WORKSPACE_DIR,
254
+ extraEnv: {
255
+ GH_TOKEN: issueInstallationAuth.token,
256
+ },
257
+ thinkingLevel: 'high',
372
258
  });
373
- const fallbackComment = `${TRIAGE_MARKER}\n\n${buildTriageFailureMessage(triageResult.error)}`;
374
- await postFinalTriageComment(fallbackComment);
375
- triageDurationMs = performance.now() - startedAt;
376
- return;
377
- }
378
- logger.info('Issue triage model invocation finished', {
379
- task: 'issue-triage',
380
- issueNumber: ctx.issueNumber,
381
- provider: TRIAGE_PROVIDER,
382
- durationMs: triageResult.durationMs,
383
- outputChars: triageResult.output.length,
384
- });
385
- const normalized = ensureStructuredTriageOutput(triageResult.output);
386
- logger.info('Issue triage output quality check completed', {
387
- task: 'issue-triage',
388
- issueNumber: ctx.issueNumber,
389
- missingSections: normalized.missingSections,
390
- missingSectionsCount: normalized.missingSections.length,
391
- });
392
- if (normalized.missingSections.length > 0) {
393
- const failureComment = `${TRIAGE_MARKER}\n\n${buildTriageFailureMessage(`Triage model returned incomplete output. Missing sections: ${normalized.missingSections.join(', ')}`)}`;
394
- await postFinalTriageComment(failureComment);
259
+ if (!triageResult.ok) {
260
+ await postFinalTriageComment(buildTriageFailureMessage(triageResult.error));
261
+ rootSpan.setStatus({ code: SpanStatusCode.ERROR, message: triageResult.error });
262
+ triageDurationMs = performance.now() - startedAt;
263
+ return;
264
+ }
265
+ const validated = await validateTriageOutput({ rawOutput: triageResult.output });
266
+ if (!validated.ok) {
267
+ await postFinalTriageComment(buildTriageFailureMessage(validated.error));
268
+ rootSpan.setStatus({ code: SpanStatusCode.ERROR, message: validated.error });
269
+ triageDurationMs = performance.now() - startedAt;
270
+ return;
271
+ }
272
+ await postFinalTriageComment(validated.commentOutput);
273
+ triageMetricOutcome = 'success';
395
274
  triageDurationMs = performance.now() - startedAt;
396
- return;
275
+ const fixAssessment = validated.assessment;
276
+ if (fixAssessment) {
277
+ logger.info('Fix assessment parsed from triage output', {
278
+ task: 'issue-triage',
279
+ issueNumber: ctx.issueNumber,
280
+ classification: fixAssessment.classification,
281
+ confidence: fixAssessment.confidence,
282
+ scope: fixAssessment.scope,
283
+ });
284
+ }
285
+ if (shouldPostHeroRoutingComment(fixAssessment)) {
286
+ const routingComment = buildHeroRoutingComment({
287
+ labels: labelNames,
288
+ primaryRepo: resolution.repositories.length === 1
289
+ ? resolution.repositories[0]
290
+ : undefined,
291
+ triageOutput: validated.output,
292
+ });
293
+ await postTriageRouting({
294
+ octokit: writeOctokit,
295
+ ctx,
296
+ body: routingComment,
297
+ });
298
+ }
299
+ if (enableAutoFix && fixAssessment) {
300
+ const fixMetricRepository = fixAssessment.targetRepo ??
301
+ (clonedRepositories.length === 1
302
+ ? clonedRepositories[0].repository.fullName
303
+ : `${ctx.owner}/${ctx.repo}`);
304
+ pendingFixMetric = {
305
+ repository: fixMetricRepository,
306
+ startedAt: performance.now(),
307
+ outcome: 'failure',
308
+ };
309
+ const fixResult = await runConfiguredFix({
310
+ ctx,
311
+ octokit: writeOctokit,
312
+ triageOutput: validated.output,
313
+ fixAssessment,
314
+ clonedRepos: clonedRepositories,
315
+ issueTitle: issue.title ?? 'Untitled issue',
316
+ issueUrl: issue.html_url ?? getIssueUrl(ctx.owner, ctx.repo, ctx.issueNumber),
317
+ ghToken: issueInstallationAuth.token,
318
+ loadRepoConfig: getCachedRepoConfig,
319
+ });
320
+ logger.info('triage.fix_attempted', {
321
+ task: 'issue-triage',
322
+ trigger: ctx.triggerType,
323
+ classification: fixAssessment.classification,
324
+ attempted: fixResult.attempted,
325
+ });
326
+ logger.info('Fix attempt result', {
327
+ task: 'issue-triage',
328
+ issueNumber: ctx.issueNumber,
329
+ attempted: fixResult.attempted,
330
+ prCreated: fixResult.attempted && fixResult.prCreated,
331
+ reason: 'reason' in fixResult ? fixResult.reason : undefined,
332
+ prUrl: 'prUrl' in fixResult ? fixResult.prUrl : undefined,
333
+ pr_number: 'prNumber' in fixResult ? fixResult.prNumber : undefined,
334
+ durationMs: performance.now() - startedAt,
335
+ });
336
+ pendingFixMetric = {
337
+ repository: fixResult.repository,
338
+ startedAt: pendingFixMetric.startedAt,
339
+ outcome: fixResult.attempted && fixResult.prCreated ? 'success' : 'failure',
340
+ };
341
+ const shouldPostFailureComment = await shouldPostAutoFixFailureComment(fixResult, {
342
+ dryRun: ctx.dryRun,
343
+ octokit: writeOctokit,
344
+ ctx,
345
+ });
346
+ if (shouldPostFailureComment) {
347
+ try {
348
+ await postAutoFixFailureComment({
349
+ octokit: writeOctokit,
350
+ ctx,
351
+ reason: 'reason' in fixResult ? fixResult.reason : 'Unknown reason',
352
+ targetRepoFullName: fixResult.repository,
353
+ });
354
+ }
355
+ catch (commentError) {
356
+ logger.warn('Failed to post auto-fix failure comment', {
357
+ task: 'issue-triage',
358
+ issue_number: ctx.issueNumber,
359
+ error: commentError instanceof Error
360
+ ? commentError.message
361
+ : String(commentError),
362
+ });
363
+ }
364
+ }
365
+ }
366
+ else {
367
+ logger.info('triage.fix_attempted', {
368
+ task: 'issue-triage',
369
+ trigger: ctx.triggerType,
370
+ classification: fixAssessment?.classification ?? 'none',
371
+ attempted: false,
372
+ });
373
+ }
397
374
  }
398
- const triageComment = `${TRIAGE_MARKER}\n\n${normalized.commentOutput}`;
399
- await postFinalTriageComment(triageComment);
400
- logger.info('Issue triage task completed successfully', {
401
- task: 'issue-triage',
402
- issueNumber: ctx.issueNumber,
403
- resolvedReposCount: clonedRepositories.length,
404
- durationMs: performance.now() - startedAt,
405
- });
406
- triageMetricOutcome = 'success';
407
- triageDurationMs = performance.now() - startedAt;
408
- const fixAssessment = parseFixAssessment(normalized.output);
409
- if (fixAssessment) {
410
- logger.info('Fix assessment parsed from triage output', {
375
+ catch (error) {
376
+ // withPhase records the exception + sets the root span ERROR status
377
+ // when we re-throw. This block keeps the existing failure log and
378
+ // user-visible cleanup side effects.
379
+ logger.error('Issue triage task failed', {
411
380
  task: 'issue-triage',
412
381
  issueNumber: ctx.issueNumber,
413
- classification: fixAssessment.classification,
414
- confidence: fixAssessment.confidence,
415
- scope: fixAssessment.scope,
416
- });
417
- }
418
- if (shouldPostHeroRoutingComment(fixAssessment)) {
419
- const routingComment = buildHeroRoutingComment({
420
- labels: labelNames,
421
- primaryRepo: resolution.repositories.length === 1 ? resolution.repositories[0] : undefined,
422
- triageOutput: normalized.output,
423
- });
424
- await postHeroRoutingComment({
425
- octokit: writeOctokit,
426
- ctx,
427
- body: routingComment,
428
- });
429
- }
430
- if (enableAutoFix && fixAssessment) {
431
- const fixMetricRepository = fixAssessment.targetRepo ??
432
- (clonedRepositories.length === 1
433
- ? clonedRepositories[0].repository.fullName
434
- : `${ctx.owner}/${ctx.repo}`);
435
- pendingFixMetric = {
436
- repository: fixMetricRepository,
437
- startedAt: performance.now(),
438
- outcome: 'failure',
439
- };
440
- const fixResult = await runConfiguredFix({
441
- ctx,
442
- octokit: writeOctokit,
443
- triageOutput: normalized.output,
444
- fixAssessment,
445
- clonedRepos: clonedRepositories,
446
- issueTitle: issue.title ?? 'Untitled issue',
447
- issueUrl: issue.html_url ?? getIssueUrl(ctx.owner, ctx.repo, ctx.issueNumber),
448
- ghToken: issueInstallationAuth.token,
449
- loadRepoConfig: getCachedRepoConfig,
450
- });
451
- logger.info('triage.fix_attempted', {
452
- task: 'issue-triage',
453
382
  trigger: ctx.triggerType,
454
- classification: fixAssessment.classification,
455
- attempted: fixResult.attempted,
456
- });
457
- logger.info('Fix attempt result', {
458
- task: 'issue-triage',
459
- issueNumber: ctx.issueNumber,
460
- attempted: fixResult.attempted,
461
- prCreated: fixResult.attempted && fixResult.prCreated,
462
- reason: 'reason' in fixResult ? fixResult.reason : undefined,
463
- prUrl: 'prUrl' in fixResult ? fixResult.prUrl : undefined,
464
- pr_number: 'prNumber' in fixResult ? fixResult.prNumber : undefined,
383
+ error: error instanceof Error ? error.message : String(error),
465
384
  durationMs: performance.now() - startedAt,
466
385
  });
467
- pendingFixMetric = {
468
- repository: fixResult.repository,
469
- startedAt: pendingFixMetric.startedAt,
470
- outcome: fixResult.attempted && fixResult.prCreated ? 'success' : 'failure',
471
- };
472
- const shouldPostFailureComment = await shouldPostAutoFixFailureComment(fixResult, {
473
- dryRun: ctx.dryRun,
474
- octokit: writeOctokit,
475
- ctx,
476
- });
477
- if (shouldPostFailureComment) {
386
+ if (ctx.triggerType === 'auto' && !triageCommentPosted) {
387
+ await deleteAutoTriagePlaceholder({
388
+ octokit: writeOctokit,
389
+ ctx,
390
+ reason: 'auto_triage_failed_before_final_comment',
391
+ });
392
+ }
393
+ if (!ctx.dryRun && ctx.triggerType !== 'auto') {
478
394
  try {
479
- await postAutoFixFailureComment({
480
- octokit: writeOctokit,
481
- ctx,
482
- reason: 'reason' in fixResult ? fixResult.reason : 'Unknown reason',
483
- targetRepoFullName: fixResult.repository,
484
- });
395
+ const reason = error instanceof Error ? error.message : String(error);
396
+ await postFinalTriageComment(buildTriageFailureMessage(reason));
485
397
  }
486
398
  catch (commentError) {
487
- logger.warn('Failed to post auto-fix failure comment', {
399
+ logger.warn('Failed to post issue triage failure response', {
488
400
  task: 'issue-triage',
489
- issue_number: ctx.issueNumber,
401
+ issueNumber: ctx.issueNumber,
490
402
  error: commentError instanceof Error
491
403
  ? commentError.message
492
404
  : String(commentError),
493
405
  });
494
406
  }
495
407
  }
408
+ throw error;
496
409
  }
497
- else {
498
- logger.info('triage.fix_attempted', {
499
- task: 'issue-triage',
500
- trigger: ctx.triggerType,
501
- classification: fixAssessment?.classification ?? 'none',
502
- attempted: false,
503
- });
504
- }
505
- }
506
- catch (error) {
507
- logger.error('Issue triage task failed', {
508
- task: 'issue-triage',
509
- issueNumber: ctx.issueNumber,
510
- trigger: ctx.triggerType,
511
- error: error instanceof Error ? error.message : String(error),
512
- durationMs: performance.now() - startedAt,
513
- });
514
- if (ctx.triggerType === 'auto' && !triageCommentPosted) {
515
- await deleteAutoTriagePlaceholder({
410
+ finally {
411
+ await removeIssueCommentReactionIfPresent({
516
412
  octokit: writeOctokit,
517
- ctx,
518
- reason: 'auto_triage_failed_before_final_comment',
413
+ owner: ctx.owner,
414
+ repo: ctx.repo,
415
+ commentId: ctx.commentId,
416
+ reactionId: ctx.reactionId,
417
+ warnMessage: 'Failed to remove issue triage reaction',
418
+ successMessage: 'Issue triage reaction removed',
419
+ logMeta: {
420
+ task: 'issue-triage',
421
+ reactionId: ctx.reactionId,
422
+ },
519
423
  });
520
- }
521
- if (!ctx.dryRun && ctx.triggerType !== 'auto') {
522
- try {
523
- const reason = error instanceof Error ? error.message : String(error);
524
- await postFinalTriageComment(`${TRIAGE_MARKER}\n\n${buildTriageFailureMessage(reason)}`);
424
+ if (pendingFixMetric) {
425
+ await recordTaskMetrics({
426
+ type: 'fix',
427
+ repository: pendingFixMetric.repository,
428
+ model: TRIAGE_PROVIDER,
429
+ outcome: pendingFixMetric.outcome,
430
+ durationMs: performance.now() - pendingFixMetric.startedAt,
431
+ });
525
432
  }
526
- catch (commentError) {
527
- logger.warn('Failed to post issue triage failure response', {
528
- task: 'issue-triage',
529
- issueNumber: ctx.issueNumber,
530
- error: commentError instanceof Error ? commentError.message : String(commentError),
433
+ if (shouldRecordTriageMetric) {
434
+ await recordTaskMetrics({
435
+ type: 'triage',
436
+ repository: triageMetricRepository,
437
+ model: TRIAGE_PROVIDER,
438
+ outcome: triageMetricOutcome,
439
+ durationMs: triageDurationMs ?? performance.now() - startedAt,
531
440
  });
532
441
  }
533
442
  }
534
- throw error;
535
- }
536
- finally {
537
- await removeIssueCommentReactionIfPresent({
538
- octokit: writeOctokit,
539
- owner: ctx.owner,
540
- repo: ctx.repo,
541
- commentId: ctx.commentId,
542
- reactionId: ctx.reactionId,
543
- warnMessage: 'Failed to remove issue triage reaction',
544
- successMessage: 'Issue triage reaction removed',
545
- logMeta: {
546
- task: 'issue-triage',
547
- reactionId: ctx.reactionId,
548
- },
549
- });
550
- logger.info('Issue triage task finalized', {
551
- task: 'issue-triage',
552
- issueNumber: ctx.issueNumber,
553
- durationMs: performance.now() - startedAt,
554
- });
555
- if (pendingFixMetric) {
556
- await recordTaskMetrics({
557
- type: 'fix',
558
- repository: pendingFixMetric.repository,
559
- model: TRIAGE_PROVIDER,
560
- outcome: pendingFixMetric.outcome,
561
- durationMs: performance.now() - pendingFixMetric.startedAt,
562
- });
563
- }
564
- if (shouldRecordTriageMetric) {
565
- await recordTaskMetrics({
566
- type: 'triage',
567
- repository: triageMetricRepository,
568
- model: TRIAGE_PROVIDER,
569
- outcome: triageMetricOutcome,
570
- durationMs: triageDurationMs ?? performance.now() - startedAt,
571
- });
572
- }
573
- }
443
+ });
574
444
  }
575
445
  async function main() {
576
446
  try {