@doist/doistbot-cli 1.0.0 → 1.0.2
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 +135 -28
- package/dist/actions/doctor.js +12 -9
- package/dist/actions/review-usage.js +20 -0
- package/dist/actions/review.js +2 -1
- package/dist/actions/setup.js +2 -1
- package/dist/auth.js +30 -9
- package/dist/config.js +1 -1
- package/dist/env.js +1 -1
- package/dist/file-store.js +112 -0
- package/dist/index.js +76 -25
- package/dist/input.js +22 -48
- package/dist/secure-store.js +105 -16
- package/dist/terminal.js +4 -1
- package/dist/test-utils/secure-store-mocks.js +29 -0
- package/dist/update-cache-entrypoint.js +16 -0
- package/dist/update-check.js +235 -0
- package/package.json +10 -7
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +5 -2
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +34 -23
- package/sandbox/dist/core/pi.js +0 -4
- package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +1 -7
- package/sandbox/dist/tasks/issue-triage/fix-attempt.js +148 -100
- package/sandbox/dist/tasks/issue-triage/pr-creator.js +16 -9
- package/sandbox/dist/tasks/issue-triage/repo-config.js +28 -2
- package/sandbox/dist/tasks/issue-triage/triage.js +31 -30
- package/sandbox/dist/tasks/review/check-run.js +134 -14
- package/sandbox/dist/tasks/review/conversation-context.js +316 -0
- package/sandbox/dist/tasks/review/engines/dedupe.js +82 -63
- package/sandbox/dist/tasks/review/local-git.js +7 -0
- package/sandbox/dist/tasks/review/multi-focus-prompt.js +2 -1
- package/sandbox/dist/tasks/review/prompt.js +15 -2
- package/sandbox/dist/tasks/review/review-feedback.js +4 -0
- package/sandbox/dist/tasks/review/review.js +39 -1
- package/sandbox/dist/tasks/review/runner.js +5 -1
- package/sandbox/dist/tasks/review/severity.js +5 -0
- package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +5 -2
- package/sandbox/node_modules/doistbot-repo-config/dist/index.js +34 -23
- package/sandbox/review-prompt.md +8 -2
- package/sandbox/src/tasks/review/prompts/review-multi-focus-base-prompt.md +7 -2
package/sandbox/dist/core/pi.js
CHANGED
|
@@ -6,25 +6,21 @@ import { fileURLToPath } from 'url';
|
|
|
6
6
|
const PI_PROVIDER_BY_DOISTBOT_PROVIDER = {
|
|
7
7
|
gemini: 'google',
|
|
8
8
|
openai: 'openai',
|
|
9
|
-
'openai-codex': 'openai-codex',
|
|
10
9
|
anthropic: 'anthropic',
|
|
11
10
|
};
|
|
12
11
|
const PI_API_KEY_ENV_VAR_BY_PROVIDER = {
|
|
13
12
|
gemini: 'GEMINI_API_KEY',
|
|
14
13
|
openai: 'OPENAI_API_KEY',
|
|
15
|
-
'openai-codex': 'OPENAI_API_KEY',
|
|
16
14
|
anthropic: 'ANTHROPIC_API_KEY',
|
|
17
15
|
};
|
|
18
16
|
export const DEFAULT_PI_MODELS = {
|
|
19
17
|
gemini: 'gemini-3.1-pro-preview',
|
|
20
18
|
openai: 'gpt-5.4',
|
|
21
|
-
'openai-codex': 'gpt-5.3-codex-spark',
|
|
22
19
|
anthropic: 'claude-opus-4-6',
|
|
23
20
|
};
|
|
24
21
|
export const MAX_PI_THINKING_BY_PROVIDER = {
|
|
25
22
|
gemini: 'high',
|
|
26
23
|
openai: 'xhigh',
|
|
27
|
-
'openai-codex': 'medium',
|
|
28
24
|
anthropic: 'xhigh',
|
|
29
25
|
};
|
|
30
26
|
const PI_PROVIDER_IDS = new Set(['google', 'openai', 'openai-codex', 'anthropic']);
|
|
@@ -161,13 +161,7 @@ async function main() {
|
|
|
161
161
|
octokit: writeOctokit,
|
|
162
162
|
triageOutput,
|
|
163
163
|
fixAssessment,
|
|
164
|
-
clonedRepos: clonedRepositories
|
|
165
|
-
owner: repository.owner,
|
|
166
|
-
repo: repository.repo,
|
|
167
|
-
fullName: repository.fullName,
|
|
168
|
-
localPath,
|
|
169
|
-
repository,
|
|
170
|
-
})),
|
|
164
|
+
clonedRepos: clonedRepositories,
|
|
171
165
|
issueTitle: issue.title ?? 'Untitled issue',
|
|
172
166
|
issueUrl: issue.html_url ?? getIssueUrl(ctx.owner, ctx.repo, ctx.issueNumber),
|
|
173
167
|
ghToken: issueInstallationAuth.token,
|
|
@@ -5,7 +5,7 @@ import { validateDiff } from './diff-validator.js';
|
|
|
5
5
|
import { CX_HERO_MANUAL_HANDLE, resolveHeroGroup } from './hero-group-map.js';
|
|
6
6
|
import { HERO_AUTOFIX_MARKER } from './markers.js';
|
|
7
7
|
import { invokeTriageModel } from './model.js';
|
|
8
|
-
import {
|
|
8
|
+
import { commitAndPushFix, findPRTemplate, openDraftFixPR } from './pr-creator.js';
|
|
9
9
|
const FIX_PROMPT_FILE = 'issue-triage-fix-prompt.md';
|
|
10
10
|
// Maximum time allowed for the Codex fix-generation invocation before we abort
|
|
11
11
|
const AUTO_FIX_CODEX_TIMEOUT_MS = 45 * 60 * 1000;
|
|
@@ -80,7 +80,7 @@ export function parsePRBodyFromOutput(output) {
|
|
|
80
80
|
function buildFixPrompt({ ctx, triageOutput, fixAssessment, issueTitle, issueUrl, targetRepo, prTemplate, }) {
|
|
81
81
|
const template = loadFixPromptTemplate();
|
|
82
82
|
return fillPromptTemplate(template, {
|
|
83
|
-
REPO_FULL: targetRepo.fullName,
|
|
83
|
+
REPO_FULL: targetRepo.repository.fullName,
|
|
84
84
|
ISSUE_NUMBER: String(ctx.issueNumber),
|
|
85
85
|
ISSUE_TITLE: issueTitle,
|
|
86
86
|
ISSUE_URL: issueUrl,
|
|
@@ -91,6 +91,31 @@ function buildFixPrompt({ ctx, triageOutput, fixAssessment, issueTitle, issueUrl
|
|
|
91
91
|
PR_TEMPLATE_SECTION: prTemplate ? buildPRTemplateSection(prTemplate) : '',
|
|
92
92
|
});
|
|
93
93
|
}
|
|
94
|
+
function formatPreviousFinding(finding) {
|
|
95
|
+
const tag = finding.severity ? `[${finding.severity}] ` : '';
|
|
96
|
+
const body = finding.body.trim().replace(/\s+/g, ' ');
|
|
97
|
+
return `- ${tag}${finding.path}:${finding.line}: ${body}`;
|
|
98
|
+
}
|
|
99
|
+
export function buildPreviousFindingsSection(findings) {
|
|
100
|
+
return [
|
|
101
|
+
'# Previous Review Findings',
|
|
102
|
+
'',
|
|
103
|
+
'A prior fix attempt for this issue was reviewed and the following issues were',
|
|
104
|
+
'flagged. Address each finding in your new fix attempt before producing the diff:',
|
|
105
|
+
'',
|
|
106
|
+
...findings.map(formatPreviousFinding),
|
|
107
|
+
'',
|
|
108
|
+
'---',
|
|
109
|
+
'',
|
|
110
|
+
].join('\n');
|
|
111
|
+
}
|
|
112
|
+
export function buildFixPromptWithFindings(input) {
|
|
113
|
+
const basePrompt = buildFixPrompt(input);
|
|
114
|
+
if (!input.previousFindings || input.previousFindings.length === 0) {
|
|
115
|
+
return basePrompt;
|
|
116
|
+
}
|
|
117
|
+
return buildPreviousFindingsSection(input.previousFindings) + basePrompt;
|
|
118
|
+
}
|
|
94
119
|
function captureGitDiff(repoLocalPath) {
|
|
95
120
|
try {
|
|
96
121
|
// Stage all changes (including new untracked files) so they appear in the diff
|
|
@@ -166,55 +191,13 @@ async function postFollowUpComment({ octokit, ctx, prUrl, prNumber, targetRepo,
|
|
|
166
191
|
heroGroup: heroGroup?.key ?? null,
|
|
167
192
|
});
|
|
168
193
|
}
|
|
169
|
-
export async function
|
|
170
|
-
const { ctx,
|
|
171
|
-
const startedAt = Date.now();
|
|
172
|
-
const fallbackRepository = fixAssessment.targetRepo ??
|
|
173
|
-
(clonedRepos.length === 1 ? clonedRepos[0].fullName : `${ctx.owner}/${ctx.repo}`);
|
|
174
|
-
if (!force && !shouldAttemptFix(fixAssessment)) {
|
|
175
|
-
logger.info('Fix attempt skipped — assessment does not qualify', {
|
|
176
|
-
task: 'issue-triage',
|
|
177
|
-
issueNumber: ctx.issueNumber,
|
|
178
|
-
classification: fixAssessment.classification,
|
|
179
|
-
confidence: fixAssessment.confidence,
|
|
180
|
-
});
|
|
181
|
-
return {
|
|
182
|
-
attempted: false,
|
|
183
|
-
repository: fallbackRepository,
|
|
184
|
-
reason: `Assessment: ${fixAssessment.classification} (confidence: ${fixAssessment.confidence})`,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
if (clonedRepos.length === 0) {
|
|
188
|
-
return {
|
|
189
|
-
attempted: false,
|
|
190
|
-
repository: fallbackRepository,
|
|
191
|
-
reason: 'No cloned repositories available for fix attempt.',
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
const matched = fixAssessment.targetRepo
|
|
195
|
-
? clonedRepos.find((r) => r.fullName === fixAssessment.targetRepo)
|
|
196
|
-
: clonedRepos.length === 1
|
|
197
|
-
? clonedRepos[0]
|
|
198
|
-
: undefined;
|
|
199
|
-
if (!matched) {
|
|
200
|
-
return {
|
|
201
|
-
attempted: false,
|
|
202
|
-
repository: fallbackRepository,
|
|
203
|
-
reason: `Unable to resolve a unique target repository from assessment: ${fixAssessment.targetRepo ?? 'missing targetRepo'}`,
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
const targetRepo = matched;
|
|
207
|
-
logger.info('Fix attempt starting', {
|
|
208
|
-
task: 'issue-triage',
|
|
209
|
-
issueNumber: ctx.issueNumber,
|
|
210
|
-
targetRepo: targetRepo.fullName,
|
|
211
|
-
trigger: ctx.triggerType,
|
|
212
|
-
});
|
|
194
|
+
export async function runFixCore(input) {
|
|
195
|
+
const { ctx, triageOutput, fixAssessment, targetRepo, resetTo = 'HEAD' } = input;
|
|
213
196
|
// Reset repo to clean state before fix attempt (triage Codex may have modified files).
|
|
214
197
|
// Use `git reset --hard` to clear both the working tree and the index, since
|
|
215
198
|
// captureGitDiff stages files with `git add -A`.
|
|
216
199
|
try {
|
|
217
|
-
execFileSync('git', ['reset', '--hard',
|
|
200
|
+
execFileSync('git', ['reset', '--hard', resetTo], {
|
|
218
201
|
cwd: targetRepo.localPath,
|
|
219
202
|
encoding: 'utf8',
|
|
220
203
|
stdio: 'pipe',
|
|
@@ -226,24 +209,29 @@ export async function attemptFix(input) {
|
|
|
226
209
|
});
|
|
227
210
|
}
|
|
228
211
|
catch (error) {
|
|
212
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
229
213
|
logger.warn('Failed to reset repo state before fix attempt', {
|
|
230
214
|
task: 'issue-triage',
|
|
231
215
|
issueNumber: ctx.issueNumber,
|
|
232
|
-
targetRepo: targetRepo.fullName,
|
|
233
|
-
error:
|
|
216
|
+
targetRepo: targetRepo.repository.fullName,
|
|
217
|
+
error: reason,
|
|
234
218
|
});
|
|
219
|
+
return {
|
|
220
|
+
ok: false,
|
|
221
|
+
reason: `Failed to reset repository state before fix attempt: ${reason}`,
|
|
222
|
+
};
|
|
235
223
|
}
|
|
236
224
|
const prTemplate = findPRTemplate(targetRepo.localPath);
|
|
237
225
|
if (prTemplate) {
|
|
238
226
|
logger.info('PR template found for fix attempt', {
|
|
239
227
|
task: 'issue-triage',
|
|
240
228
|
issueNumber: ctx.issueNumber,
|
|
241
|
-
targetRepo: targetRepo.fullName,
|
|
229
|
+
targetRepo: targetRepo.repository.fullName,
|
|
242
230
|
templateChars: prTemplate.length,
|
|
243
231
|
});
|
|
244
232
|
}
|
|
245
233
|
// Run Codex to generate the fix
|
|
246
|
-
const prompt =
|
|
234
|
+
const prompt = buildFixPromptWithFindings({
|
|
247
235
|
ctx,
|
|
248
236
|
triageOutput,
|
|
249
237
|
fixAssessment,
|
|
@@ -251,6 +239,7 @@ export async function attemptFix(input) {
|
|
|
251
239
|
issueUrl: input.issueUrl,
|
|
252
240
|
targetRepo,
|
|
253
241
|
prTemplate,
|
|
242
|
+
previousFindings: input.previousFindings,
|
|
254
243
|
});
|
|
255
244
|
const timeoutMs = fixAssessment.classification === 'speculative'
|
|
256
245
|
? SPECULATIVE_CODEX_TIMEOUT_MS
|
|
@@ -265,19 +254,14 @@ export async function attemptFix(input) {
|
|
|
265
254
|
},
|
|
266
255
|
});
|
|
267
256
|
if (!codexResult.ok) {
|
|
268
|
-
logger.warn('Fix
|
|
257
|
+
logger.warn('Fix attempt invocation failed', {
|
|
269
258
|
task: 'issue-triage',
|
|
270
259
|
issueNumber: ctx.issueNumber,
|
|
271
260
|
error: codexResult.error,
|
|
272
261
|
timedOut: codexResult.timedOut,
|
|
273
262
|
durationMs: codexResult.durationMs,
|
|
274
263
|
});
|
|
275
|
-
return {
|
|
276
|
-
attempted: true,
|
|
277
|
-
repository: targetRepo.fullName,
|
|
278
|
-
prCreated: false,
|
|
279
|
-
reason: `Codex fix failed: ${codexResult.error}`,
|
|
280
|
-
};
|
|
264
|
+
return { ok: false, reason: `Fix attempt failed: ${codexResult.error}` };
|
|
281
265
|
}
|
|
282
266
|
const fixDescription = codexResult.output;
|
|
283
267
|
logger.info('Fix Codex invocation completed', {
|
|
@@ -293,12 +277,7 @@ export async function attemptFix(input) {
|
|
|
293
277
|
issueNumber: ctx.issueNumber,
|
|
294
278
|
output: fixDescription.slice(0, 500),
|
|
295
279
|
});
|
|
296
|
-
return {
|
|
297
|
-
attempted: true,
|
|
298
|
-
repository: targetRepo.fullName,
|
|
299
|
-
prCreated: false,
|
|
300
|
-
reason: 'Codex determined fix cannot be applied automatically.',
|
|
301
|
-
};
|
|
280
|
+
return { ok: false, reason: 'Codex determined fix cannot be applied automatically.' };
|
|
302
281
|
}
|
|
303
282
|
// Capture the diff
|
|
304
283
|
const diff = captureGitDiff(targetRepo.localPath);
|
|
@@ -307,12 +286,7 @@ export async function attemptFix(input) {
|
|
|
307
286
|
task: 'issue-triage',
|
|
308
287
|
issueNumber: ctx.issueNumber,
|
|
309
288
|
});
|
|
310
|
-
return {
|
|
311
|
-
attempted: true,
|
|
312
|
-
repository: targetRepo.fullName,
|
|
313
|
-
prCreated: false,
|
|
314
|
-
reason: 'Codex produced no file changes.',
|
|
315
|
-
};
|
|
289
|
+
return { ok: false, reason: 'Codex produced no file changes.' };
|
|
316
290
|
}
|
|
317
291
|
// Validate the diff — speculative fixes bypass the file-count limit since they
|
|
318
292
|
// are draft PRs intended as starting points that may touch more files.
|
|
@@ -324,33 +298,14 @@ export async function attemptFix(input) {
|
|
|
324
298
|
issueNumber: ctx.issueNumber,
|
|
325
299
|
reason: validation.reason,
|
|
326
300
|
});
|
|
327
|
-
return {
|
|
328
|
-
attempted: true,
|
|
329
|
-
repository: targetRepo.fullName,
|
|
330
|
-
prCreated: false,
|
|
331
|
-
reason: `Diff validation failed: ${validation.reason}`,
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
if (ctx.dryRun) {
|
|
335
|
-
logger.info('Fix attempt dry run — would create PR', {
|
|
336
|
-
task: 'issue-triage',
|
|
337
|
-
issueNumber: ctx.issueNumber,
|
|
338
|
-
targetRepo: targetRepo.fullName,
|
|
339
|
-
diffLines: diff.split('\n').length,
|
|
340
|
-
});
|
|
341
|
-
return {
|
|
342
|
-
attempted: true,
|
|
343
|
-
repository: targetRepo.fullName,
|
|
344
|
-
prCreated: false,
|
|
345
|
-
reason: 'Dry run — PR not created.',
|
|
346
|
-
};
|
|
301
|
+
return { ok: false, reason: `Diff validation failed: ${validation.reason}` };
|
|
347
302
|
}
|
|
348
303
|
const generatedPRTitle = parsePRTitleFromOutput(fixDescription);
|
|
349
304
|
if (generatedPRTitle) {
|
|
350
305
|
logger.info('Parsed PR title from Codex output', {
|
|
351
306
|
task: 'issue-triage',
|
|
352
307
|
issueNumber: ctx.issueNumber,
|
|
353
|
-
|
|
308
|
+
generatedPRTitle,
|
|
354
309
|
});
|
|
355
310
|
}
|
|
356
311
|
const generatedPRBody = parsePRBodyFromOutput(fixDescription);
|
|
@@ -361,20 +316,113 @@ export async function attemptFix(input) {
|
|
|
361
316
|
prBodyChars: generatedPRBody.length,
|
|
362
317
|
});
|
|
363
318
|
}
|
|
319
|
+
return {
|
|
320
|
+
ok: true,
|
|
321
|
+
codexOutput: fixDescription,
|
|
322
|
+
diff,
|
|
323
|
+
generatedPRTitle,
|
|
324
|
+
generatedPRBody,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
export async function attemptFix(input) {
|
|
328
|
+
const { ctx, octokit, triageOutput, fixAssessment, clonedRepos, force } = input;
|
|
329
|
+
const startedAt = Date.now();
|
|
330
|
+
const fallbackRepository = fixAssessment.targetRepo ??
|
|
331
|
+
(clonedRepos.length === 1
|
|
332
|
+
? clonedRepos[0].repository.fullName
|
|
333
|
+
: `${ctx.owner}/${ctx.repo}`);
|
|
334
|
+
if (!force && !shouldAttemptFix(fixAssessment)) {
|
|
335
|
+
logger.info('Fix attempt skipped — assessment does not qualify', {
|
|
336
|
+
task: 'issue-triage',
|
|
337
|
+
issueNumber: ctx.issueNumber,
|
|
338
|
+
classification: fixAssessment.classification,
|
|
339
|
+
confidence: fixAssessment.confidence,
|
|
340
|
+
});
|
|
341
|
+
return {
|
|
342
|
+
attempted: false,
|
|
343
|
+
repository: fallbackRepository,
|
|
344
|
+
reason: `Assessment: ${fixAssessment.classification} (confidence: ${fixAssessment.confidence})`,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
if (clonedRepos.length === 0) {
|
|
348
|
+
return {
|
|
349
|
+
attempted: false,
|
|
350
|
+
repository: fallbackRepository,
|
|
351
|
+
reason: 'No cloned repositories available for fix attempt.',
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
const matched = fixAssessment.targetRepo
|
|
355
|
+
? clonedRepos.find((r) => r.repository.fullName === fixAssessment.targetRepo)
|
|
356
|
+
: clonedRepos.length === 1
|
|
357
|
+
? clonedRepos[0]
|
|
358
|
+
: undefined;
|
|
359
|
+
if (!matched) {
|
|
360
|
+
return {
|
|
361
|
+
attempted: false,
|
|
362
|
+
repository: fallbackRepository,
|
|
363
|
+
reason: `Unable to resolve a unique target repository from assessment: ${fixAssessment.targetRepo ?? 'missing targetRepo'}`,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
const targetRepo = matched;
|
|
367
|
+
logger.info('Fix attempt starting', {
|
|
368
|
+
task: 'issue-triage',
|
|
369
|
+
issueNumber: ctx.issueNumber,
|
|
370
|
+
targetRepo: targetRepo.repository.fullName,
|
|
371
|
+
trigger: ctx.triggerType,
|
|
372
|
+
});
|
|
373
|
+
const coreResult = await runFixCore({
|
|
374
|
+
ctx,
|
|
375
|
+
triageOutput,
|
|
376
|
+
fixAssessment,
|
|
377
|
+
targetRepo,
|
|
378
|
+
issueTitle: input.issueTitle,
|
|
379
|
+
issueUrl: input.issueUrl,
|
|
380
|
+
ghToken: input.ghToken,
|
|
381
|
+
});
|
|
382
|
+
if (!coreResult.ok) {
|
|
383
|
+
return {
|
|
384
|
+
attempted: true,
|
|
385
|
+
repository: targetRepo.repository.fullName,
|
|
386
|
+
prCreated: false,
|
|
387
|
+
reason: coreResult.reason,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
if (ctx.dryRun) {
|
|
391
|
+
logger.info('Fix attempt dry run — would create PR', {
|
|
392
|
+
task: 'issue-triage',
|
|
393
|
+
issueNumber: ctx.issueNumber,
|
|
394
|
+
targetRepo: targetRepo.repository.fullName,
|
|
395
|
+
diffLines: coreResult.diff.split('\n').length,
|
|
396
|
+
});
|
|
397
|
+
return {
|
|
398
|
+
attempted: true,
|
|
399
|
+
repository: targetRepo.repository.fullName,
|
|
400
|
+
prCreated: false,
|
|
401
|
+
reason: 'Dry run — PR not created.',
|
|
402
|
+
};
|
|
403
|
+
}
|
|
364
404
|
try {
|
|
365
|
-
const
|
|
366
|
-
repoOwner: targetRepo.owner,
|
|
367
|
-
repoName: targetRepo.repo,
|
|
405
|
+
const { branch } = await commitAndPushFix({
|
|
406
|
+
repoOwner: targetRepo.repository.owner,
|
|
407
|
+
repoName: targetRepo.repository.repo,
|
|
368
408
|
repoLocalPath: targetRepo.localPath,
|
|
369
409
|
githubToken: ctx.githubToken,
|
|
370
410
|
issueNumber: ctx.issueNumber,
|
|
371
411
|
issueTitle: input.issueTitle,
|
|
412
|
+
prTitle: coreResult.generatedPRTitle ?? undefined,
|
|
413
|
+
});
|
|
414
|
+
const prResult = await openDraftFixPR({
|
|
415
|
+
repoOwner: targetRepo.repository.owner,
|
|
416
|
+
repoName: targetRepo.repository.repo,
|
|
417
|
+
octokit,
|
|
418
|
+
issueNumber: ctx.issueNumber,
|
|
419
|
+
issueTitle: input.issueTitle,
|
|
372
420
|
issueUrl: input.issueUrl,
|
|
373
421
|
triageAnalysis: triageOutput,
|
|
374
|
-
fixDescription: stripPRTitleSection(
|
|
375
|
-
|
|
376
|
-
prTitle: generatedPRTitle ?? undefined,
|
|
377
|
-
prBody: generatedPRBody ?? undefined,
|
|
422
|
+
fixDescription: stripPRTitleSection(coreResult.codexOutput),
|
|
423
|
+
branch,
|
|
424
|
+
prTitle: coreResult.generatedPRTitle ?? undefined,
|
|
425
|
+
prBody: coreResult.generatedPRBody ?? undefined,
|
|
378
426
|
fixAssessment,
|
|
379
427
|
});
|
|
380
428
|
// Post follow-up comment on the issue
|
|
@@ -395,7 +443,7 @@ export async function attemptFix(input) {
|
|
|
395
443
|
});
|
|
396
444
|
return {
|
|
397
445
|
attempted: true,
|
|
398
|
-
repository: targetRepo.fullName,
|
|
446
|
+
repository: targetRepo.repository.fullName,
|
|
399
447
|
prCreated: true,
|
|
400
448
|
prUrl: prResult.prUrl,
|
|
401
449
|
prNumber: prResult.prNumber,
|
|
@@ -410,7 +458,7 @@ export async function attemptFix(input) {
|
|
|
410
458
|
});
|
|
411
459
|
return {
|
|
412
460
|
attempted: true,
|
|
413
|
-
repository: targetRepo.fullName,
|
|
461
|
+
repository: targetRepo.repository.fullName,
|
|
414
462
|
prCreated: false,
|
|
415
463
|
reason: `PR creation failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
416
464
|
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from 'child_process';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import { Octokit } from 'octokit';
|
|
5
4
|
import { logger } from '../../core/logging.js';
|
|
6
5
|
import { fillPromptTemplate, loadPromptTemplateFile } from '../../core/prompt-template.js';
|
|
7
6
|
const PR_TEMPLATE_PATHS = [
|
|
@@ -11,6 +10,7 @@ const PR_TEMPLATE_PATHS = [
|
|
|
11
10
|
'PULL_REQUEST_TEMPLATE.md',
|
|
12
11
|
'docs/pull_request_template.md',
|
|
13
12
|
];
|
|
13
|
+
const GIT_PUSH_TIMEOUT_MS = 2 * 60 * 1000;
|
|
14
14
|
export function findPRTemplate(repoLocalPath) {
|
|
15
15
|
for (const templatePath of PR_TEMPLATE_PATHS) {
|
|
16
16
|
const fullPath = path.join(repoLocalPath, templatePath);
|
|
@@ -81,6 +81,7 @@ function pushBranch({ repoLocalPath, branch, token, owner, repo, }) {
|
|
|
81
81
|
cwd: repoLocalPath,
|
|
82
82
|
encoding: 'utf8',
|
|
83
83
|
stdio: 'pipe',
|
|
84
|
+
timeout: GIT_PUSH_TIMEOUT_MS,
|
|
84
85
|
env: {
|
|
85
86
|
...process.env,
|
|
86
87
|
GIT_CONFIG_COUNT: '1',
|
|
@@ -194,7 +195,7 @@ function buildPRBody({ issueNumber, issueUrl, triageAnalysis, fixDescription, fi
|
|
|
194
195
|
ISSUE_NUMBER: String(issueNumber),
|
|
195
196
|
});
|
|
196
197
|
}
|
|
197
|
-
export async function
|
|
198
|
+
export async function commitAndPushFix(input) {
|
|
198
199
|
const branch = generateBranchName(input.issueNumber);
|
|
199
200
|
logger.info('Creating fix PR', {
|
|
200
201
|
task: 'issue-triage',
|
|
@@ -209,7 +210,6 @@ export async function createFixPR(input) {
|
|
|
209
210
|
issueTitle: input.issueTitle,
|
|
210
211
|
prTitle: input.prTitle,
|
|
211
212
|
});
|
|
212
|
-
const octokit = new Octokit({ auth: input.githubToken });
|
|
213
213
|
pushBranch({
|
|
214
214
|
repoLocalPath: input.repoLocalPath,
|
|
215
215
|
branch,
|
|
@@ -217,11 +217,19 @@ export async function createFixPR(input) {
|
|
|
217
217
|
owner: input.repoOwner,
|
|
218
218
|
repo: input.repoName,
|
|
219
219
|
});
|
|
220
|
+
const headSha = execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
221
|
+
cwd: input.repoLocalPath,
|
|
222
|
+
encoding: 'utf8',
|
|
223
|
+
stdio: 'pipe',
|
|
224
|
+
}).trim();
|
|
220
225
|
logger.info('Fix branch pushed, creating draft PR', {
|
|
221
226
|
task: 'issue-triage',
|
|
222
227
|
repository: `${input.repoOwner}/${input.repoName}`,
|
|
223
228
|
branch,
|
|
224
229
|
});
|
|
230
|
+
return { branch, headSha };
|
|
231
|
+
}
|
|
232
|
+
export async function openDraftFixPR(input) {
|
|
225
233
|
const prBody = input.prBody
|
|
226
234
|
? stripEmptySections(input.prBody)
|
|
227
235
|
: buildPRBody({
|
|
@@ -231,19 +239,19 @@ export async function createFixPR(input) {
|
|
|
231
239
|
fixDescription: input.fixDescription,
|
|
232
240
|
fixAssessment: input.fixAssessment,
|
|
233
241
|
});
|
|
234
|
-
const { data: repo } = await octokit.rest.repos.get({
|
|
242
|
+
const { data: repo } = await input.octokit.rest.repos.get({
|
|
235
243
|
owner: input.repoOwner,
|
|
236
244
|
repo: input.repoName,
|
|
237
245
|
});
|
|
238
246
|
const prTitle = input.prTitle
|
|
239
247
|
? `${input.prTitle} (Doist/Issues#${input.issueNumber})`
|
|
240
248
|
: `fix: ${input.issueTitle} (Doist/Issues#${input.issueNumber})`;
|
|
241
|
-
const { data: pr } = await octokit.rest.pulls.create({
|
|
249
|
+
const { data: pr } = await input.octokit.rest.pulls.create({
|
|
242
250
|
owner: input.repoOwner,
|
|
243
251
|
repo: input.repoName,
|
|
244
252
|
title: prTitle,
|
|
245
253
|
body: prBody,
|
|
246
|
-
head: branch,
|
|
254
|
+
head: input.branch,
|
|
247
255
|
base: repo.default_branch,
|
|
248
256
|
draft: true,
|
|
249
257
|
});
|
|
@@ -251,7 +259,7 @@ export async function createFixPR(input) {
|
|
|
251
259
|
const prLabel = input.fixAssessment?.classification === 'speculative'
|
|
252
260
|
? 'doistbot-speculative-fix'
|
|
253
261
|
: 'doistbot-auto-fix';
|
|
254
|
-
await octokit.rest.issues.addLabels({
|
|
262
|
+
await input.octokit.rest.issues.addLabels({
|
|
255
263
|
owner: input.repoOwner,
|
|
256
264
|
repo: input.repoName,
|
|
257
265
|
issue_number: pr.number,
|
|
@@ -271,11 +279,10 @@ export async function createFixPR(input) {
|
|
|
271
279
|
repository: `${input.repoOwner}/${input.repoName}`,
|
|
272
280
|
prNumber: pr.number,
|
|
273
281
|
prUrl: pr.html_url,
|
|
274
|
-
branch,
|
|
282
|
+
branch: input.branch,
|
|
275
283
|
});
|
|
276
284
|
return {
|
|
277
285
|
prUrl: pr.html_url,
|
|
278
286
|
prNumber: pr.number,
|
|
279
|
-
branch,
|
|
280
287
|
};
|
|
281
288
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { getRepoConfig } from 'doistbot-repo-config';
|
|
2
2
|
export function isAutoFixEnabled(config) {
|
|
3
|
-
return config?.issue_triage?.enable_auto_fix
|
|
3
|
+
return config?.issue_triage?.enable_auto_fix !== false;
|
|
4
4
|
}
|
|
5
5
|
export function isAutoTriageEnabled(config) {
|
|
6
|
-
return config?.issue_triage?.enable_auto_triage
|
|
6
|
+
return config?.issue_triage?.enable_auto_triage !== false;
|
|
7
7
|
}
|
|
8
8
|
export function createRepoConfigLoader({ octokit, logger, }) {
|
|
9
9
|
const cache = new Map();
|
|
@@ -22,3 +22,29 @@ export function createRepoConfigLoader({ octokit, logger, }) {
|
|
|
22
22
|
return config;
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Returns true only when every repository allows the feature. Treats a config
|
|
27
|
+
* load error as an opt-out (fail closed) so transient GitHub API failures
|
|
28
|
+
* cannot silently override an explicit `enable_*: false`.
|
|
29
|
+
*/
|
|
30
|
+
export async function allRepositoriesAllow({ repositories, loadRepoConfig, isEnabled, feature, logger, }) {
|
|
31
|
+
for (const repository of repositories) {
|
|
32
|
+
let config;
|
|
33
|
+
try {
|
|
34
|
+
config = await loadRepoConfig(repository);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
logger.warn('Failed to load repo config; failing closed', {
|
|
38
|
+
component: 'issue-triage',
|
|
39
|
+
feature,
|
|
40
|
+
repository: repository.fullName,
|
|
41
|
+
error: error instanceof Error ? error.message : String(error),
|
|
42
|
+
});
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (!isEnabled(config)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
@@ -12,7 +12,7 @@ import { attemptFix } from './fix-attempt.js';
|
|
|
12
12
|
import { invokeTriageModel } from './model.js';
|
|
13
13
|
import { buildTriageFailureMessage, buildUnresolvedLabelsOutput, ensureStructuredTriageOutput, parseFixAssessment, REQUIRED_HEADINGS, } from './output.js';
|
|
14
14
|
import { fillTemplate, loadPromptTemplate } from './prompt.js';
|
|
15
|
-
import { createRepoConfigLoader, isAutoFixEnabled, isAutoTriageEnabled } from './repo-config.js';
|
|
15
|
+
import { allRepositoriesAllow, createRepoConfigLoader, isAutoFixEnabled, isAutoTriageEnabled, } from './repo-config.js';
|
|
16
16
|
import { buildHeroRoutingComment, postHeroRoutingComment, shouldPostHeroRoutingComment, } from './routing.js';
|
|
17
17
|
import { cloneRepository, createGitHubClients, extractLabelNames, fetchIssue, fetchIssueComments, getIssueUrl, prepareWorkspaceReposDirectory, TRIAGE_MARKER, } from './shared.js';
|
|
18
18
|
import { deleteAutoTriagePlaceholder, postIssueComment } from './side-effects.js';
|
|
@@ -246,17 +246,16 @@ async function runIssueTriagePipeline(ctx, mode) {
|
|
|
246
246
|
logger,
|
|
247
247
|
});
|
|
248
248
|
if (ctx.triggerType === 'auto') {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
249
|
+
const enableAutoTriage = await allRepositoriesAllow({
|
|
250
|
+
repositories: resolution.repositories,
|
|
251
|
+
loadRepoConfig: getCachedRepoConfig,
|
|
252
|
+
isEnabled: isAutoTriageEnabled,
|
|
253
|
+
feature: 'auto_triage',
|
|
254
|
+
logger,
|
|
255
|
+
});
|
|
257
256
|
if (!enableAutoTriage) {
|
|
258
257
|
shouldRecordTriageMetric = false;
|
|
259
|
-
logger.info('Auto issue triage skipped:
|
|
258
|
+
logger.info('Auto issue triage skipped: a resolved repository disabled auto triage', {
|
|
260
259
|
task: 'issue-triage',
|
|
261
260
|
issueNumber: ctx.issueNumber,
|
|
262
261
|
resolvedRepos: resolution.repositories.map((repository) => repository.fullName),
|
|
@@ -264,14 +263,28 @@ async function runIssueTriagePipeline(ctx, mode) {
|
|
|
264
263
|
await deleteAutoTriagePlaceholder({
|
|
265
264
|
octokit: writeOctokit,
|
|
266
265
|
ctx,
|
|
267
|
-
reason: '
|
|
266
|
+
reason: 'auto_triage_disabled',
|
|
268
267
|
});
|
|
269
268
|
return;
|
|
270
269
|
}
|
|
271
270
|
}
|
|
271
|
+
let enableAutoFix = ctx.enableAutoFixOverride ?? true;
|
|
272
|
+
if (ctx.enableAutoFixOverride === undefined) {
|
|
273
|
+
enableAutoFix = await allRepositoriesAllow({
|
|
274
|
+
repositories: resolution.repositories,
|
|
275
|
+
loadRepoConfig: getCachedRepoConfig,
|
|
276
|
+
isEnabled: isAutoFixEnabled,
|
|
277
|
+
feature: 'auto_fix',
|
|
278
|
+
logger,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
272
281
|
const reposDir = prepareWorkspaceReposDirectory();
|
|
273
282
|
const clonedRepositories = [];
|
|
274
283
|
const failedRepositories = [];
|
|
284
|
+
logger.info('Issue triage clone phase started', {
|
|
285
|
+
task: 'issue-triage',
|
|
286
|
+
issue_number: ctx.issueNumber,
|
|
287
|
+
});
|
|
275
288
|
for (const repository of resolution.repositories) {
|
|
276
289
|
try {
|
|
277
290
|
const cloneResult = await cloneRepository({
|
|
@@ -319,16 +332,6 @@ async function runIssueTriagePipeline(ctx, mode) {
|
|
|
319
332
|
type: 'installation',
|
|
320
333
|
installationId: ctx.installationId,
|
|
321
334
|
}));
|
|
322
|
-
let enableAutoFix = ctx.enableAutoFixOverride ?? false;
|
|
323
|
-
if (ctx.enableAutoFixOverride === undefined) {
|
|
324
|
-
for (const { repository } of clonedRepositories) {
|
|
325
|
-
const config = await getCachedRepoConfig(repository);
|
|
326
|
-
if (isAutoFixEnabled(config)) {
|
|
327
|
-
enableAutoFix = true;
|
|
328
|
-
break;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
335
|
const prompt = buildPrompt({
|
|
333
336
|
ctx,
|
|
334
337
|
issue,
|
|
@@ -438,13 +441,7 @@ async function runIssueTriagePipeline(ctx, mode) {
|
|
|
438
441
|
octokit: writeOctokit,
|
|
439
442
|
triageOutput: normalized.output,
|
|
440
443
|
fixAssessment,
|
|
441
|
-
clonedRepos: clonedRepositories
|
|
442
|
-
owner: repository.owner,
|
|
443
|
-
repo: repository.repo,
|
|
444
|
-
fullName: repository.fullName,
|
|
445
|
-
localPath,
|
|
446
|
-
repository,
|
|
447
|
-
})),
|
|
444
|
+
clonedRepos: clonedRepositories,
|
|
448
445
|
issueTitle: issue.title ?? 'Untitled issue',
|
|
449
446
|
issueUrl: issue.html_url ?? getIssueUrl(ctx.owner, ctx.repo, ctx.issueNumber),
|
|
450
447
|
ghToken: issueInstallationAuth.token,
|
|
@@ -552,8 +549,12 @@ async function main() {
|
|
|
552
549
|
try {
|
|
553
550
|
await runIssueTriage(parseIssueTriageContext());
|
|
554
551
|
}
|
|
555
|
-
catch {
|
|
556
|
-
|
|
552
|
+
catch (error) {
|
|
553
|
+
logger.error('Issue triage task failed in main', {
|
|
554
|
+
task: 'issue-triage',
|
|
555
|
+
error: error instanceof Error ? error.message : String(error),
|
|
556
|
+
});
|
|
557
|
+
process.exitCode = 1;
|
|
557
558
|
}
|
|
558
559
|
}
|
|
559
560
|
if (process.env.VITEST !== 'true' && process.env.TASK_TYPE === 'issue-triage') {
|