@doist/doistbot-cli 1.0.0 → 1.0.1
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/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 +8 -6
- 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-triage/fix-attempt.js +136 -90
- 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 +30 -23
- 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
|
@@ -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;
|
|
@@ -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,12 +209,17 @@ 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
216
|
targetRepo: targetRepo.fullName,
|
|
233
|
-
error:
|
|
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) {
|
|
@@ -243,7 +231,7 @@ export async function attemptFix(input) {
|
|
|
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,11 +298,91 @@ export async function attemptFix(input) {
|
|
|
324
298
|
issueNumber: ctx.issueNumber,
|
|
325
299
|
reason: validation.reason,
|
|
326
300
|
});
|
|
301
|
+
return { ok: false, reason: `Diff validation failed: ${validation.reason}` };
|
|
302
|
+
}
|
|
303
|
+
const generatedPRTitle = parsePRTitleFromOutput(fixDescription);
|
|
304
|
+
if (generatedPRTitle) {
|
|
305
|
+
logger.info('Parsed PR title from Codex output', {
|
|
306
|
+
task: 'issue-triage',
|
|
307
|
+
issueNumber: ctx.issueNumber,
|
|
308
|
+
generatedPRTitle,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
const generatedPRBody = parsePRBodyFromOutput(fixDescription);
|
|
312
|
+
if (generatedPRBody) {
|
|
313
|
+
logger.info('Parsed PR body from Codex output', {
|
|
314
|
+
task: 'issue-triage',
|
|
315
|
+
issueNumber: ctx.issueNumber,
|
|
316
|
+
prBodyChars: generatedPRBody.length,
|
|
317
|
+
});
|
|
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 ? clonedRepos[0].fullName : `${ctx.owner}/${ctx.repo}`);
|
|
332
|
+
if (!force && !shouldAttemptFix(fixAssessment)) {
|
|
333
|
+
logger.info('Fix attempt skipped — assessment does not qualify', {
|
|
334
|
+
task: 'issue-triage',
|
|
335
|
+
issueNumber: ctx.issueNumber,
|
|
336
|
+
classification: fixAssessment.classification,
|
|
337
|
+
confidence: fixAssessment.confidence,
|
|
338
|
+
});
|
|
339
|
+
return {
|
|
340
|
+
attempted: false,
|
|
341
|
+
repository: fallbackRepository,
|
|
342
|
+
reason: `Assessment: ${fixAssessment.classification} (confidence: ${fixAssessment.confidence})`,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
if (clonedRepos.length === 0) {
|
|
346
|
+
return {
|
|
347
|
+
attempted: false,
|
|
348
|
+
repository: fallbackRepository,
|
|
349
|
+
reason: 'No cloned repositories available for fix attempt.',
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
const matched = fixAssessment.targetRepo
|
|
353
|
+
? clonedRepos.find((r) => r.fullName === fixAssessment.targetRepo)
|
|
354
|
+
: clonedRepos.length === 1
|
|
355
|
+
? clonedRepos[0]
|
|
356
|
+
: undefined;
|
|
357
|
+
if (!matched) {
|
|
358
|
+
return {
|
|
359
|
+
attempted: false,
|
|
360
|
+
repository: fallbackRepository,
|
|
361
|
+
reason: `Unable to resolve a unique target repository from assessment: ${fixAssessment.targetRepo ?? 'missing targetRepo'}`,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
const targetRepo = matched;
|
|
365
|
+
logger.info('Fix attempt starting', {
|
|
366
|
+
task: 'issue-triage',
|
|
367
|
+
issueNumber: ctx.issueNumber,
|
|
368
|
+
targetRepo: targetRepo.fullName,
|
|
369
|
+
trigger: ctx.triggerType,
|
|
370
|
+
});
|
|
371
|
+
const coreResult = await runFixCore({
|
|
372
|
+
ctx,
|
|
373
|
+
triageOutput,
|
|
374
|
+
fixAssessment,
|
|
375
|
+
targetRepo,
|
|
376
|
+
issueTitle: input.issueTitle,
|
|
377
|
+
issueUrl: input.issueUrl,
|
|
378
|
+
ghToken: input.ghToken,
|
|
379
|
+
});
|
|
380
|
+
if (!coreResult.ok) {
|
|
327
381
|
return {
|
|
328
382
|
attempted: true,
|
|
329
383
|
repository: targetRepo.fullName,
|
|
330
384
|
prCreated: false,
|
|
331
|
-
reason:
|
|
385
|
+
reason: coreResult.reason,
|
|
332
386
|
};
|
|
333
387
|
}
|
|
334
388
|
if (ctx.dryRun) {
|
|
@@ -336,7 +390,7 @@ export async function attemptFix(input) {
|
|
|
336
390
|
task: 'issue-triage',
|
|
337
391
|
issueNumber: ctx.issueNumber,
|
|
338
392
|
targetRepo: targetRepo.fullName,
|
|
339
|
-
diffLines: diff.split('\n').length,
|
|
393
|
+
diffLines: coreResult.diff.split('\n').length,
|
|
340
394
|
});
|
|
341
395
|
return {
|
|
342
396
|
attempted: true,
|
|
@@ -345,36 +399,28 @@ export async function attemptFix(input) {
|
|
|
345
399
|
reason: 'Dry run — PR not created.',
|
|
346
400
|
};
|
|
347
401
|
}
|
|
348
|
-
const generatedPRTitle = parsePRTitleFromOutput(fixDescription);
|
|
349
|
-
if (generatedPRTitle) {
|
|
350
|
-
logger.info('Parsed PR title from Codex output', {
|
|
351
|
-
task: 'issue-triage',
|
|
352
|
-
issueNumber: ctx.issueNumber,
|
|
353
|
-
prTitleChars: generatedPRTitle.length,
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
const generatedPRBody = parsePRBodyFromOutput(fixDescription);
|
|
357
|
-
if (generatedPRBody) {
|
|
358
|
-
logger.info('Parsed PR body from Codex output', {
|
|
359
|
-
task: 'issue-triage',
|
|
360
|
-
issueNumber: ctx.issueNumber,
|
|
361
|
-
prBodyChars: generatedPRBody.length,
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
402
|
try {
|
|
365
|
-
const
|
|
403
|
+
const { branch } = await commitAndPushFix({
|
|
366
404
|
repoOwner: targetRepo.owner,
|
|
367
405
|
repoName: targetRepo.repo,
|
|
368
406
|
repoLocalPath: targetRepo.localPath,
|
|
369
407
|
githubToken: ctx.githubToken,
|
|
370
408
|
issueNumber: ctx.issueNumber,
|
|
371
409
|
issueTitle: input.issueTitle,
|
|
410
|
+
prTitle: coreResult.generatedPRTitle ?? undefined,
|
|
411
|
+
});
|
|
412
|
+
const prResult = await openDraftFixPR({
|
|
413
|
+
repoOwner: targetRepo.owner,
|
|
414
|
+
repoName: targetRepo.repo,
|
|
415
|
+
octokit,
|
|
416
|
+
issueNumber: ctx.issueNumber,
|
|
417
|
+
issueTitle: input.issueTitle,
|
|
372
418
|
issueUrl: input.issueUrl,
|
|
373
419
|
triageAnalysis: triageOutput,
|
|
374
|
-
fixDescription: stripPRTitleSection(
|
|
375
|
-
|
|
376
|
-
prTitle: generatedPRTitle ?? undefined,
|
|
377
|
-
prBody: generatedPRBody ?? undefined,
|
|
420
|
+
fixDescription: stripPRTitleSection(coreResult.codexOutput),
|
|
421
|
+
branch,
|
|
422
|
+
prTitle: coreResult.generatedPRTitle ?? undefined,
|
|
423
|
+
prBody: coreResult.generatedPRBody ?? undefined,
|
|
378
424
|
fixAssessment,
|
|
379
425
|
});
|
|
380
426
|
// Post follow-up comment on the issue
|
|
@@ -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,
|
|
@@ -552,8 +555,12 @@ async function main() {
|
|
|
552
555
|
try {
|
|
553
556
|
await runIssueTriage(parseIssueTriageContext());
|
|
554
557
|
}
|
|
555
|
-
catch {
|
|
556
|
-
|
|
558
|
+
catch (error) {
|
|
559
|
+
logger.error('Issue triage task failed in main', {
|
|
560
|
+
task: 'issue-triage',
|
|
561
|
+
error: error instanceof Error ? error.message : String(error),
|
|
562
|
+
});
|
|
563
|
+
process.exitCode = 1;
|
|
557
564
|
}
|
|
558
565
|
}
|
|
559
566
|
if (process.env.VITEST !== 'true' && process.env.TASK_TYPE === 'issue-triage') {
|