@doist/doistbot-cli 1.0.1 → 1.0.3
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/review-usage.js +20 -0
- package/dist/actions/review.js +126 -10
- package/dist/actions/setup.js +3 -2
- package/dist/index.js +18 -8
- package/dist/runtime.js +3 -3
- package/dist/update-check.js +1 -1
- package/package.json +6 -5
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +1 -0
- package/sandbox/dist/core/bot-logins.js +13 -0
- package/sandbox/dist/core/retry.js +29 -0
- package/sandbox/dist/core/shared.js +2 -2
- package/sandbox/dist/main.js +3 -0
- package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +1 -7
- package/sandbox/dist/tasks/issue-triage/fix-attempt.js +157 -28
- package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +190 -0
- package/sandbox/dist/tasks/issue-triage/fix-failure-reasons.js +2 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/effects.js +464 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/effects.types.js +1 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/fake-effects.js +65 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/promotion-task.js +110 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/runners.js +227 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/state-machine.js +165 -0
- package/sandbox/dist/tasks/issue-triage/hero-group-map.js +32 -0
- package/sandbox/dist/tasks/issue-triage/markers.js +2 -0
- package/sandbox/dist/tasks/issue-triage/output.js +24 -3
- package/sandbox/dist/tasks/issue-triage/pr-creator.js +72 -30
- package/sandbox/dist/tasks/issue-triage/repo-config.js +13 -0
- package/sandbox/dist/tasks/issue-triage/triage.js +31 -10
- package/sandbox/dist/tasks/issue-triage/types.js +1 -0
- package/sandbox/dist/tasks/review/author-format.js +14 -0
- package/sandbox/dist/tasks/review/author-profile.js +67 -0
- package/sandbox/dist/tasks/review/check-run.js +8 -1
- package/sandbox/dist/tasks/review/comment-mapper.js +17 -12
- package/sandbox/dist/tasks/review/conversation-context.js +174 -20
- package/sandbox/dist/tasks/review/council/summary.js +141 -12
- package/sandbox/dist/tasks/review/engines/council.js +19 -3
- package/sandbox/dist/tasks/review/engines/dedupe.js +37 -28
- package/sandbox/dist/tasks/review/engines/multi-focus.js +123 -6
- package/sandbox/dist/tasks/review/engines/shared.js +28 -5
- package/sandbox/dist/tasks/review/finding-output.js +18 -0
- package/sandbox/dist/tasks/review/github-comment-format.js +32 -0
- package/sandbox/dist/tasks/review/local.js +1 -0
- package/sandbox/dist/tasks/review/multi-focus-prompt.js +0 -1
- package/sandbox/dist/tasks/review/review.js +12 -2
- package/sandbox/dist/tasks/review/runner.js +36 -2
- package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +1 -0
- package/sandbox/review-prompt.md +18 -1
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/efficiency.md +10 -6
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/general.md +2 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/quality.md +15 -1
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/standards.md +2 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/tests.md +14 -1
- package/sandbox/src/tasks/review/prompts/review-multi-focus-base-prompt.md +31 -4
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/reuse.md +0 -13
|
@@ -2,8 +2,9 @@ import { execFileSync } from 'child_process';
|
|
|
2
2
|
import { logger } from '../../core/logging.js';
|
|
3
3
|
import { fillPromptTemplate, loadPromptTemplateFile } from '../../core/prompt-template.js';
|
|
4
4
|
import { validateDiff } from './diff-validator.js';
|
|
5
|
+
import { CODEX_CANNOT_FIX_REASON, FIX_LOOP_FAILED_REASON_PREFIX } from './fix-failure-reasons.js';
|
|
5
6
|
import { CX_HERO_MANUAL_HANDLE, resolveHeroGroup } from './hero-group-map.js';
|
|
6
|
-
import { HERO_AUTOFIX_MARKER } from './markers.js';
|
|
7
|
+
import { AUTOFIX_FAILURE_MARKER, FIX_LOOP_START_MARKER, HERO_AUTOFIX_MARKER } from './markers.js';
|
|
7
8
|
import { invokeTriageModel } from './model.js';
|
|
8
9
|
import { commitAndPushFix, findPRTemplate, openDraftFixPR } from './pr-creator.js';
|
|
9
10
|
const FIX_PROMPT_FILE = 'issue-triage-fix-prompt.md';
|
|
@@ -18,6 +19,9 @@ export function shouldAttemptFix(assessment) {
|
|
|
18
19
|
}
|
|
19
20
|
return assessment.confidence === 'high' || assessment.confidence === 'medium';
|
|
20
21
|
}
|
|
22
|
+
export function buildFixAssessmentSkipReason(assessment) {
|
|
23
|
+
return `Assessment: ${assessment.classification} (confidence: ${assessment.confidence})`;
|
|
24
|
+
}
|
|
21
25
|
function loadFixPromptTemplate() {
|
|
22
26
|
return loadPromptTemplateFile({
|
|
23
27
|
fileName: FIX_PROMPT_FILE,
|
|
@@ -80,7 +84,7 @@ export function parsePRBodyFromOutput(output) {
|
|
|
80
84
|
function buildFixPrompt({ ctx, triageOutput, fixAssessment, issueTitle, issueUrl, targetRepo, prTemplate, }) {
|
|
81
85
|
const template = loadFixPromptTemplate();
|
|
82
86
|
return fillPromptTemplate(template, {
|
|
83
|
-
REPO_FULL: targetRepo.fullName,
|
|
87
|
+
REPO_FULL: targetRepo.repository.fullName,
|
|
84
88
|
ISSUE_NUMBER: String(ctx.issueNumber),
|
|
85
89
|
ISSUE_TITLE: issueTitle,
|
|
86
90
|
ISSUE_URL: issueUrl,
|
|
@@ -160,6 +164,110 @@ export function buildFollowUpCommentBody({ prUrl, targetRepoFullName, fixAssessm
|
|
|
160
164
|
`The PR targets \`${targetRepoFullName}\`. Please review it carefully before merging.`,
|
|
161
165
|
].join('\n');
|
|
162
166
|
}
|
|
167
|
+
function isNonRetryableFailure(reason) {
|
|
168
|
+
return reason.includes(CODEX_CANNOT_FIX_REASON);
|
|
169
|
+
}
|
|
170
|
+
async function findSourceIssueCommentByMarker({ octokit, ctx, marker, }) {
|
|
171
|
+
const comments = await octokit.paginate(octokit.rest.issues.listComments, {
|
|
172
|
+
owner: ctx.owner,
|
|
173
|
+
repo: ctx.repo,
|
|
174
|
+
issue_number: ctx.issueNumber,
|
|
175
|
+
per_page: 100,
|
|
176
|
+
});
|
|
177
|
+
return comments.find((c) => typeof c.body === 'string' && c.body.includes(marker));
|
|
178
|
+
}
|
|
179
|
+
export async function shouldPostAutoFixFailureComment(fixResult, { dryRun, octokit, ctx }) {
|
|
180
|
+
if (dryRun)
|
|
181
|
+
return false;
|
|
182
|
+
if (!fixResult.attempted)
|
|
183
|
+
return false;
|
|
184
|
+
if (fixResult.prCreated)
|
|
185
|
+
return false;
|
|
186
|
+
// A PR opened during the loop is already announced via FIX_LOOP_START_MARKER;
|
|
187
|
+
// a "didn't produce a fix PR" comment would be misleading.
|
|
188
|
+
if (fixResult.prNumber !== undefined)
|
|
189
|
+
return false;
|
|
190
|
+
if (isNonRetryableFailure(fixResult.reason))
|
|
191
|
+
return false;
|
|
192
|
+
if (fixResult.reason.startsWith(FIX_LOOP_FAILED_REASON_PREFIX)) {
|
|
193
|
+
try {
|
|
194
|
+
const hasFixLoopStartComment = await sourceIssueHasFixLoopStartComment({ octokit, ctx });
|
|
195
|
+
if (hasFixLoopStartComment) {
|
|
196
|
+
logger.info('Auto-fix failure comment skipped', {
|
|
197
|
+
task: 'issue-triage',
|
|
198
|
+
issue_number: ctx.issueNumber,
|
|
199
|
+
reason: 'fix-loop start comment exists',
|
|
200
|
+
});
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
logger.warn('Failed to check fix-loop start comment', {
|
|
206
|
+
task: 'issue-triage',
|
|
207
|
+
issue_number: ctx.issueNumber,
|
|
208
|
+
error: error instanceof Error ? error.message : String(error),
|
|
209
|
+
});
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
export async function sourceIssueHasFixLoopStartComment({ octokit, ctx, }) {
|
|
216
|
+
const comment = await findSourceIssueCommentByMarker({
|
|
217
|
+
octokit,
|
|
218
|
+
ctx,
|
|
219
|
+
marker: FIX_LOOP_START_MARKER,
|
|
220
|
+
});
|
|
221
|
+
return comment !== undefined;
|
|
222
|
+
}
|
|
223
|
+
export function buildAutoFixFailureCommentBody({ reason, targetRepoFullName, }) {
|
|
224
|
+
const repoNote = targetRepoFullName ? ` in \`${targetRepoFullName}\`` : '';
|
|
225
|
+
return [
|
|
226
|
+
AUTOFIX_FAILURE_MARKER,
|
|
227
|
+
'',
|
|
228
|
+
'### Automated fix attempt failed',
|
|
229
|
+
'',
|
|
230
|
+
`I attempted an automated fix but couldn't open a fix PR${repoNote}.`,
|
|
231
|
+
'',
|
|
232
|
+
'**Reason:**',
|
|
233
|
+
'',
|
|
234
|
+
'```',
|
|
235
|
+
reason,
|
|
236
|
+
'```',
|
|
237
|
+
'',
|
|
238
|
+
'You can retry by commenting `@doistbot /fix`.',
|
|
239
|
+
].join('\n');
|
|
240
|
+
}
|
|
241
|
+
export async function postAutoFixFailureComment({ octokit, ctx, reason, targetRepoFullName, }) {
|
|
242
|
+
const body = buildAutoFixFailureCommentBody({ reason, targetRepoFullName });
|
|
243
|
+
const existing = await findSourceIssueCommentByMarker({
|
|
244
|
+
octokit,
|
|
245
|
+
ctx,
|
|
246
|
+
marker: AUTOFIX_FAILURE_MARKER,
|
|
247
|
+
});
|
|
248
|
+
if (existing) {
|
|
249
|
+
await octokit.rest.issues.updateComment({
|
|
250
|
+
owner: ctx.owner,
|
|
251
|
+
repo: ctx.repo,
|
|
252
|
+
comment_id: existing.id,
|
|
253
|
+
body,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
await octokit.rest.issues.createComment({
|
|
258
|
+
owner: ctx.owner,
|
|
259
|
+
repo: ctx.repo,
|
|
260
|
+
issue_number: ctx.issueNumber,
|
|
261
|
+
body,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
logger.info('Auto-fix failure comment posted', {
|
|
265
|
+
task: 'issue-triage',
|
|
266
|
+
issue_number: ctx.issueNumber,
|
|
267
|
+
target_repo: targetRepoFullName,
|
|
268
|
+
operation: existing ? 'update' : 'create',
|
|
269
|
+
});
|
|
270
|
+
}
|
|
163
271
|
async function postFollowUpComment({ octokit, ctx, prUrl, prNumber, targetRepo, fixAssessment, }) {
|
|
164
272
|
const heroGroup = resolveHeroGroup([], targetRepo);
|
|
165
273
|
if (!heroGroup) {
|
|
@@ -193,6 +301,18 @@ async function postFollowUpComment({ octokit, ctx, prUrl, prNumber, targetRepo,
|
|
|
193
301
|
}
|
|
194
302
|
export async function runFixCore(input) {
|
|
195
303
|
const { ctx, triageOutput, fixAssessment, targetRepo, resetTo = 'HEAD' } = input;
|
|
304
|
+
if (!input.force && !shouldAttemptFix(fixAssessment)) {
|
|
305
|
+
logger.info('Fix core skipped: assessment does not qualify', {
|
|
306
|
+
task: 'issue-triage',
|
|
307
|
+
issue_number: ctx.issueNumber,
|
|
308
|
+
classification: fixAssessment.classification,
|
|
309
|
+
confidence: fixAssessment.confidence,
|
|
310
|
+
});
|
|
311
|
+
return {
|
|
312
|
+
ok: false,
|
|
313
|
+
reason: buildFixAssessmentSkipReason(fixAssessment),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
196
316
|
// Reset repo to clean state before fix attempt (triage Codex may have modified files).
|
|
197
317
|
// Use `git reset --hard` to clear both the working tree and the index, since
|
|
198
318
|
// captureGitDiff stages files with `git add -A`.
|
|
@@ -213,7 +333,7 @@ export async function runFixCore(input) {
|
|
|
213
333
|
logger.warn('Failed to reset repo state before fix attempt', {
|
|
214
334
|
task: 'issue-triage',
|
|
215
335
|
issueNumber: ctx.issueNumber,
|
|
216
|
-
targetRepo: targetRepo.fullName,
|
|
336
|
+
targetRepo: targetRepo.repository.fullName,
|
|
217
337
|
error: reason,
|
|
218
338
|
});
|
|
219
339
|
return {
|
|
@@ -226,7 +346,7 @@ export async function runFixCore(input) {
|
|
|
226
346
|
logger.info('PR template found for fix attempt', {
|
|
227
347
|
task: 'issue-triage',
|
|
228
348
|
issueNumber: ctx.issueNumber,
|
|
229
|
-
targetRepo: targetRepo.fullName,
|
|
349
|
+
targetRepo: targetRepo.repository.fullName,
|
|
230
350
|
templateChars: prTemplate.length,
|
|
231
351
|
});
|
|
232
352
|
}
|
|
@@ -277,7 +397,7 @@ export async function runFixCore(input) {
|
|
|
277
397
|
issueNumber: ctx.issueNumber,
|
|
278
398
|
output: fixDescription.slice(0, 500),
|
|
279
399
|
});
|
|
280
|
-
return { ok: false, reason:
|
|
400
|
+
return { ok: false, reason: CODEX_CANNOT_FIX_REASON };
|
|
281
401
|
}
|
|
282
402
|
// Capture the diff
|
|
283
403
|
const diff = captureGitDiff(targetRepo.localPath);
|
|
@@ -328,7 +448,9 @@ export async function attemptFix(input) {
|
|
|
328
448
|
const { ctx, octokit, triageOutput, fixAssessment, clonedRepos, force } = input;
|
|
329
449
|
const startedAt = Date.now();
|
|
330
450
|
const fallbackRepository = fixAssessment.targetRepo ??
|
|
331
|
-
(clonedRepos.length === 1
|
|
451
|
+
(clonedRepos.length === 1
|
|
452
|
+
? clonedRepos[0].repository.fullName
|
|
453
|
+
: `${ctx.owner}/${ctx.repo}`);
|
|
332
454
|
if (!force && !shouldAttemptFix(fixAssessment)) {
|
|
333
455
|
logger.info('Fix attempt skipped — assessment does not qualify', {
|
|
334
456
|
task: 'issue-triage',
|
|
@@ -339,7 +461,7 @@ export async function attemptFix(input) {
|
|
|
339
461
|
return {
|
|
340
462
|
attempted: false,
|
|
341
463
|
repository: fallbackRepository,
|
|
342
|
-
reason:
|
|
464
|
+
reason: buildFixAssessmentSkipReason(fixAssessment),
|
|
343
465
|
};
|
|
344
466
|
}
|
|
345
467
|
if (clonedRepos.length === 0) {
|
|
@@ -350,7 +472,7 @@ export async function attemptFix(input) {
|
|
|
350
472
|
};
|
|
351
473
|
}
|
|
352
474
|
const matched = fixAssessment.targetRepo
|
|
353
|
-
? clonedRepos.find((r) => r.fullName === fixAssessment.targetRepo)
|
|
475
|
+
? clonedRepos.find((r) => r.repository.fullName === fixAssessment.targetRepo)
|
|
354
476
|
: clonedRepos.length === 1
|
|
355
477
|
? clonedRepos[0]
|
|
356
478
|
: undefined;
|
|
@@ -365,7 +487,7 @@ export async function attemptFix(input) {
|
|
|
365
487
|
logger.info('Fix attempt starting', {
|
|
366
488
|
task: 'issue-triage',
|
|
367
489
|
issueNumber: ctx.issueNumber,
|
|
368
|
-
targetRepo: targetRepo.fullName,
|
|
490
|
+
targetRepo: targetRepo.repository.fullName,
|
|
369
491
|
trigger: ctx.triggerType,
|
|
370
492
|
});
|
|
371
493
|
const coreResult = await runFixCore({
|
|
@@ -376,11 +498,12 @@ export async function attemptFix(input) {
|
|
|
376
498
|
issueTitle: input.issueTitle,
|
|
377
499
|
issueUrl: input.issueUrl,
|
|
378
500
|
ghToken: input.ghToken,
|
|
501
|
+
force,
|
|
379
502
|
});
|
|
380
503
|
if (!coreResult.ok) {
|
|
381
504
|
return {
|
|
382
505
|
attempted: true,
|
|
383
|
-
repository: targetRepo.fullName,
|
|
506
|
+
repository: targetRepo.repository.fullName,
|
|
384
507
|
prCreated: false,
|
|
385
508
|
reason: coreResult.reason,
|
|
386
509
|
};
|
|
@@ -389,29 +512,30 @@ export async function attemptFix(input) {
|
|
|
389
512
|
logger.info('Fix attempt dry run — would create PR', {
|
|
390
513
|
task: 'issue-triage',
|
|
391
514
|
issueNumber: ctx.issueNumber,
|
|
392
|
-
targetRepo: targetRepo.fullName,
|
|
515
|
+
targetRepo: targetRepo.repository.fullName,
|
|
393
516
|
diffLines: coreResult.diff.split('\n').length,
|
|
394
517
|
});
|
|
395
518
|
return {
|
|
396
519
|
attempted: true,
|
|
397
|
-
repository: targetRepo.fullName,
|
|
520
|
+
repository: targetRepo.repository.fullName,
|
|
398
521
|
prCreated: false,
|
|
399
522
|
reason: 'Dry run — PR not created.',
|
|
400
523
|
};
|
|
401
524
|
}
|
|
525
|
+
let openedPr;
|
|
402
526
|
try {
|
|
403
527
|
const { branch } = await commitAndPushFix({
|
|
404
|
-
repoOwner: targetRepo.owner,
|
|
405
|
-
repoName: targetRepo.repo,
|
|
528
|
+
repoOwner: targetRepo.repository.owner,
|
|
529
|
+
repoName: targetRepo.repository.repo,
|
|
406
530
|
repoLocalPath: targetRepo.localPath,
|
|
407
531
|
githubToken: ctx.githubToken,
|
|
408
532
|
issueNumber: ctx.issueNumber,
|
|
409
533
|
issueTitle: input.issueTitle,
|
|
410
534
|
prTitle: coreResult.generatedPRTitle ?? undefined,
|
|
411
535
|
});
|
|
412
|
-
|
|
413
|
-
repoOwner: targetRepo.owner,
|
|
414
|
-
repoName: targetRepo.repo,
|
|
536
|
+
openedPr = await openDraftFixPR({
|
|
537
|
+
repoOwner: targetRepo.repository.owner,
|
|
538
|
+
repoName: targetRepo.repository.repo,
|
|
415
539
|
octokit,
|
|
416
540
|
issueNumber: ctx.issueNumber,
|
|
417
541
|
issueTitle: input.issueTitle,
|
|
@@ -427,38 +551,43 @@ export async function attemptFix(input) {
|
|
|
427
551
|
await postFollowUpComment({
|
|
428
552
|
octokit,
|
|
429
553
|
ctx,
|
|
430
|
-
prUrl:
|
|
431
|
-
prNumber:
|
|
554
|
+
prUrl: openedPr.prUrl,
|
|
555
|
+
prNumber: openedPr.prNumber,
|
|
432
556
|
targetRepo: targetRepo.repository,
|
|
433
557
|
fixAssessment,
|
|
434
558
|
});
|
|
435
559
|
logger.info('Fix attempt completed successfully', {
|
|
436
560
|
task: 'issue-triage',
|
|
437
561
|
issueNumber: ctx.issueNumber,
|
|
438
|
-
prUrl:
|
|
439
|
-
prNumber:
|
|
562
|
+
prUrl: openedPr.prUrl,
|
|
563
|
+
prNumber: openedPr.prNumber,
|
|
440
564
|
durationMs: Date.now() - startedAt,
|
|
441
565
|
});
|
|
442
566
|
return {
|
|
443
567
|
attempted: true,
|
|
444
|
-
repository: targetRepo.fullName,
|
|
568
|
+
repository: targetRepo.repository.fullName,
|
|
445
569
|
prCreated: true,
|
|
446
|
-
prUrl:
|
|
447
|
-
prNumber:
|
|
570
|
+
prUrl: openedPr.prUrl,
|
|
571
|
+
prNumber: openedPr.prNumber,
|
|
448
572
|
};
|
|
449
573
|
}
|
|
450
574
|
catch (error) {
|
|
451
|
-
|
|
575
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
576
|
+
logger.error(openedPr ? 'Fix PR follow-up failed' : 'Fix PR creation failed', {
|
|
452
577
|
task: 'issue-triage',
|
|
453
578
|
issueNumber: ctx.issueNumber,
|
|
454
|
-
error:
|
|
579
|
+
error: reason,
|
|
580
|
+
...(openedPr ? { pr_number: openedPr.prNumber, pr_url: openedPr.prUrl } : {}),
|
|
455
581
|
durationMs: Date.now() - startedAt,
|
|
456
582
|
});
|
|
457
583
|
return {
|
|
458
584
|
attempted: true,
|
|
459
|
-
repository: targetRepo.fullName,
|
|
585
|
+
repository: targetRepo.repository.fullName,
|
|
460
586
|
prCreated: false,
|
|
461
|
-
reason:
|
|
587
|
+
reason: openedPr
|
|
588
|
+
? `PR follow-up failed after opening PR: ${reason}`
|
|
589
|
+
: `PR creation failed: ${reason}`,
|
|
590
|
+
...(openedPr ? { prNumber: openedPr.prNumber } : {}),
|
|
462
591
|
};
|
|
463
592
|
}
|
|
464
593
|
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { logger } from '../../core/logging.js';
|
|
2
|
+
import { DefaultLoopEffects } from './fix-loop/effects.js';
|
|
3
|
+
import { runFixLoop } from './fix-loop/runners.js';
|
|
4
|
+
import { attemptFix, buildFixAssessmentSkipReason, shouldAttemptFix } from './fix-attempt.js';
|
|
5
|
+
import { FIX_LOOP_FAILED_REASON_PREFIX } from './fix-failure-reasons.js';
|
|
6
|
+
import { getAutoFixMode } from './repo-config.js';
|
|
7
|
+
function fallbackRepository(input) {
|
|
8
|
+
return (input.fixAssessment.targetRepo ??
|
|
9
|
+
(input.clonedRepos.length === 1
|
|
10
|
+
? input.clonedRepos[0].repository.fullName
|
|
11
|
+
: `${input.ctx.owner}/${input.ctx.repo}`));
|
|
12
|
+
}
|
|
13
|
+
function resolveFixTarget(input) {
|
|
14
|
+
// TODO: Refactor attemptFix to accept a resolved target repo so dispatch and
|
|
15
|
+
// the legacy engine do not duplicate target-resolution logic.
|
|
16
|
+
if (input.clonedRepos.length === 0) {
|
|
17
|
+
return {
|
|
18
|
+
ok: false,
|
|
19
|
+
repository: fallbackRepository(input),
|
|
20
|
+
reason: 'No cloned repositories available for fix attempt.',
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (input.fixAssessment.targetRepo) {
|
|
24
|
+
const targetRepo = input.clonedRepos.find((repo) => repo.repository.fullName === input.fixAssessment.targetRepo);
|
|
25
|
+
if (targetRepo) {
|
|
26
|
+
return { ok: true, targetRepo };
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
ok: false,
|
|
30
|
+
repository: fallbackRepository(input),
|
|
31
|
+
reason: `Unable to resolve a unique target repository from assessment: ${input.fixAssessment.targetRepo}`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (input.clonedRepos.length === 1) {
|
|
35
|
+
return { ok: true, targetRepo: input.clonedRepos[0] };
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
repository: fallbackRepository(input),
|
|
40
|
+
reason: 'Unable to resolve a unique target repository from assessment: missing targetRepo',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function toAttemptFixInput(input) {
|
|
44
|
+
return {
|
|
45
|
+
ctx: input.ctx,
|
|
46
|
+
octokit: input.octokit,
|
|
47
|
+
triageOutput: input.triageOutput,
|
|
48
|
+
fixAssessment: input.fixAssessment,
|
|
49
|
+
clonedRepos: input.clonedRepos,
|
|
50
|
+
issueTitle: input.issueTitle,
|
|
51
|
+
issueUrl: input.issueUrl,
|
|
52
|
+
ghToken: input.ghToken,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function stateLogMeta(state) {
|
|
56
|
+
return {
|
|
57
|
+
kind: state.kind,
|
|
58
|
+
...(state.kind !== 'AWAITING_FIRST_FIX' && 'iteration' in state
|
|
59
|
+
? { iteration: state.iteration }
|
|
60
|
+
: {}),
|
|
61
|
+
...('branch' in state && state.branch !== undefined ? { branch: state.branch } : {}),
|
|
62
|
+
...('prNumber' in state && state.prNumber !== undefined
|
|
63
|
+
? { pr_number: state.prNumber }
|
|
64
|
+
: {}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function logDryRunTransition({ from, event, to }) {
|
|
68
|
+
logger.info('DRY RUN: fix loop state transition', {
|
|
69
|
+
task: 'fix-loop',
|
|
70
|
+
from: stateLogMeta(from),
|
|
71
|
+
event: event.kind,
|
|
72
|
+
to: stateLogMeta(to),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function errorReason(error) {
|
|
76
|
+
return error instanceof Error ? error.message : String(error);
|
|
77
|
+
}
|
|
78
|
+
function prUrlFor(targetRepo, prNumber) {
|
|
79
|
+
return `https://github.com/${targetRepo.repository.fullName}/pull/${prNumber}`;
|
|
80
|
+
}
|
|
81
|
+
export async function runConfiguredFix(input) {
|
|
82
|
+
const target = resolveFixTarget(input);
|
|
83
|
+
if (!target.ok) {
|
|
84
|
+
return { attempted: false, repository: target.repository, reason: target.reason };
|
|
85
|
+
}
|
|
86
|
+
const targetRepo = target.targetRepo;
|
|
87
|
+
let repoConfig;
|
|
88
|
+
try {
|
|
89
|
+
repoConfig = await input.loadRepoConfig(targetRepo.repository);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
logger.warn('Fix loop config load failed', {
|
|
93
|
+
task: 'fix-loop',
|
|
94
|
+
repository: targetRepo.repository.fullName,
|
|
95
|
+
error: errorReason(error),
|
|
96
|
+
});
|
|
97
|
+
return {
|
|
98
|
+
attempted: true,
|
|
99
|
+
repository: targetRepo.repository.fullName,
|
|
100
|
+
prCreated: false,
|
|
101
|
+
reason: `Fix loop config load failed: ${errorReason(error)}`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const autoFixMode = getAutoFixMode(repoConfig, input.ctx.enableAutoFixOverride);
|
|
105
|
+
if (autoFixMode === 'disabled') {
|
|
106
|
+
return {
|
|
107
|
+
attempted: false,
|
|
108
|
+
repository: targetRepo.repository.fullName,
|
|
109
|
+
reason: 'Auto fix disabled by repository config.',
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (autoFixMode === 'one-pass') {
|
|
113
|
+
return attemptFix(toAttemptFixInput(input));
|
|
114
|
+
}
|
|
115
|
+
if (!shouldAttemptFix(input.fixAssessment)) {
|
|
116
|
+
return {
|
|
117
|
+
attempted: false,
|
|
118
|
+
repository: targetRepo.repository.fullName,
|
|
119
|
+
reason: buildFixAssessmentSkipReason(input.fixAssessment),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const session = {
|
|
123
|
+
kind: 'fix-loop',
|
|
124
|
+
ctx: input.ctx,
|
|
125
|
+
octokit: input.octokit,
|
|
126
|
+
triageOutput: input.triageOutput,
|
|
127
|
+
fixAssessment: input.fixAssessment,
|
|
128
|
+
targetRepo,
|
|
129
|
+
issueTitle: input.issueTitle,
|
|
130
|
+
issueUrl: input.issueUrl,
|
|
131
|
+
ghToken: input.ghToken,
|
|
132
|
+
pushToken: input.ctx.githubToken,
|
|
133
|
+
apiKeys: {
|
|
134
|
+
openai: input.ctx.openaiApiKey,
|
|
135
|
+
...(input.ctx.geminiApiKey !== undefined && { gemini: input.ctx.geminiApiKey }),
|
|
136
|
+
...(input.ctx.anthropicApiKey !== undefined && {
|
|
137
|
+
anthropic: input.ctx.anthropicApiKey,
|
|
138
|
+
}),
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
let finalState;
|
|
142
|
+
try {
|
|
143
|
+
finalState = await runFixLoop({
|
|
144
|
+
effects: new DefaultLoopEffects(session, { dryRun: input.ctx.dryRun }),
|
|
145
|
+
...(input.ctx.dryRun ? { onTransition: logDryRunTransition } : {}),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
return {
|
|
150
|
+
attempted: true,
|
|
151
|
+
repository: targetRepo.repository.fullName,
|
|
152
|
+
prCreated: false,
|
|
153
|
+
reason: `${FIX_LOOP_FAILED_REASON_PREFIX} ${errorReason(error)}`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
if (finalState.kind === 'PUSHED_AWAITING_CI') {
|
|
157
|
+
if (input.ctx.dryRun) {
|
|
158
|
+
return {
|
|
159
|
+
attempted: true,
|
|
160
|
+
repository: targetRepo.repository.fullName,
|
|
161
|
+
prCreated: false,
|
|
162
|
+
reason: 'Dry run: PR not created.',
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const prNumber = finalState.prNumber;
|
|
166
|
+
const prUrl = prUrlFor(targetRepo, prNumber);
|
|
167
|
+
return {
|
|
168
|
+
attempted: true,
|
|
169
|
+
repository: targetRepo.repository.fullName,
|
|
170
|
+
prCreated: true,
|
|
171
|
+
prUrl,
|
|
172
|
+
prNumber,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const reason = finalState.kind === 'DONE_NEEDS_HUMAN'
|
|
176
|
+
? finalState.reason
|
|
177
|
+
: `Fix loop ended in ${finalState.kind}`;
|
|
178
|
+
return {
|
|
179
|
+
attempted: true,
|
|
180
|
+
repository: targetRepo.repository.fullName,
|
|
181
|
+
prCreated: false,
|
|
182
|
+
reason,
|
|
183
|
+
// If a PR was opened earlier in the loop (REVIEWING/ITERATING reached
|
|
184
|
+
// before DONE_NEEDS_HUMAN), surface it so callers can distinguish a
|
|
185
|
+
// silent failure from an in-progress PR.
|
|
186
|
+
...('prNumber' in finalState && finalState.prNumber !== undefined
|
|
187
|
+
? { prNumber: finalState.prNumber }
|
|
188
|
+
: {}),
|
|
189
|
+
};
|
|
190
|
+
}
|