@doist/doistbot-cli 1.0.5 → 1.0.7

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 (61) hide show
  1. package/dist/actions/auth.js +78 -24
  2. package/dist/actions/doctor.js +14 -7
  3. package/dist/actions/review.js +67 -10
  4. package/dist/auth.js +51 -6
  5. package/dist/config.js +77 -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 +285 -174
  16. package/sandbox/dist/core/prompt-template.js +45 -3
  17. package/sandbox/dist/core/repository-map.js +24 -0
  18. package/sandbox/dist/core/tracing-setup.js +15 -0
  19. package/sandbox/dist/core/tracing.js +80 -0
  20. package/sandbox/dist/main.js +4 -1
  21. package/sandbox/dist/providers/credentials.js +12 -2
  22. package/sandbox/dist/providers/gemini.js +5 -1
  23. package/sandbox/dist/providers/helpers.js +63 -17
  24. package/sandbox/dist/providers/index.js +4 -0
  25. package/sandbox/dist/providers/openrouter-pi.js +25 -0
  26. package/sandbox/dist/providers/pi-review.js +19 -2
  27. package/sandbox/dist/tasks/chat/chat.js +173 -102
  28. package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +210 -185
  29. package/sandbox/dist/tasks/issue-summarize/model.js +120 -49
  30. package/sandbox/dist/tasks/issue-summarize/prompt.js +2 -2
  31. package/sandbox/dist/tasks/issue-summarize/summarize.js +104 -88
  32. package/sandbox/dist/tasks/issue-triage/clone.js +40 -0
  33. package/sandbox/dist/tasks/issue-triage/context.js +25 -1
  34. package/sandbox/dist/tasks/issue-triage/fix-attempt.js +33 -10
  35. package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +154 -101
  36. package/sandbox/dist/tasks/issue-triage/fix-loop/runners.js +15 -6
  37. package/sandbox/dist/tasks/issue-triage/hero-group-map.js +2 -0
  38. package/sandbox/dist/tasks/issue-triage/model.js +120 -36
  39. package/sandbox/dist/tasks/issue-triage/output.js +27 -0
  40. package/sandbox/dist/tasks/issue-triage/pr-creator.js +5 -4
  41. package/sandbox/dist/tasks/issue-triage/prompt.js +2 -2
  42. package/sandbox/dist/tasks/issue-triage/resolution.js +26 -0
  43. package/sandbox/dist/tasks/issue-triage/routing.js +9 -9
  44. package/sandbox/dist/tasks/issue-triage/side-effects.js +9 -0
  45. package/sandbox/dist/tasks/issue-triage/triage.js +290 -420
  46. package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +3 -3
  47. package/sandbox/dist/tasks/review/check-run.js +2 -1
  48. package/sandbox/dist/tasks/review/conversation-context.js +2 -1
  49. package/sandbox/dist/tasks/review/engines/dedupe.js +110 -44
  50. package/sandbox/dist/tasks/review/engines/multi-focus.js +254 -113
  51. package/sandbox/dist/tasks/review/engines/shared.js +43 -6
  52. package/sandbox/dist/tasks/review/github-comment-format.js +72 -10
  53. package/sandbox/dist/tasks/review/local.js +7 -4
  54. package/sandbox/dist/tasks/review/review-summary.js +48 -7
  55. package/sandbox/dist/tasks/review/review.js +267 -186
  56. package/sandbox/dist/tasks/review/runner.js +33 -7
  57. package/sandbox/dist/tasks/review/summary-model.js +189 -20
  58. package/sandbox/dist/tasks/review/thinking-level.js +4 -0
  59. package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +4 -8
  60. package/sandbox/node_modules/doistbot-repo-config/dist/index.js +8 -20
  61. package/sandbox/src/tasks/review/prompts/review-multi-focus-base-prompt.md +1 -0
@@ -1,9 +1,11 @@
1
1
  import { performance } from 'node:perf_hooks';
2
+ import { SpanStatusCode } from '@opentelemetry/api';
2
3
  import { isKnownDoistbotLogin } from '../../core/bot-logins.js';
