@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.
Files changed (39) hide show
  1. package/dist/actions/auth.js +135 -28
  2. package/dist/actions/doctor.js +12 -9
  3. package/dist/actions/review-usage.js +20 -0
  4. package/dist/actions/review.js +2 -1
  5. package/dist/actions/setup.js +2 -1
  6. package/dist/auth.js +30 -9
  7. package/dist/config.js +1 -1
  8. package/dist/env.js +1 -1
  9. package/dist/file-store.js +112 -0
  10. package/dist/index.js +76 -25
  11. package/dist/input.js +22 -48
  12. package/dist/secure-store.js +105 -16
  13. package/dist/terminal.js +4 -1
  14. package/dist/test-utils/secure-store-mocks.js +29 -0
  15. package/dist/update-cache-entrypoint.js +16 -0
  16. package/dist/update-check.js +235 -0
  17. package/package.json +10 -7
  18. package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +5 -2
  19. package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +34 -23
  20. package/sandbox/dist/core/pi.js +0 -4
  21. package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +1 -7
  22. package/sandbox/dist/tasks/issue-triage/fix-attempt.js +148 -100
  23. package/sandbox/dist/tasks/issue-triage/pr-creator.js +16 -9
  24. package/sandbox/dist/tasks/issue-triage/repo-config.js +28 -2
  25. package/sandbox/dist/tasks/issue-triage/triage.js +31 -30
  26. package/sandbox/dist/tasks/review/check-run.js +134 -14
  27. package/sandbox/dist/tasks/review/conversation-context.js +316 -0
  28. package/sandbox/dist/tasks/review/engines/dedupe.js +82 -63
  29. package/sandbox/dist/tasks/review/local-git.js +7 -0
  30. package/sandbox/dist/tasks/review/multi-focus-prompt.js +2 -1
  31. package/sandbox/dist/tasks/review/prompt.js +15 -2
  32. package/sandbox/dist/tasks/review/review-feedback.js +4 -0
  33. package/sandbox/dist/tasks/review/review.js +39 -1
  34. package/sandbox/dist/tasks/review/runner.js +5 -1
  35. package/sandbox/dist/tasks/review/severity.js +5 -0
  36. package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +5 -2
  37. package/sandbox/node_modules/doistbot-repo-config/dist/index.js +34 -23
  38. package/sandbox/review-prompt.md +8 -2
  39. package/sandbox/src/tasks/review/prompts/review-multi-focus-base-prompt.md +7 -2
@@ -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.map(({ repository, localPath }) => ({
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 { createFixPR, findPRTemplate } from './pr-creator.js';
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 attemptFix(input) {
170
- const { ctx, octokit, triageOutput, fixAssessment, clonedRepos, force } = input;
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', 'HEAD'], {
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: error instanceof Error ? error.message : String(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 = buildFixPrompt({
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 Codex invocation failed', {
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
- prTitleChars: generatedPRTitle.length,
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 prResult = await createFixPR({
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(fixDescription),
375
- diff,
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 createFixPR(input) {
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 === true;
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 === true;
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
- let enableAutoTriage = false;
250
- for (const repository of resolution.repositories) {
251
- const config = await getCachedRepoConfig(repository);
252
- if (isAutoTriageEnabled(config)) {
253
- enableAutoTriage = true;
254
- break;
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: no resolved repository enables auto triage', {
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: 'auto_triage_not_enabled',
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.map(({ repository, localPath }) => ({
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
- process.exit(1);
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') {