@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.
- package/dist/actions/auth.js +55 -21
- package/dist/actions/doctor.js +13 -6
- package/dist/actions/review.js +75 -10
- package/dist/auth.js +51 -6
- package/dist/config.js +34 -5
- package/dist/env.js +1 -0
- package/dist/runtime.js +1 -1
- package/dist/terminal.js +2 -0
- package/package.json +4 -4
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +4 -8
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +8 -20
- package/sandbox/dist/core/check-run.js +16 -0
- package/sandbox/dist/core/datadog-metrics.js +6 -52
- package/sandbox/dist/core/logger-span-processor.js +78 -0
- package/sandbox/dist/core/pi.js +257 -176
- package/sandbox/dist/core/tracing-setup.js +15 -0
- package/sandbox/dist/core/tracing.js +71 -0
- package/sandbox/dist/main.js +4 -1
- package/sandbox/dist/providers/credentials.js +12 -2
- package/sandbox/dist/providers/gemini.js +5 -1
- package/sandbox/dist/providers/helpers.js +12 -17
- package/sandbox/dist/providers/index.js +4 -0
- package/sandbox/dist/providers/openrouter-pi.js +25 -0
- package/sandbox/dist/providers/pi-review.js +12 -1
- package/sandbox/dist/tasks/chat/chat.js +161 -101
- package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +210 -185
- package/sandbox/dist/tasks/issue-summarize/model.js +1 -1
- package/sandbox/dist/tasks/issue-triage/clone.js +40 -0
- package/sandbox/dist/tasks/issue-triage/context.js +25 -1
- package/sandbox/dist/tasks/issue-triage/fix-attempt.js +31 -8
- package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +154 -101
- package/sandbox/dist/tasks/issue-triage/fix-loop/runners.js +15 -6
- package/sandbox/dist/tasks/issue-triage/model.js +120 -36
- package/sandbox/dist/tasks/issue-triage/output.js +27 -0
- package/sandbox/dist/tasks/issue-triage/pr-creator.js +3 -2
- package/sandbox/dist/tasks/issue-triage/resolution.js +26 -0
- package/sandbox/dist/tasks/issue-triage/routing.js +9 -9
- package/sandbox/dist/tasks/issue-triage/side-effects.js +9 -0
- package/sandbox/dist/tasks/issue-triage/triage.js +288 -418
- package/sandbox/dist/tasks/review/check-run.js +2 -1
- package/sandbox/dist/tasks/review/conversation-context.js +2 -1
- package/sandbox/dist/tasks/review/engines/dedupe.js +5 -4
- package/sandbox/dist/tasks/review/engines/multi-focus.js +40 -9
- package/sandbox/dist/tasks/review/engines/shared.js +8 -6
- package/sandbox/dist/tasks/review/github-comment-format.js +64 -10
- package/sandbox/dist/tasks/review/local.js +2 -4
- package/sandbox/dist/tasks/review/review-summary.js +45 -7
- package/sandbox/dist/tasks/review/review.js +38 -24
- package/sandbox/dist/tasks/review/runner.js +23 -2
- package/sandbox/dist/tasks/review/summary-model.js +164 -17
- package/sandbox/dist/tasks/review/thinking-level.js +4 -0
- package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +4 -8
- package/sandbox/node_modules/doistbot-repo-config/dist/index.js +8 -20
|
@@ -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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -4,7 +4,7 @@ const ISSUE_SUMMARIZE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
|
4
4
|
const ISSUE_SUMMARIZE_MULTIMODAL_MODEL = 'google/gemini-2.5-pro';
|
|
5
5
|
export function createIssueSummarizeModel(apiKey) {
|
|
6
6
|
return createPiInvoker({
|
|
7
|
-
provider: '
|
|
7
|
+
provider: 'zai',
|
|
8
8
|
apiKey,
|
|
9
9
|
piProviderId: 'openrouter',
|
|
10
10
|
cwd: process.cwd(),
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { logger } from '../../core/logging.js';
|
|
2
|
+
import { withPhase } from '../../core/tracing.js';
|
|
3
|
+
import { cloneRepository } from './shared.js';
|
|
4
|
+
export async function cloneTriageRepositories(params) {
|
|
5
|
+
return withPhase('triage.clone', { 'repos.count': params.repositories.length }, async (span) => {
|
|
6
|
+
const cloneFn = params.cloneRepositoryImpl ?? cloneRepository;
|
|
7
|
+
const clonedRepositories = [];
|
|
8
|
+
const failedRepositories = [];
|
|
9
|
+
for (const repository of params.repositories) {
|
|
10
|
+
try {
|
|
11
|
+
const cloneResult = await cloneFn({
|
|
12
|
+
app: params.app,
|
|
13
|
+
repository,
|
|
14
|
+
reposDir: params.reposDir,
|
|
15
|
+
fallbackInstallationId: params.fallbackInstallationId,
|
|
16
|
+
});
|
|
17
|
+
clonedRepositories.push(cloneResult);
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
21
|
+
// Keep per-repo diagnostics (auth/git errors) visible even
|
|
22
|
+
// when EVERY clone fails and the orchestrator bails early.
|
|
23
|
+
logger.warn('Repository clone failed', {
|
|
24
|
+
repository: repository.fullName,
|
|
25
|
+
error: message,
|
|
26
|
+
});
|
|
27
|
+
failedRepositories.push({
|
|
28
|
+
repository: repository.fullName,
|
|
29
|
+
error: message,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
span.setAttributes({
|
|
34
|
+
'clone.success_count': clonedRepositories.length,
|
|
35
|
+
'clone.failed_count': failedRepositories.length,
|
|
36
|
+
'repos.count': params.repositories.length,
|
|
37
|
+
});
|
|
38
|
+
return { clonedRepositories, failedRepositories };
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { optionalPositiveInt, requiredEnv, requiredPositiveInt } from '../../core/shared.js';
|
|
2
|
+
import { withPhase } from '../../core/tracing.js';
|
|
3
|
+
import { extractLabelNames, fetchIssue, fetchIssueComments, } from './shared.js';
|
|
2
4
|
function parseOptionalBoolean(value) {
|
|
3
5
|
const normalized = value?.trim().toLowerCase();
|
|
4
6
|
if (normalized === 'true')
|
|
@@ -28,6 +30,7 @@ export function parseIssueTriageContext(env = process.env) {
|
|
|
28
30
|
privateKey: requiredEnv('PRIVATE_KEY', env),
|
|
29
31
|
githubToken: requiredEnv('GITHUB_TOKEN', env),
|
|
30
32
|
openaiApiKey: parseOpenAIApiKey(env),
|
|
33
|
+
geminiApiKey: env.GEMINI_API_KEY?.trim() || undefined,
|
|
31
34
|
commentId,
|
|
32
35
|
reactionId,
|
|
33
36
|
placeholderCommentId: optionalPositiveInt(env.PLACEHOLDER_COMMENT_ID),
|
|
@@ -35,6 +38,27 @@ export function parseIssueTriageContext(env = process.env) {
|
|
|
35
38
|
dryRun: env.DRY_RUN?.toLowerCase() === 'true',
|
|
36
39
|
triggerType,
|
|
37
40
|
enableAutoFixOverride: parseOptionalBoolean(env.ENABLE_AUTO_FIX),
|
|
38
|
-
openRouterApiKey: env.OPENROUTER_API_KEY?.trim() || undefined,
|
|
39
41
|
};
|
|
40
42
|
}
|
|
43
|
+
export async function loadTriageContext(params) {
|
|
44
|
+
return withPhase('triage.context', { 'issue.number': params.issueNumber }, async (span) => {
|
|
45
|
+
const [issue, comments] = await Promise.all([
|
|
46
|
+
fetchIssue(params.octokit, {
|
|
47
|
+
owner: params.owner,
|
|
48
|
+
repo: params.repo,
|
|
49
|
+
issueNumber: params.issueNumber,
|
|
50
|
+
}),
|
|
51
|
+
fetchIssueComments(params.octokit, {
|
|
52
|
+
owner: params.owner,
|
|
53
|
+
repo: params.repo,
|
|
54
|
+
issueNumber: params.issueNumber,
|
|
55
|
+
}),
|
|
56
|
+
]);
|
|
57
|
+
const labelNames = extractLabelNames(issue.labels);
|
|
58
|
+
span.setAttributes({
|
|
59
|
+
'comments.count': comments.length,
|
|
60
|
+
'labels.count': labelNames.length,
|
|
61
|
+
});
|
|
62
|
+
return { issue, comments, labelNames };
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -164,8 +164,20 @@ export function buildFollowUpCommentBody({ prUrl, targetRepoFullName, fixAssessm
|
|
|
164
164
|
`The PR targets \`${targetRepoFullName}\`. Please review it carefully before merging.`,
|
|
165
165
|
].join('\n');
|
|
166
166
|
}
|
|
167
|
-
|
|
168
|
-
|
|
167
|
+
// Detect the "## Cannot Fix" verdict and pull Codex's own explanation in one pass,
|
|
168
|
+
// so the failure comment can tell the human *why* it bailed, not just that it did.
|
|
169
|
+
// Returns null when the marker is absent (not a cannot-fix verdict), the explanation
|
|
170
|
+
// string when present, or '' when the model gave the verdict with no explanation.
|
|
171
|
+
// The full tail is intentional — the prompt makes "## Cannot Fix" the terminal output.
|
|
172
|
+
// ponytail: 2000-char cap keeps the comment readable; raise it if reasons get truncated.
|
|
173
|
+
export function extractCannotFixDetail(output) {
|
|
174
|
+
const idx = output.indexOf(CANNOT_FIX_MARKER);
|
|
175
|
+
if (idx === -1)
|
|
176
|
+
return null;
|
|
177
|
+
return output
|
|
178
|
+
.slice(idx + CANNOT_FIX_MARKER.length)
|
|
179
|
+
.trim()
|
|
180
|
+
.slice(0, 2000);
|
|
169
181
|
}
|
|
170
182
|
async function findSourceIssueCommentByMarker({ octokit, ctx, marker, }) {
|
|
171
183
|
const comments = await octokit.paginate(octokit.rest.issues.listComments, {
|
|
@@ -187,8 +199,6 @@ export async function shouldPostAutoFixFailureComment(fixResult, { dryRun, octok
|
|
|
187
199
|
// a "didn't produce a fix PR" comment would be misleading.
|
|
188
200
|
if (fixResult.prNumber !== undefined)
|
|
189
201
|
return false;
|
|
190
|
-
if (isNonRetryableFailure(fixResult.reason))
|
|
191
|
-
return false;
|
|
192
202
|
if (fixResult.reason.startsWith(FIX_LOOP_FAILED_REASON_PREFIX)) {
|
|
193
203
|
try {
|
|
194
204
|
const hasFixLoopStartComment = await sourceIssueHasFixLoopStartComment({ octokit, ctx });
|
|
@@ -231,9 +241,11 @@ export function buildAutoFixFailureCommentBody({ reason, targetRepoFullName, })
|
|
|
231
241
|
'',
|
|
232
242
|
'**Reason:**',
|
|
233
243
|
'',
|
|
234
|
-
'
|
|
244
|
+
// Tilde fence, not backticks: the reason often carries Codex's own
|
|
245
|
+
// backtick-fenced output (logs, CLI), which would close a ``` block early.
|
|
246
|
+
'~~~',
|
|
235
247
|
reason,
|
|
236
|
-
'
|
|
248
|
+
'~~~',
|
|
237
249
|
'',
|
|
238
250
|
'You can retry by commenting `@doistbot /fix`.',
|
|
239
251
|
].join('\n');
|
|
@@ -365,6 +377,9 @@ export async function runFixCore(input) {
|
|
|
365
377
|
? SPECULATIVE_CODEX_TIMEOUT_MS
|
|
366
378
|
: AUTO_FIX_CODEX_TIMEOUT_MS;
|
|
367
379
|
const codexResult = await invokeTriageModel({
|
|
380
|
+
// Distinct phase name: this is fix generation, not triage analysis.
|
|
381
|
+
// Callers (e.g. issue-fix-retry) override to keep it in their namespace.
|
|
382
|
+
phaseName: input.modelPhaseName ?? 'triage.fix-attempt.model',
|
|
368
383
|
prompt,
|
|
369
384
|
apiKey: ctx.openaiApiKey,
|
|
370
385
|
cwd: targetRepo.localPath,
|
|
@@ -391,13 +406,19 @@ export async function runFixCore(input) {
|
|
|
391
406
|
outputChars: fixDescription.length,
|
|
392
407
|
});
|
|
393
408
|
// Check if Codex reported it cannot fix
|
|
394
|
-
|
|
409
|
+
const cannotFixDetail = extractCannotFixDetail(fixDescription);
|
|
410
|
+
if (cannotFixDetail !== null) {
|
|
395
411
|
logger.info('Fix Codex reported cannot fix', {
|
|
396
412
|
task: 'issue-triage',
|
|
397
413
|
issueNumber: ctx.issueNumber,
|
|
398
414
|
output: fixDescription.slice(0, 500),
|
|
399
415
|
});
|
|
400
|
-
return {
|
|
416
|
+
return {
|
|
417
|
+
ok: false,
|
|
418
|
+
reason: cannotFixDetail
|
|
419
|
+
? `${CODEX_CANNOT_FIX_REASON}\n\n${cannotFixDetail}`
|
|
420
|
+
: CODEX_CANNOT_FIX_REASON,
|
|
421
|
+
};
|
|
401
422
|
}
|
|
402
423
|
// Capture the diff
|
|
403
424
|
const diff = captureGitDiff(targetRepo.localPath);
|
|
@@ -499,6 +520,7 @@ export async function attemptFix(input) {
|
|
|
499
520
|
issueUrl: input.issueUrl,
|
|
500
521
|
ghToken: input.ghToken,
|
|
501
522
|
force,
|
|
523
|
+
modelPhaseName: input.modelPhaseName,
|
|
502
524
|
});
|
|
503
525
|
if (!coreResult.ok) {
|
|
504
526
|
return {
|
|
@@ -520,6 +542,7 @@ export async function attemptFix(input) {
|
|
|
520
542
|
repository: targetRepo.repository.fullName,
|
|
521
543
|
prCreated: false,
|
|
522
544
|
reason: 'Dry run — PR not created.',
|
|
545
|
+
dryRun: true,
|
|
523
546
|
};
|
|
524
547
|
}
|
|
525
548
|
let openedPr;
|