3
4
  import { recordTaskMetrics } from '../../core/datadog-metrics.js';
4
5
  import { logger } from '../../core/logging.js';
5
6
  import { removeIssueCommentReactionIfPresent } from '../../core/reactions.js';
6
7
  import { resolveIssueRepositories } from '../../core/repository-resolver.js';
8
+ import { recordDecision, withPhase } from '../../core/tracing.js';
7
9
  import { parseIssueTriageContext } from '../issue-triage/context.js';
8
10
  import { attemptFix } from '../issue-triage/fix-attempt.js';
9
11
  import { parseFixAssessment } from '../issue-triage/output.js';
@@ -19,217 +21,240 @@ async function main() {
19
21
  repository: `${ctx.owner}/${ctx.repo}`,
20
22
  issueNumber: ctx.issueNumber,
21
23
  });
22
- logger.info('Fix retry task started', {
24
+ // Root span for the whole run. The Codex fix attempt is wrapped in its own
25
+ // issue-fix-retry.fix-attempt phase below, keeping its nested model span in
26
+ // this task's namespace rather than triage's.
27
+ await withPhase('issue-fix-retry', {
23
28
  task: 'issue-fix-retry',
24
- issueNumber: ctx.issueNumber,
25
- commentId: ctx.commentId,
26
- });
27
- const { app, readOctokit, writeOctokit } = await createGitHubClients(ctx);
28
- async function postComment(body) {
29
- if (ctx.dryRun)
30
- return;
31
- await writeOctokit.rest.issues.createComment({
32
- owner: ctx.owner,
33
- repo: ctx.repo,
34
- issue_number: ctx.issueNumber,
35
- body,
36
- });
37
- }
38
- try {
39
- const [issue, comments] = await Promise.all([
40
- fetchIssue(readOctokit, ctx),
41
- fetchIssueComments(readOctokit, ctx),
42
- ]);
43
- // Find the most recent triage comment from the bot (search backwards to get the latest one)
44
- let triageComment;
45
- for (let i = comments.length - 1; i >= 0; i--) {
46
- const comment = comments[i];
47
- if (comment &&
48
- isKnownDoistbotLogin(comment.user?.login) &&
49
- comment.body?.includes(TRIAGE_MARKER)) {
50
- triageComment = comment;
51
- break;
52
- }
53
- }
54
- if (!triageComment?.body) {
55
- logger.warn('No triage comment found for fix retry', {
56
- task: 'issue-fix-retry',
57
- issueNumber: ctx.issueNumber,
29
+ 'gen_ai.operation.name': 'invoke_agent',
30
+ 'gen_ai.agent.name': 'doistbot.issue-fix-retry',
31
+ 'issue.number': ctx.issueNumber,
32
+ 'comment.id': ctx.commentId,
33
+ dry_run: ctx.dryRun,
34
+ }, async (span) => {
35
+ const { app, readOctokit, writeOctokit } = await createGitHubClients(ctx);
36
+ async function postComment(body) {
37
+ if (ctx.dryRun)
38
+ return;
39
+ await writeOctokit.rest.issues.createComment({
40
+ owner: ctx.owner,
41
+ repo: ctx.repo,
42
+ issue_number: ctx.issueNumber,
43
+ body,
58
44
  });
59
- await postComment('No previous triage found on this issue. Run `@doistbot /triage` first, then retry with `@doistbot /fix`.');
60
- return;
61
45
  }
62
- // Strip the triage marker to get the raw triage output
63
- const triageOutput = triageComment.body.replace(TRIAGE_MARKER, '').trim();
64
- const fixAssessment = parseFixAssessment(triageOutput);
65
- if (!fixAssessment) {
66
- logger.warn('No fix assessment found in triage output', {
46
+ try {
47
+ const [issue, comments] = await Promise.all([
48
+ fetchIssue(readOctokit, ctx),
49
+ fetchIssueComments(readOctokit, ctx),
50
+ ]);
51
+ // Find the most recent triage comment from the bot (search backwards to get the latest one)
52
+ let triageComment;
53
+ for (let i = comments.length - 1; i >= 0; i--) {
54
+ const comment = comments[i];
55
+ if (comment &&
56
+ isKnownDoistbotLogin(comment.user?.login) &&
57
+ comment.body?.includes(TRIAGE_MARKER)) {
58
+ triageComment = comment;
59
+ break;
60
+ }
61
+ }
62
+ if (!triageComment?.body) {
63
+ // User-actionable (no prior triage), not a malfunction → span stays ok.
64
+ logger.warn('No triage comment found for fix retry', {
65
+ task: 'issue-fix-retry',
66
+ issueNumber: ctx.issueNumber,
67
+ });
68
+ await postComment('No previous triage found on this issue. Run `@doistbot /triage` first, then retry with `@doistbot /fix`.');
69
+ return;
70
+ }
71
+ // Strip the triage marker to get the raw triage output
72
+ const triageOutput = triageComment.body.replace(TRIAGE_MARKER, '').trim();
73
+ const fixAssessment = parseFixAssessment(triageOutput);
74
+ if (!fixAssessment) {
75
+ logger.warn('No fix assessment found in triage output', {
76
+ task: 'issue-fix-retry',
77
+ issueNumber: ctx.issueNumber,
78
+ });
79
+ await postComment("The previous triage doesn't include an automated fix assessment. Try running `@doistbot /triage` again on a repo with auto-fix enabled.");
80
+ return;
81
+ }
82
+ if (fixAssessment.targetRepo) {
83
+ metricRepository = fixAssessment.targetRepo;
84
+ }
85
+ logger.info('Fix assessment found from previous triage', {
67
86
  task: 'issue-fix-retry',
68
87
  issueNumber: ctx.issueNumber,
88
+ classification: fixAssessment.classification,
89
+ confidence: fixAssessment.confidence,
90
+ scope: fixAssessment.scope,
69
91
  });
70
- await postComment("The previous triage doesn't include an automated fix assessment. Try running `@doistbot /triage` again on a repo with auto-fix enabled.");
71
- return;
72
- }
73
- if (fixAssessment.targetRepo) {
74
- metricRepository = fixAssessment.targetRepo;
75
- }
76
- logger.info('Fix assessment found from previous triage', {
77
- task: 'issue-fix-retry',
78
- issueNumber: ctx.issueNumber,
79
- classification: fixAssessment.classification,
80
- confidence: fixAssessment.confidence,
81
- scope: fixAssessment.scope,
82
- });
83
- // Resolve repositories to clone. Prefer the target repo from the assessment
84
- // (stable across label changes) and fall back to label-based resolution.
85
- const reposDir = prepareWorkspaceReposDirectory();
86
- const clonedRepositories = [];
87
- if (fixAssessment.targetRepo) {
88
- const resolution = resolveIssueRepositories(extractLabelNames(issue.labels));
89
- const repoTarget = resolution.status === 'resolved'
90
- ? resolution.repositories.find((r) => r.fullName === fixAssessment.targetRepo)
91
- : undefined;
92
- if (repoTarget) {
93
- try {
94
- const cloneResult = await cloneRepository({
95
- app,
96
- repository: repoTarget,
97
- reposDir,
98
- fallbackInstallationId: ctx.installationId,
99
- });
100
- clonedRepositories.push(cloneResult);
101
- metricRepository = cloneResult.repository.fullName;
92
+ // Resolve repositories to clone. Prefer the target repo from the assessment
93
+ // (stable across label changes) and fall back to label-based resolution.
94
+ const reposDir = prepareWorkspaceReposDirectory();
95
+ const clonedRepositories = [];
96
+ if (fixAssessment.targetRepo) {
97
+ const resolution = resolveIssueRepositories(extractLabelNames(issue.labels));
98
+ const repoTarget = resolution.status === 'resolved'
99
+ ? resolution.repositories.find((r) => r.fullName === fixAssessment.targetRepo)
100
+ : undefined;
101
+ if (repoTarget) {
102
+ try {
103
+ const cloneResult = await cloneRepository({
104
+ app,
105
+ repository: repoTarget,
106
+ reposDir,
107
+ fallbackInstallationId: ctx.installationId,
108
+ });
109
+ clonedRepositories.push(cloneResult);
110
+ metricRepository = cloneResult.repository.fullName;
111
+ }
112
+ catch (error) {
113
+ logger.warn('Fix retry clone failed for assessment target repo', {
114
+ task: 'issue-fix-retry',
115
+ repository: fixAssessment.targetRepo,
116
+ error: error instanceof Error ? error.message : String(error),
117
+ });
118
+ }
102
119
  }
103
- catch (error) {
104
- logger.warn('Fix retry clone failed for assessment target repo', {
120
+ }
121
+ // Fall back to label-based resolution if the assessment target wasn't cloned
122
+ if (clonedRepositories.length === 0) {
123
+ const labelNames = extractLabelNames(issue.labels);
124
+ const resolution = resolveIssueRepositories(labelNames);
125
+ if (resolution.status === 'unresolved') {
126
+ // User-actionable (labels don't map to a repo) → span stays ok.
127
+ logger.warn('Fix retry failed: could not resolve repositories from labels', {
105
128
  task: 'issue-fix-retry',
106
- repository: fixAssessment.targetRepo,
107
- error: error instanceof Error ? error.message : String(error),
129
+ issueNumber: ctx.issueNumber,
130
+ reason: resolution.reason,
108
131
  });
132
+ await postComment(`Could not resolve target repositories from the issue labels. Please check the labels and try again.\n\nReason: ${resolution.reason}`);
133
+ return;
134
+ }
135
+ if (resolution.repositories.length === 1) {
136
+ metricRepository = resolution.repositories[0].fullName;
137
+ }
138
+ for (const repository of resolution.repositories) {
139
+ try {
140
+ const cloneResult = await cloneRepository({
141
+ app,
142
+ repository,
143
+ reposDir,
144
+ fallbackInstallationId: ctx.installationId,
145
+ });
146
+ clonedRepositories.push(cloneResult);
147
+ }
148
+ catch (error) {
149
+ logger.warn('Fix retry clone failed', {
150
+ task: 'issue-fix-retry',
151
+ repository: repository.fullName,
152
+ error: error instanceof Error ? error.message : String(error),
153
+ });
154
+ }
109
155
  }
110
156
  }
111
- }
112
- // Fall back to label-based resolution if the assessment target wasn't cloned
113
- if (clonedRepositories.length === 0) {
114
- const labelNames = extractLabelNames(issue.labels);
115
- const resolution = resolveIssueRepositories(labelNames);
116
- if (resolution.status === 'unresolved') {
117
- logger.warn('Fix retry failed: could not resolve repositories from labels', {
157
+ if (clonedRepositories.length === 0) {
158
+ // Infrastructure failure → mark the run failed.
159
+ const message = 'Fix retry failed: all repository clones failed';
160
+ logger.error(message, {
118
161
  task: 'issue-fix-retry',
119
162
  issueNumber: ctx.issueNumber,
120
- reason: resolution.reason,
121
163
  });
122
- await postComment(`Could not resolve target repositories from the issue labels. Please check the labels and try again.\n\nReason: ${resolution.reason}`);
164
+ span.setStatus({ code: SpanStatusCode.ERROR, message });
165
+ await postComment('Fix retry failed — could not clone the target repositories. Please try again later.');
123
166
  return;
124
167
  }
125
- if (resolution.repositories.length === 1) {
126
- metricRepository = resolution.repositories[0].fullName;
127
- }
128
- for (const repository of resolution.repositories) {
129
- try {
130
- const cloneResult = await cloneRepository({
131
- app,
132
- repository,
133
- reposDir,
134
- fallbackInstallationId: ctx.installationId,
135
- });
136
- clonedRepositories.push(cloneResult);
137
- }
138
- catch (error) {
139
- logger.warn('Fix retry clone failed', {
140
- task: 'issue-fix-retry',
141
- repository: repository.fullName,
142
- error: error instanceof Error ? error.message : String(error),
168
+ const issueInstallationAuth = (await app.octokit.auth({
169
+ type: 'installation',
170
+ installationId: ctx.installationId,
171
+ }));
172
+ // Wrap the Codex fix attempt in a retry-namespaced phase; its
173
+ // model span (issue-fix-retry.fix-attempt.model) nests under it.
174
+ const fixResult = await withPhase('issue-fix-retry.fix-attempt', {
175
+ 'gen_ai.operation.name': 'invoke_agent',
176
+ 'gen_ai.agent.name': 'codex.fix',
177
+ }, () => attemptFix({
178
+ ctx,
179
+ octokit: writeOctokit,
180
+ triageOutput,
181
+ fixAssessment,
182
+ clonedRepos: clonedRepositories,
183
+ issueTitle: issue.title ?? 'Untitled issue',
184
+ issueUrl: issue.html_url ?? getIssueUrl(ctx.owner, ctx.repo, ctx.issueNumber),
185
+ ghToken: issueInstallationAuth.token,
186
+ force: true,
187
+ modelPhaseName: 'issue-fix-retry.fix-attempt.model',
188
+ }));
189
+ metricRepository = fixResult.repository;
190
+ metricOutcome = fixResult.attempted && fixResult.prCreated ? 'success' : 'failure';
191
+ const reason = 'reason' in fixResult ? fixResult.reason : undefined;
192
+ const isDryRun = 'dryRun' in fixResult && fixResult.dryRun === true;
193
+ recordDecision('issue-fix-retry.outcome', {
194
+ attempted: fixResult.attempted,
195
+ pr_created: fixResult.attempted && fixResult.prCreated,
196
+ reason,
197
+ pr_url: 'prUrl' in fixResult ? fixResult.prUrl : undefined,
198
+ });
199
+ // No PR is a failed retry — unless it was a dry run (expected).
200
+ if (!fixResult.attempted || !fixResult.prCreated) {
201
+ if (!isDryRun) {
202
+ span.setStatus({
203
+ code: SpanStatusCode.ERROR,
204
+ message: reason ?? 'Fix retry produced no PR',
143
205
  });
144
206
  }
207
+ await postComment(`Fix retry failed to produce a PR.\n\nReason: ${reason ?? 'Unknown reason'}`);
145
208
  }
146
209
  }
147
- if (clonedRepositories.length === 0) {
148
- logger.error('Fix retry failed: all repository clones failed', {
210
+ catch (error) {
211
+ const message = error instanceof Error ? error.message : String(error);
212
+ logger.error('Fix retry task failed', {
149
213
  task: 'issue-fix-retry',
150
214
  issueNumber: ctx.issueNumber,
215
+ error: message,
216
+ durationMs: performance.now() - startedAt,
151
217
  });
152
- await postComment('Fix retry failed — could not clone the target repositories. Please try again later.');
153
- return;
154
- }
155
- const issueInstallationAuth = (await app.octokit.auth({
156
- type: 'installation',
157
- installationId: ctx.installationId,
158
- }));
159
- const fixResult = await attemptFix({
160
- ctx,
161
- octokit: writeOctokit,
162
- triageOutput,
163
- fixAssessment,
164
- clonedRepos: clonedRepositories,
165
- issueTitle: issue.title ?? 'Untitled issue',
166
- issueUrl: issue.html_url ?? getIssueUrl(ctx.owner, ctx.repo, ctx.issueNumber),
167
- ghToken: issueInstallationAuth.token,
168
- force: true,
169
- });
170
- logger.info('Fix retry result', {
171
- task: 'issue-fix-retry',
172
- issueNumber: ctx.issueNumber,
173
- attempted: fixResult.attempted,
174
- prCreated: fixResult.attempted && fixResult.prCreated,
175
- reason: 'reason' in fixResult ? fixResult.reason : undefined,
176
- prUrl: 'prUrl' in fixResult ? fixResult.prUrl : undefined,
177
- durationMs: performance.now() - startedAt,
178
- });
179
- metricRepository = fixResult.repository;
180
- metricOutcome = fixResult.attempted && fixResult.prCreated ? 'success' : 'failure';
181
- // Inform the user if no PR was created
182
- if (!fixResult.attempted || !fixResult.prCreated) {
183
- const reason = 'reason' in fixResult ? fixResult.reason : 'Unknown reason';
184
- await postComment(`Fix retry failed to produce a PR.\n\nReason: ${reason}`);
185
- }
186
- }
187
- catch (error) {
188
- logger.error('Fix retry task failed', {
189
- task: 'issue-fix-retry',
190
- issueNumber: ctx.issueNumber,
191
- error: error instanceof Error ? error.message : String(error),
192
- durationMs: performance.now() - startedAt,
193
- });
194
- try {
195
- await postComment('Sorry, the fix retry encountered an error. Please try again or contact the team maintaining the bot.');
218
+ // The catch swallows the error (posts a user message), so set the
219
+ // span status explicitly — withPhase only auto-marks ERROR on rethrow.
220
+ span.setStatus({ code: SpanStatusCode.ERROR, message });
221
+ try {
222
+ await postComment('Sorry, the fix retry encountered an error. Please try again or contact the team maintaining the bot.');
223
+ }
224
+ catch (commentError) {
225
+ logger.warn('Failed to post fix retry failure response', {
226
+ task: 'issue-fix-retry',
227
+ issueNumber: ctx.issueNumber,
228
+ error: commentError instanceof Error
229
+ ? commentError.message
230
+ : String(commentError),
231
+ });
232
+ }
233
+ exitCode = 1;
196
234
  }
197
- catch (commentError) {
198
- logger.warn('Failed to post fix retry failure response', {
199
- task: 'issue-fix-retry',
200
- issueNumber: ctx.issueNumber,
201
- error: commentError instanceof Error ? commentError.message : String(commentError),
235
+ finally {
236
+ await removeIssueCommentReactionIfPresent({
237
+ octokit: writeOctokit,
238
+ owner: ctx.owner,
239
+ repo: ctx.repo,
240
+ commentId: ctx.commentId,
241
+ reactionId: ctx.reactionId,
242
+ warnMessage: 'Failed to remove fix retry reaction',
243
+ successMessage: 'Fix retry reaction removed',
244
+ logMeta: {
245
+ task: 'issue-fix-retry',
246
+ reactionId: ctx.reactionId,
247
+ },
248
+ });
249
+ await recordTaskMetrics({
250
+ type: 'fix',
251
+ repository: metricRepository,
252
+ model: 'openai',
253
+ outcome: metricOutcome,
254
+ durationMs: performance.now() - startedAt,
202
255
  });
203
256
  }
204
- exitCode = 1;
205
- }
206
- finally {
207
- await removeIssueCommentReactionIfPresent({
208
- octokit: writeOctokit,
209
- owner: ctx.owner,
210
- repo: ctx.repo,
211
- commentId: ctx.commentId,
212
- reactionId: ctx.reactionId,
213
- warnMessage: 'Failed to remove fix retry reaction',
214
- successMessage: 'Fix retry reaction removed',
215
- logMeta: {
216
- task: 'issue-fix-retry',
217
- reactionId: ctx.reactionId,
218
- },
219
- });
220
- await recordTaskMetrics({
221
- type: 'fix',
222
- repository: metricRepository,
223
- model: 'openai',
224
- outcome: metricOutcome,
225
- durationMs: performance.now() - startedAt,
226
- });
227
- logger.info('Fix retry task finalized', {
228
- task: 'issue-fix-retry',
229
- issueNumber: ctx.issueNumber,
230
- durationMs: performance.now() - startedAt,
231
- });
232
- }
257
+ });
233
258
  if (exitCode !== 0) {
234
259
  process.exit(exitCode);
235
260
  }