@doist/doistbot-cli 1.0.4 → 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 +62 -28
- package/dist/actions/doctor.js +16 -11
- package/dist/actions/review.js +77 -11
- package/dist/auth.js +53 -8
- package/dist/config.js +40 -6
- package/dist/env.js +1 -1
- package/dist/file-store.js +1 -1
- package/dist/runtime.js +1 -1
- package/dist/terminal.js +2 -7
- package/package.json +7 -7
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +5 -23
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +9 -85
- package/sandbox/dist/core/bot-logins.js +0 -13
- package/sandbox/dist/core/check-run.js +16 -0
- package/sandbox/dist/core/datadog-metrics.js +6 -54
- package/sandbox/dist/core/logger-span-processor.js +78 -0
- package/sandbox/dist/core/pi.js +270 -186
- 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/config.js +1 -1
- package/sandbox/dist/providers/credentials.js +40 -0
- package/sandbox/dist/providers/gemini.js +8 -10
- package/sandbox/dist/providers/helpers.js +18 -38
- package/sandbox/dist/providers/index.js +4 -3
- package/sandbox/dist/providers/openai.js +1 -8
- package/sandbox/dist/providers/openrouter-pi.js +25 -0
- package/sandbox/dist/providers/pi-review.js +28 -55
- package/sandbox/dist/providers/schemas.js +0 -39
- package/sandbox/dist/tasks/chat/chat.js +162 -101
- package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +210 -185
- package/sandbox/dist/tasks/issue-summarize/context.js +1 -1
- package/sandbox/dist/tasks/issue-summarize/model.js +4 -2
- package/sandbox/dist/tasks/issue-summarize/summarize.js +1 -1
- package/sandbox/dist/tasks/issue-triage/clone.js +40 -0
- package/sandbox/dist/tasks/issue-triage/context.js +25 -2
- 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/persist-logs/dd-proxy.js +2 -2
- package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +1 -1
- package/sandbox/dist/tasks/persist-logs/persist-logs.js +1 -1
- package/sandbox/dist/tasks/review/check-run.js +2 -1
- package/sandbox/dist/tasks/review/conversation-context.js +15 -8
- package/sandbox/dist/tasks/review/engines/dedupe.js +19 -9
- package/sandbox/dist/tasks/review/engines/multi-focus.js +62 -36
- package/sandbox/dist/tasks/review/engines/shared.js +15 -61
- package/sandbox/dist/tasks/review/github-comment-format.js +64 -10
- package/sandbox/dist/tasks/review/local.js +2 -29
- package/sandbox/dist/tasks/review/prompt.js +2 -49
- package/sandbox/dist/tasks/review/review-summary.js +120 -0
- package/sandbox/dist/tasks/review/review.js +49 -69
- package/sandbox/dist/tasks/review/runner.js +57 -52
- package/sandbox/dist/tasks/review/summary-model.js +172 -16
- package/sandbox/dist/tasks/review/thinking-level.js +4 -2
- package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +5 -23
- package/sandbox/node_modules/doistbot-repo-config/dist/index.js +9 -85
- package/sandbox/_node_modules/doistbot-repo-config/dist/review-engine.d.ts +0 -11
- package/sandbox/_node_modules/doistbot-repo-config/dist/review-engine.js +0 -41
- package/sandbox/dist/providers/anthropic.js +0 -30
- package/sandbox/dist/tasks/review/author-format.js +0 -14
- package/sandbox/dist/tasks/review/author-profile.js +0 -67
- package/sandbox/dist/tasks/review/council/normalizer.js +0 -104
- package/sandbox/dist/tasks/review/council/orchestrator.js +0 -194
- package/sandbox/dist/tasks/review/council/provider-health.js +0 -31
- package/sandbox/dist/tasks/review/council/summary.js +0 -192
- package/sandbox/dist/tasks/review/council/types.js +0 -4
- package/sandbox/dist/tasks/review/council/voter.js +0 -384
- package/sandbox/dist/tasks/review/engines/council.js +0 -126
- package/sandbox/dist/tasks/review/engines/index.js +0 -12
- package/sandbox/dist/tasks/review/review-engine.js +0 -5
- package/sandbox/node_modules/doistbot-repo-config/dist/review-engine.d.ts +0 -11
- package/sandbox/node_modules/doistbot-repo-config/dist/review-engine.js +0 -41
- package/sandbox/review-prompt.md +0 -209
- package/sandbox/vote-prompt.md +0 -69
|
@@ -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
|
}
|
|
@@ -11,7 +11,7 @@ export function parseSummarizeContext(env = process.env) {
|
|
|
11
11
|
appId: requiredEnv('APP_ID', env),
|
|
12
12
|
privateKey: requiredEnv('PRIVATE_KEY', env),
|
|
13
13
|
githubToken: requiredEnv('GITHUB_TOKEN', env),
|
|
14
|
-
|
|
14
|
+
openRouterApiKey: requiredEnv('OPENROUTER_API_KEY', env),
|
|
15
15
|
commentId,
|
|
16
16
|
reactionId,
|
|
17
17
|
deliveryId: env.DELIVERY_ID,
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { logger } from '../../core/logging.js';
|
|
2
2
|
import { buildPiImageContent, createPiInvoker } from '../../core/pi.js';
|
|
3
3
|
const ISSUE_SUMMARIZE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
4
|
+
const ISSUE_SUMMARIZE_MULTIMODAL_MODEL = 'google/gemini-2.5-pro';
|
|
4
5
|
export function createIssueSummarizeModel(apiKey) {
|
|
5
6
|
return createPiInvoker({
|
|
6
|
-
provider: '
|
|
7
|
+
provider: 'zai',
|
|
7
8
|
apiKey,
|
|
9
|
+
piProviderId: 'openrouter',
|
|
8
10
|
cwd: process.cwd(),
|
|
9
11
|
capabilityPreset: 'read-only',
|
|
10
12
|
});
|
|
@@ -43,7 +45,7 @@ export async function invokeIssueSummarizeModel({ model, prompt, mediaImages, })
|
|
|
43
45
|
.join('\n\n');
|
|
44
46
|
try {
|
|
45
47
|
const result = await model.invoke({
|
|
46
|
-
modelName:
|
|
48
|
+
modelName: ISSUE_SUMMARIZE_MULTIMODAL_MODEL,
|
|
47
49
|
prompt: `${prompt}\n\n<media_context>\n${mediaContext}\n</media_context>`,
|
|
48
50
|
timeoutMs: ISSUE_SUMMARIZE_TIMEOUT_MS,
|
|
49
51
|
images: mediaImages.map((mediaImage) => buildPiImageContent({
|
|
@@ -39,7 +39,7 @@ function summarizeMediaInspectionStats(mediaInspections) {
|
|
|
39
39
|
async function summarizeIssue({ ctx, issue, entries, mediaReferences, mediaInspections, }) {
|
|
40
40
|
const template = loadPromptTemplate();
|
|
41
41
|
const threadPrompt = prepareThreadPrompt(entries);
|
|
42
|
-
const summarizeModel = createIssueSummarizeModel(ctx.
|
|
42
|
+
const summarizeModel = createIssueSummarizeModel(ctx.openRouterApiKey);
|
|
43
43
|
logger.info('Prepared summarize thread prompt', {
|
|
44
44
|
entryCount: entries.length,
|
|
45
45
|
chunked: threadPrompt.chunked,
|
|
@@ -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,7 +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
|
-
geminiApiKey: env.GEMINI_API_KEY?.trim() || undefined,
|
|
39
|
-
anthropicApiKey: env.ANTHROPIC_API_KEY?.trim() || undefined,
|
|
40
41
|
};
|
|
41
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
|
+
}
|