@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.
Files changed (54) hide show
  1. package/dist/actions/review-usage.js +20 -0
  2. package/dist/actions/review.js +126 -10
  3. package/dist/actions/setup.js +3 -2
  4. package/dist/index.js +18 -8
  5. package/dist/runtime.js +3 -3
  6. package/dist/update-check.js +1 -1
  7. package/package.json +6 -5
  8. package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +1 -0
  9. package/sandbox/dist/core/bot-logins.js +13 -0
  10. package/sandbox/dist/core/retry.js +29 -0
  11. package/sandbox/dist/core/shared.js +2 -2
  12. package/sandbox/dist/main.js +3 -0
  13. package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +1 -7
  14. package/sandbox/dist/tasks/issue-triage/fix-attempt.js +157 -28
  15. package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +190 -0
  16. package/sandbox/dist/tasks/issue-triage/fix-failure-reasons.js +2 -0
  17. package/sandbox/dist/tasks/issue-triage/fix-loop/effects.js +464 -0
  18. package/sandbox/dist/tasks/issue-triage/fix-loop/effects.types.js +1 -0
  19. package/sandbox/dist/tasks/issue-triage/fix-loop/fake-effects.js +65 -0
  20. package/sandbox/dist/tasks/issue-triage/fix-loop/promotion-task.js +110 -0
  21. package/sandbox/dist/tasks/issue-triage/fix-loop/runners.js +227 -0
  22. package/sandbox/dist/tasks/issue-triage/fix-loop/state-machine.js +165 -0
  23. package/sandbox/dist/tasks/issue-triage/hero-group-map.js +32 -0
  24. package/sandbox/dist/tasks/issue-triage/markers.js +2 -0
  25. package/sandbox/dist/tasks/issue-triage/output.js +24 -3
  26. package/sandbox/dist/tasks/issue-triage/pr-creator.js +72 -30
  27. package/sandbox/dist/tasks/issue-triage/repo-config.js +13 -0
  28. package/sandbox/dist/tasks/issue-triage/triage.js +31 -10
  29. package/sandbox/dist/tasks/issue-triage/types.js +1 -0
  30. package/sandbox/dist/tasks/review/author-format.js +14 -0
  31. package/sandbox/dist/tasks/review/author-profile.js +67 -0
  32. package/sandbox/dist/tasks/review/check-run.js +8 -1
  33. package/sandbox/dist/tasks/review/comment-mapper.js +17 -12
  34. package/sandbox/dist/tasks/review/conversation-context.js +174 -20
  35. package/sandbox/dist/tasks/review/council/summary.js +141 -12
  36. package/sandbox/dist/tasks/review/engines/council.js +19 -3
  37. package/sandbox/dist/tasks/review/engines/dedupe.js +37 -28
  38. package/sandbox/dist/tasks/review/engines/multi-focus.js +123 -6
  39. package/sandbox/dist/tasks/review/engines/shared.js +28 -5
  40. package/sandbox/dist/tasks/review/finding-output.js +18 -0
  41. package/sandbox/dist/tasks/review/github-comment-format.js +32 -0
  42. package/sandbox/dist/tasks/review/local.js +1 -0
  43. package/sandbox/dist/tasks/review/multi-focus-prompt.js +0 -1
  44. package/sandbox/dist/tasks/review/review.js +12 -2
  45. package/sandbox/dist/tasks/review/runner.js +36 -2
  46. package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +1 -0
  47. package/sandbox/review-prompt.md +18 -1
  48. package/sandbox/src/tasks/review/prompts/review-focus-prompts/efficiency.md +10 -6
  49. package/sandbox/src/tasks/review/prompts/review-focus-prompts/general.md +2 -0
  50. package/sandbox/src/tasks/review/prompts/review-focus-prompts/quality.md +15 -1
  51. package/sandbox/src/tasks/review/prompts/review-focus-prompts/standards.md +2 -0
  52. package/sandbox/src/tasks/review/prompts/review-focus-prompts/tests.md +14 -1
  53. package/sandbox/src/tasks/review/prompts/review-multi-focus-base-prompt.md +31 -4
  54. 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: 'Codex determined fix cannot be applied automatically.' };
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 ? clonedRepos[0].fullName : `${ctx.owner}/${ctx.repo}`);
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: `Assessment: ${fixAssessment.classification} (confidence: ${fixAssessment.confidence})`,
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
- const prResult = await openDraftFixPR({
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: prResult.prUrl,
431
- prNumber: prResult.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: prResult.prUrl,
439
- prNumber: prResult.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: prResult.prUrl,
447
- prNumber: prResult.prNumber,
570
+ prUrl: openedPr.prUrl,
571
+ prNumber: openedPr.prNumber,
448
572
  };
449
573
  }
450
574
  catch (error) {
451
- logger.error('Fix PR creation failed', {
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: error instanceof Error ? error.message : String(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: `PR creation failed: ${error instanceof Error ? error.message : String(error)}`,
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
+ }
@@ -0,0 +1,2 @@
1
+ export const CODEX_CANNOT_FIX_REASON = 'Codex determined fix cannot be applied automatically.';
2
+ export const FIX_LOOP_FAILED_REASON_PREFIX = 'Fix loop failed:';