@todoist/doistbot-cli 0.0.0-stage → 1.0.16

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 (185) hide show
  1. package/dist/actions/auth.js +225 -0
  2. package/dist/actions/doctor.js +39 -0
  3. package/dist/actions/review-usage.js +25 -0
  4. package/dist/actions/review.js +311 -0
  5. package/dist/actions/setup.js +14 -0
  6. package/dist/auth.js +202 -0
  7. package/dist/config.js +145 -0
  8. package/dist/env.js +10 -0
  9. package/dist/file-store.js +112 -0
  10. package/dist/index.js +164 -0
  11. package/dist/input.js +40 -0
  12. package/dist/runtime.js +105 -0
  13. package/dist/secure-store.js +148 -0
  14. package/dist/terminal.js +185 -0
  15. package/dist/test-utils/secure-store-mocks.js +29 -0
  16. package/dist/update-cache-entrypoint.js +16 -0
  17. package/dist/update-check.js +235 -0
  18. package/package.json +65 -5
  19. package/sandbox/_node_modules/doistbot-logging/dist/index.d.ts +23 -0
  20. package/sandbox/_node_modules/doistbot-logging/dist/index.js +51 -0
  21. package/sandbox/_node_modules/doistbot-logging/package.json +12 -0
  22. package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +93 -0
  23. package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +197 -0
  24. package/sandbox/_node_modules/doistbot-repo-config/package.json +12 -0
  25. package/sandbox/dist/core/autofix-resources.js +58 -0
  26. package/sandbox/dist/core/autofix-service-query.js +69 -0
  27. package/sandbox/dist/core/autofix-test-policy.js +11 -0
  28. package/sandbox/dist/core/autofix-verification.js +355 -0
  29. package/sandbox/dist/core/bot-logins.js +14 -0
  30. package/sandbox/dist/core/check-run.js +158 -0
  31. package/sandbox/dist/core/datadog-metrics.js +113 -0
  32. package/sandbox/dist/core/environment.js +49 -0
  33. package/sandbox/dist/core/git-auth.js +17 -0
  34. package/sandbox/dist/core/github-history.js +4 -0
  35. package/sandbox/dist/core/github-issues.js +18 -0
  36. package/sandbox/dist/core/label-parsing.js +1 -0
  37. package/sandbox/dist/core/langfuse-observation.js +33 -0
  38. package/sandbox/dist/core/langfuse-session.js +39 -0
  39. package/sandbox/dist/core/logger-span-processor.js +90 -0
  40. package/sandbox/dist/core/logging.js +18 -0
  41. package/sandbox/dist/core/pi-autofix-extension.js +188 -0
  42. package/sandbox/dist/core/pi-execution-limits-extension.js +79 -0
  43. package/sandbox/dist/core/pi-progress-logging.js +51 -0
  44. package/sandbox/dist/core/pi-tool-output-filter-extension.js +65 -0
  45. package/sandbox/dist/core/pi.js +870 -0
  46. package/sandbox/dist/core/pii-scrubber.js +31 -0
  47. package/sandbox/dist/core/prompt-template.js +63 -0
  48. package/sandbox/dist/core/reactions.js +36 -0
  49. package/sandbox/dist/core/regexp.js +3 -0
  50. package/sandbox/dist/core/repository-map.js +1 -0
  51. package/sandbox/dist/core/repository-resolver.js +1 -0
  52. package/sandbox/dist/core/retry.js +29 -0
  53. package/sandbox/dist/core/s3-utils.js +42 -0
  54. package/sandbox/dist/core/shared.js +124 -0
  55. package/sandbox/dist/core/snapshot-helpers.js +17 -0
  56. package/sandbox/dist/core/span-test-helpers.js +13 -0
  57. package/sandbox/dist/core/tool-environment.js +83 -0
  58. package/sandbox/dist/core/tool-output.js +18 -0
  59. package/sandbox/dist/core/tracing-setup.js +92 -0
  60. package/sandbox/dist/core/tracing.js +81 -0
  61. package/sandbox/dist/main.js +51 -0
  62. package/sandbox/dist/providers/config.js +23 -0
  63. package/sandbox/dist/providers/credentials.js +40 -0
  64. package/sandbox/dist/providers/gemini.js +28 -0
  65. package/sandbox/dist/providers/helpers.js +253 -0
  66. package/sandbox/dist/providers/index.js +27 -0
  67. package/sandbox/dist/providers/openai.js +23 -0
  68. package/sandbox/dist/providers/openrouter-pi.js +25 -0
  69. package/sandbox/dist/providers/pi-review.js +301 -0
  70. package/sandbox/dist/providers/process.js +34 -0
  71. package/sandbox/dist/providers/schemas.js +61 -0
  72. package/sandbox/dist/providers/types.js +4 -0
  73. package/sandbox/dist/tasks/chat/chat.js +440 -0
  74. package/sandbox/dist/tasks/chat/prompt.js +20 -0
  75. package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +304 -0
  76. package/sandbox/dist/tasks/issue-summarize/context.js +21 -0
  77. package/sandbox/dist/tasks/issue-summarize/media.js +335 -0
  78. package/sandbox/dist/tasks/issue-summarize/model.js +230 -0
  79. package/sandbox/dist/tasks/issue-summarize/output.js +45 -0
  80. package/sandbox/dist/tasks/issue-summarize/prompt.js +11 -0
  81. package/sandbox/dist/tasks/issue-summarize/summarize.js +238 -0
  82. package/sandbox/dist/tasks/issue-summarize/thread.js +123 -0
  83. package/sandbox/dist/tasks/issue-summarize/types.js +1 -0
  84. package/sandbox/dist/tasks/issue-triage/assignee-review.js +114 -0
  85. package/sandbox/dist/tasks/issue-triage/autofix-capabilities.js +22 -0
  86. package/sandbox/dist/tasks/issue-triage/autofix-checkpoint.js +122 -0
  87. package/sandbox/dist/tasks/issue-triage/autofix-policy.js +36 -0
  88. package/sandbox/dist/tasks/issue-triage/clone.js +40 -0
  89. package/sandbox/dist/tasks/issue-triage/context.js +57 -0
  90. package/sandbox/dist/tasks/issue-triage/diff-validator.js +102 -0
  91. package/sandbox/dist/tasks/issue-triage/fix-attempt.js +617 -0
  92. package/sandbox/dist/tasks/issue-triage/fix-context.js +5 -0
  93. package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +263 -0
  94. package/sandbox/dist/tasks/issue-triage/fix-failure-reasons.js +2 -0
  95. package/sandbox/dist/tasks/issue-triage/fix-loop/effects.js +526 -0
  96. package/sandbox/dist/tasks/issue-triage/fix-loop/effects.types.js +1 -0
  97. package/sandbox/dist/tasks/issue-triage/fix-loop/fake-effects.js +65 -0
  98. package/sandbox/dist/tasks/issue-triage/fix-loop/promotion-task.js +108 -0
  99. package/sandbox/dist/tasks/issue-triage/fix-loop/recovery.js +512 -0
  100. package/sandbox/dist/tasks/issue-triage/fix-loop/runners.js +233 -0
  101. package/sandbox/dist/tasks/issue-triage/fix-loop/state-machine.js +171 -0
  102. package/sandbox/dist/tasks/issue-triage/github-request.js +2 -0
  103. package/sandbox/dist/tasks/issue-triage/hero-group-map.js +206 -0
  104. package/sandbox/dist/tasks/issue-triage/markers.js +6 -0
  105. package/sandbox/dist/tasks/issue-triage/model.js +124 -0
  106. package/sandbox/dist/tasks/issue-triage/output.js +164 -0
  107. package/sandbox/dist/tasks/issue-triage/pr-creator.js +585 -0
  108. package/sandbox/dist/tasks/issue-triage/pr-readiness.js +25 -0
  109. package/sandbox/dist/tasks/issue-triage/prompt.js +63 -0
  110. package/sandbox/dist/tasks/issue-triage/repo-config.js +47 -0
  111. package/sandbox/dist/tasks/issue-triage/repo-prompts.js +77 -0
  112. package/sandbox/dist/tasks/issue-triage/report-fixture.js +53 -0
  113. package/sandbox/dist/tasks/issue-triage/report-history.js +113 -0
  114. package/sandbox/dist/tasks/issue-triage/report-schema.js +63 -0
  115. package/sandbox/dist/tasks/issue-triage/report.js +111 -0
  116. package/sandbox/dist/tasks/issue-triage/resolution.js +30 -0
  117. package/sandbox/dist/tasks/issue-triage/routing.js +169 -0
  118. package/sandbox/dist/tasks/issue-triage/sandbox-environment.js +59 -0
  119. package/sandbox/dist/tasks/issue-triage/shared.js +86 -0
  120. package/sandbox/dist/tasks/issue-triage/side-effects.js +102 -0
  121. package/sandbox/dist/tasks/issue-triage/triage.js +442 -0
  122. package/sandbox/dist/tasks/issue-triage/types.js +1 -0
  123. package/sandbox/dist/tasks/persist-logs/context.js +19 -0
  124. package/sandbox/dist/tasks/persist-logs/dd-proxy.js +116 -0
  125. package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +156 -0
  126. package/sandbox/dist/tasks/persist-logs/persist-logs.js +491 -0
  127. package/sandbox/dist/tasks/refresh-s3-urls/refresh-s3-urls.js +214 -0
  128. package/sandbox/dist/tasks/review/check-run.js +326 -0
  129. package/sandbox/dist/tasks/review/comment-mapper.js +46 -0
  130. package/sandbox/dist/tasks/review/conversation-context.js +477 -0
  131. package/sandbox/dist/tasks/review/diff.js +95 -0
  132. package/sandbox/dist/tasks/review/engines/dedupe.js +291 -0
  133. package/sandbox/dist/tasks/review/engines/multi-focus.js +419 -0
  134. package/sandbox/dist/tasks/review/engines/shared.js +87 -0
  135. package/sandbox/dist/tasks/review/engines/types.js +1 -0
  136. package/sandbox/dist/tasks/review/finding-output.js +18 -0
  137. package/sandbox/dist/tasks/review/format.js +17 -0
  138. package/sandbox/dist/tasks/review/github-comment-format.js +94 -0
  139. package/sandbox/dist/tasks/review/local-git.js +374 -0
  140. package/sandbox/dist/tasks/review/local.js +118 -0
  141. package/sandbox/dist/tasks/review/multi-focus-prompt.js +59 -0
  142. package/sandbox/dist/tasks/review/prompt.js +81 -0
  143. package/sandbox/dist/tasks/review/review-feedback.js +48 -0
  144. package/sandbox/dist/tasks/review/review-mode-github.js +58 -0
  145. package/sandbox/dist/tasks/review/review-mode-policy.js +232 -0
  146. package/sandbox/dist/tasks/review/review-mode-types.js +1 -0
  147. package/sandbox/dist/tasks/review/review-mode.js +296 -0
  148. package/sandbox/dist/tasks/review/review-summary.js +123 -0
  149. package/sandbox/dist/tasks/review/review.js +391 -0
  150. package/sandbox/dist/tasks/review/runner.js +255 -0
  151. package/sandbox/dist/tasks/review/severity.js +5 -0
  152. package/sandbox/dist/tasks/review/summary-model.js +271 -0
  153. package/sandbox/dist/tasks/review/thinking-level.js +14 -0
  154. package/sandbox/dist/tasks/review/translation-review-policy.js +37 -0
  155. package/sandbox/dist/tasks/review/types.js +1 -0
  156. package/sandbox/dist/tasks/review-slice-plan/context.js +66 -0
  157. package/sandbox/dist/tasks/review-slice-plan/plan.js +210 -0
  158. package/sandbox/dist/tasks/review-slice-plan/prompt.js +59 -0
  159. package/sandbox/dist/tasks/review-slice-plan/review-slice-plan.js +367 -0
  160. package/sandbox/dist/tasks/sentry-triage/store.js +133 -0
  161. package/sandbox/dist/tasks/sentry-triage/triage.js +217 -0
  162. package/sandbox/node_modules/doistbot-logging/dist/index.d.ts +23 -0
  163. package/sandbox/node_modules/doistbot-logging/dist/index.js +51 -0
  164. package/sandbox/node_modules/doistbot-logging/package.json +12 -0
  165. package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +93 -0
  166. package/sandbox/node_modules/doistbot-repo-config/dist/index.js +197 -0
  167. package/sandbox/node_modules/doistbot-repo-config/package.json +12 -0
  168. package/sandbox/src/review/prompts/ai-internal-tools.md +254 -0
  169. package/sandbox/src/review/prompts/backend-repository-standards.md +102 -0
  170. package/sandbox/src/review/prompts/observability-standard.md +169 -0
  171. package/sandbox/src/review/prompts/outline-map.json +26 -0
  172. package/sandbox/src/review/prompts/secrets-management.md +286 -0
  173. package/sandbox/src/review/prompts/service-production-readiness.md +122 -0
  174. package/sandbox/src/review/prompts/workload-placement.md +345 -0
  175. package/sandbox/src/tasks/review/prompts/review-focus-prompts/efficiency.md +16 -0
  176. package/sandbox/src/tasks/review/prompts/review-focus-prompts/general.md +32 -0
  177. package/sandbox/src/tasks/review/prompts/review-focus-prompts/quality.md +20 -0
  178. package/sandbox/src/tasks/review/prompts/review-focus-prompts/reuse.md +13 -0
  179. package/sandbox/src/tasks/review/prompts/review-focus-prompts/security.md +18 -0
  180. package/sandbox/src/tasks/review/prompts/review-focus-prompts/standards.md +21 -0
  181. package/sandbox/src/tasks/review/prompts/review-focus-prompts/tests.md +19 -0
  182. package/sandbox/src/tasks/review/prompts/review-mode.md +40 -0
  183. package/sandbox/src/tasks/review/prompts/review-multi-focus-base-prompt.md +142 -0
  184. package/scripts/restore-runtime-assets.mjs +21 -0
  185. package/README.md +0 -3
@@ -0,0 +1,617 @@
1
+ import { execFileSync } from 'child_process';
2
+ import { writeFileSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { sampleAutofixResources, summarizeAutofixResources } from '../../core/autofix-resources.js';
5
+ import { AUTOFIX_RUNTIME_ENV, hashAutofixInputs, isCurrentCheck, readVerificationState, saveVerificationState, selectCheckResult, } from '../../core/autofix-verification.js';
6
+ import { logger } from '../../core/logging.js';
7
+ import { fillPromptTemplate, loadPromptTemplateFile, resolvePromptFile, } from '../../core/prompt-template.js';
8
+ import { recordEvent } from '../../core/tracing.js';
9
+ import { resolveAutofixCapabilityEnv } from './autofix-capabilities.js';
10
+ import { AutofixCheckpointStore, checkpointIdentity } from './autofix-checkpoint.js';
11
+ import { describeAutofixPolicy, loadAutofixPolicy } from './autofix-policy.js';
12
+ import { validateDiff } from './diff-validator.js';
13
+ import { CODEX_CANNOT_FIX_REASON, FIX_LOOP_FAILED_REASON_PREFIX } from './fix-failure-reasons.js';
14
+ import { AUTOFIX_FAILURE_MARKER, FIX_LOOP_START_MARKER } from './markers.js';
15
+ import { invokeTriageModel } from './model.js';
16
+ import { findPRTemplate, RECORDED_CHECKS_HEADING } from './pr-creator.js';
17
+ import { describeWorkflow, loadRepositoryWorkflow, serializeAutofixPolicy } from './repo-prompts.js';
18
+ import { describeSandboxEnvironment, sandboxEnvironmentError } from './sandbox-environment.js';
19
+ const FIX_PROMPT_FILE = 'issue-triage-fix-prompt.md';
20
+ // Maximum time allowed for the Codex fix-generation invocation before we abort
21
+ const AUTO_FIX_CODEX_TIMEOUT_MS = 140 * 60 * 1000;
22
+ const CANNOT_FIX_MARKER = '## Cannot Fix';
23
+ export function shouldAttemptFix(assessment) {
24
+ return (assessment.classification === 'logs-needed' ||
25
+ (assessment.classification === 'auto-fixable' && assessment.confidence === 'high'));
26
+ }
27
+ export function buildFixAssessmentSkipReason(assessment) {
28
+ return `Assessment: ${assessment.classification} (confidence: ${assessment.confidence})`;
29
+ }
30
+ function loadFixPromptTemplate() {
31
+ return loadPromptTemplateFile({
32
+ fileName: resolvePromptFile(import.meta.url, FIX_PROMPT_FILE),
33
+ promptLabel: 'issue triage fix',
34
+ });
35
+ }
36
+ function buildPRTemplateSection(prTemplate) {
37
+ return [
38
+ '### PR Body',
39
+ '',
40
+ 'The target repository uses the following pull request template:',
41
+ '',
42
+ '<template>',
43
+ prTemplate,
44
+ '</template>',
45
+ '',
46
+ 'After the Changes Made and Verification sections, output a `## PR Body` section containing a',
47
+ 'filled-in version of this template. Replace placeholder text with details about your changes. For',
48
+ "checkbox items, check the ones that apply. Keep the template's structure intact.",
49
+ '',
50
+ 'Reference the source issue with its full URL because the PR may be in another repository.',
51
+ 'For logging work, use "Related to" instead of closing keywords such as "Fixes" or "Closes",',
52
+ 'including when the repository template contains a closing keyword. Logging does not fix the issue.',
53
+ ].join('\n');
54
+ }
55
+ export function parsePRTitleFromOutput(output) {
56
+ const headingPattern = /^## PR Title\s*$/m;
57
+ const headingMatch = output.match(headingPattern);
58
+ if (headingMatch?.index === undefined) {
59
+ return null;
60
+ }
61
+ const afterHeading = output.slice(headingMatch.index + headingMatch[0].length);
62
+ const nextHeadingIdx = afterHeading.search(/^##\s/m);
63
+ const section = nextHeadingIdx >= 0 ? afterHeading.slice(0, nextHeadingIdx) : afterHeading;
64
+ const title = section.trim().split('\n')[0]?.trim() ?? null;
65
+ return title || null;
66
+ }
67
+ export function stripPRTitleSection(output) {
68
+ const headingPattern = /^## PR Title\s*$/m;
69
+ const headingMatch = output.match(headingPattern);
70
+ if (headingMatch?.index === undefined) {
71
+ return output;
72
+ }
73
+ const before = output.slice(0, headingMatch.index);
74
+ const afterHeading = output.slice(headingMatch.index + headingMatch[0].length);
75
+ const nextHeadingIdx = afterHeading.search(/^##\s/m);
76
+ const after = nextHeadingIdx >= 0 ? afterHeading.slice(nextHeadingIdx) : '';
77
+ return (before + after).replace(/\n{3,}/g, '\n\n').trim();
78
+ }
79
+ export function parsePRBodyFromOutput(output) {
80
+ const marker = '## PR Body';
81
+ const startIdx = output.indexOf(marker);
82
+ if (startIdx === -1) {
83
+ return null;
84
+ }
85
+ const trimmed = output.slice(startIdx + marker.length).trim();
86
+ return trimmed || null;
87
+ }
88
+ function buildFixPrompt({ triageOutput, fixAssessment, issueUrl, targetRepo, prTemplate, workflow, }) {
89
+ const template = loadFixPromptTemplate();
90
+ return (fillPromptTemplate(template, {
91
+ IMPLEMENTATION_WORKFLOW: describeWorkflow(workflow, 'autofix-workflow.md'),
92
+ REPO_FULL: targetRepo.repository.fullName,
93
+ ISSUE_URL: issueUrl,
94
+ TRIAGE_ANALYSIS: triageOutput,
95
+ FIX_SCOPE: fixAssessment.scope,
96
+ FIX_CLASSIFICATION: fixAssessment.classification,
97
+ LOGGING_PLAN_CHECK: fixAssessment.classification === 'logs-needed'
98
+ ? loadPromptTemplateFile({
99
+ fileName: resolvePromptFile(import.meta.url, 'logging-plan-check.md'),
100
+ promptLabel: 'logging plan check',
101
+ })
102
+ : '',
103
+ TARGET_REPO_ROOT: targetRepo.localPath,
104
+ PR_TEMPLATE_SECTION: prTemplate ? buildPRTemplateSection(prTemplate) : '',
105
+ }) + describeSandboxEnvironment());
106
+ }
107
+ function formatPreviousFinding(finding) {
108
+ const tag = finding.severity ? `[${finding.severity}] ` : '';
109
+ const body = finding.body.trim().replace(/\s+/g, ' ');
110
+ return `- ${tag}${finding.path}:${finding.line}: ${body}`;
111
+ }
112
+ export function buildPreviousFindingsSection(findings) {
113
+ return [
114
+ '# Previous Review Findings',
115
+ '',
116
+ 'A prior fix attempt for this issue was reviewed and the following issues were',
117
+ 'flagged. Address each finding in your new fix attempt before producing the diff:',
118
+ '',
119
+ ...findings.map(formatPreviousFinding),
120
+ '',
121
+ '---',
122
+ '',
123
+ ].join('\n');
124
+ }
125
+ export function buildFixPromptWithFindings(input) {
126
+ const basePrompt = buildFixPrompt(input);
127
+ if (!input.previousFindings || input.previousFindings.length === 0) {
128
+ return basePrompt;
129
+ }
130
+ return buildPreviousFindingsSection(input.previousFindings) + basePrompt;
131
+ }
132
+ function captureGitDiff(repoLocalPath) {
133
+ try {
134
+ // Stage all changes (including new untracked files) so they appear in the diff
135
+ // and flow through validation before being committed.
136
+ execFileSync('git', ['add', '-A'], {
137
+ cwd: repoLocalPath,
138
+ encoding: 'utf8',
139
+ stdio: 'pipe',
140
+ });
141
+ return execFileSync('git', [
142
+ 'diff',
143
+ '--no-ext-diff',
144
+ '--no-textconv',
145
+ '--full-index',
146
+ '--cached',
147
+ 'HEAD',
148
+ '--binary',
149
+ ], {
150
+ cwd: repoLocalPath,
151
+ encoding: 'utf8',
152
+ maxBuffer: 5 * 1024 * 1024,
153
+ });
154
+ }
155
+ catch (error) {
156
+ logger.error('Failed to capture git diff', {
157
+ task: 'issue-triage',
158
+ repoLocalPath,
159
+ error: error instanceof Error ? error.message : String(error),
160
+ });
161
+ return '';
162
+ }
163
+ }
164
+ // Detect the "## Cannot Fix" verdict and pull Codex's own explanation in one pass,
165
+ // so the failure comment can tell the human *why* it bailed, not just that it did.
166
+ // Returns null when the marker is absent (not a cannot-fix verdict), the explanation
167
+ // string when present, or '' when the model gave the verdict with no explanation.
168
+ // The full tail is intentional — the prompt makes "## Cannot Fix" the terminal output.
169
+ // ponytail: 2000-char cap keeps the comment readable; raise it if reasons get truncated.
170
+ export function extractCannotFixDetail(output) {
171
+ const idx = output.indexOf(CANNOT_FIX_MARKER);
172
+ if (idx === -1)
173
+ return null;
174
+ return output
175
+ .slice(idx + CANNOT_FIX_MARKER.length)
176
+ .trim()
177
+ .slice(0, 2000);
178
+ }
179
+ async function findSourceIssueCommentByMarker({ octokit, ctx, marker, }) {
180
+ const comments = await octokit.paginate(octokit.rest.issues.listComments, {
181
+ owner: ctx.owner,
182
+ repo: ctx.repo,
183
+ issue_number: ctx.issueNumber,
184
+ per_page: 100,
185
+ });
186
+ return comments.find((c) => typeof c.body === 'string' && c.body.includes(marker));
187
+ }
188
+ export async function shouldPostAutoFixFailureComment(fixResult, { dryRun, octokit, ctx }) {
189
+ if (dryRun)
190
+ return false;
191
+ if (!fixResult.attempted)
192
+ return false;
193
+ if (fixResult.prCreated)
194
+ return false;
195
+ // A PR opened during the loop is already announced via FIX_LOOP_START_MARKER;
196
+ // a "didn't produce a fix PR" comment would be misleading.
197
+ if (fixResult.interruption || fixResult.recoveryRefused)
198
+ return true;
199
+ if (fixResult.prNumber !== undefined)
200
+ return false;
201
+ if (fixResult.reason.startsWith(FIX_LOOP_FAILED_REASON_PREFIX)) {
202
+ try {
203
+ const hasFixLoopStartComment = await sourceIssueHasFixLoopStartComment({ octokit, ctx });
204
+ if (hasFixLoopStartComment) {
205
+ logger.info('Auto-fix failure comment skipped', {
206
+ task: 'issue-triage',
207
+ issue_number: ctx.issueNumber,
208
+ reason: 'fix-loop start comment exists',
209
+ });
210
+ return false;
211
+ }
212
+ }
213
+ catch (error) {
214
+ logger.warn('Failed to check fix-loop start comment', {
215
+ task: 'issue-triage',
216
+ issue_number: ctx.issueNumber,
217
+ error: error instanceof Error ? error.message : String(error),
218
+ });
219
+ return false;
220
+ }
221
+ }
222
+ return true;
223
+ }
224
+ export async function sourceIssueHasFixLoopStartComment({ octokit, ctx, }) {
225
+ const comment = await findSourceIssueCommentByMarker({
226
+ octokit,
227
+ ctx,
228
+ marker: FIX_LOOP_START_MARKER,
229
+ });
230
+ return comment !== undefined;
231
+ }
232
+ export function buildAutoFixFailureCommentBody({ reason, targetRepoFullName, prNumber, interruption, }) {
233
+ const repoNote = targetRepoFullName ? ` in \`${targetRepoFullName}\`` : '';
234
+ return [
235
+ AUTOFIX_FAILURE_MARKER,
236
+ '',
237
+ '### Automated fix attempt failed',
238
+ '',
239
+ prNumber
240
+ ? `The fix stopped${repoNote}. PR #${prNumber} keeps the changes already pushed.`
241
+ : `The fix stopped before a PR was opened${repoNote}.`,
242
+ '',
243
+ '**Reason:**',
244
+ '',
245
+ // Tilde fence, not backticks: the reason often carries Codex's own
246
+ // backtick-fenced output (logs, CLI), which would close a ``` block early.
247
+ '~~~',
248
+ reason,
249
+ '~~~',
250
+ '',
251
+ ...(interruption ? [buildRecoveryNotice(interruption), ''] : []),
252
+ 'You can retry with `@doistbot /fix` or `@doistbot /retry`.',
253
+ ].join('\n');
254
+ }
255
+ export function buildRecoveryNotice(interruption) {
256
+ const work = interruption.stage === 'review'
257
+ ? 'The PR and review step are saved'
258
+ : 'The patch, check results, and loop step are saved';
259
+ return interruption.saved
260
+ ? `${work} in shared storage. Retry will continue from the last saved checkpoint if the report, check policy, and repository state still match. It will not start the fix again.`
261
+ : 'I could not confirm a save of the latest work in shared storage. A retry may need to start a fresh attempt.';
262
+ }
263
+ export async function postAutoFixFailureComment({ octokit, ctx, reason, targetRepoFullName, prNumber, interruption, }) {
264
+ const body = buildAutoFixFailureCommentBody({
265
+ reason,
266
+ targetRepoFullName,
267
+ prNumber,
268
+ interruption,
269
+ });
270
+ const existing = await findSourceIssueCommentByMarker({
271
+ octokit,
272
+ ctx,
273
+ marker: AUTOFIX_FAILURE_MARKER,
274
+ });
275
+ if (existing) {
276
+ await octokit.rest.issues.updateComment({
277
+ owner: ctx.owner,
278
+ repo: ctx.repo,
279
+ comment_id: existing.id,
280
+ body,
281
+ });
282
+ }
283
+ else {
284
+ await octokit.rest.issues.createComment({
285
+ owner: ctx.owner,
286
+ repo: ctx.repo,
287
+ issue_number: ctx.issueNumber,
288
+ body,
289
+ });
290
+ }
291
+ logger.info('Auto-fix failure comment posted', {
292
+ task: 'issue-triage',
293
+ issue_number: ctx.issueNumber,
294
+ target_repo: targetRepoFullName,
295
+ operation: existing ? 'update' : 'create',
296
+ });
297
+ }
298
+ export async function runFixCore(input) {
299
+ const attemptStartedAt = Date.now();
300
+ const { ctx, triageOutput, fixAssessment, targetRepo, resetTo = 'HEAD' } = input;
301
+ if (!input.force && !shouldAttemptFix(fixAssessment)) {
302
+ logger.info('Fix core skipped: assessment does not qualify', {
303
+ task: 'issue-triage',
304
+ issue_number: ctx.issueNumber,
305
+ classification: fixAssessment.classification,
306
+ confidence: fixAssessment.confidence,
307
+ });
308
+ return {
309
+ ok: false,
310
+ reason: buildFixAssessmentSkipReason(fixAssessment),
311
+ };
312
+ }
313
+ const environmentError = sandboxEnvironmentError(targetRepo.repository.fullName);
314
+ if (environmentError)
315
+ return { ok: false, reason: environmentError };
316
+ const capabilityEnvResult = resolveAutofixCapabilityEnv(targetRepo.repository.fullName);
317
+ if (!capabilityEnvResult.ok) {
318
+ return capabilityEnvResult;
319
+ }
320
+ let policy;
321
+ let baseCommit;
322
+ let store;
323
+ let verification = { roundsUsed: 0, results: [] };
324
+ let restoredPatch = '';
325
+ let identity;
326
+ let workflow;
327
+ try {
328
+ policy = loadAutofixPolicy(targetRepo.localPath);
329
+ workflow = loadRepositoryWorkflow(targetRepo.localPath, 'autofix');
330
+ baseCommit = execFileSync('git', ['rev-parse', resetTo], {
331
+ cwd: targetRepo.localPath,
332
+ encoding: 'utf8',
333
+ stdio: 'pipe',
334
+ }).trim();
335
+ identity = checkpointIdentity({
336
+ issueUrl: input.issueUrl,
337
+ repository: targetRepo.repository.fullName,
338
+ baseCommit,
339
+ triageOutput,
340
+ policy: serializeAutofixPolicy(policy, workflow),
341
+ });
342
+ store = new AutofixCheckpointStore(identity,
343
+ // Sentry persists through IssueFixRecovery, under its short-lived prefix.
344
+ ctx.dryRun || ctx.sentryIssue ? undefined : process.env.LOG_SNAPSHOT_BUCKET);
345
+ if (input.checkpoint) {
346
+ const checkpoint = input.checkpoint;
347
+ if (checkpoint && checkpoint.baseCommit !== baseCommit)
348
+ throw new Error('Saved patch base does not match the fix attempt');
349
+ if (checkpoint) {
350
+ if (checkpoint.patch &&
351
+ !validateDiff(checkpoint.patch, {
352
+ forbiddenTestPaths: policy.forbidden_test_paths,
353
+ }).valid) {
354
+ throw new Error('Saved patch does not meet the current autofix rules');
355
+ }
356
+ restoredPatch = checkpoint.patch;
357
+ verification = checkpoint.verification;
358
+ }
359
+ }
360
+ }
361
+ catch (error) {
362
+ return {
363
+ ok: false,
364
+ reason: `Could not prepare autofix policy or checkpoint: ${error instanceof Error ? error.message : String(error)}`,
365
+ };
366
+ }
367
+ // Reset repo to clean state before fix attempt (triage Codex may have modified files).
368
+ // Use `git reset --hard` to clear both the working tree and the index, since
369
+ // captureGitDiff stages files with `git add -A`.
370
+ try {
371
+ execFileSync('git', ['reset', '--hard', resetTo], {
372
+ cwd: targetRepo.localPath,
373
+ encoding: 'utf8',
374
+ stdio: 'pipe',
375
+ });
376
+ execFileSync('git', ['clean', '-fd'], {
377
+ cwd: targetRepo.localPath,
378
+ encoding: 'utf8',
379
+ stdio: 'pipe',
380
+ });
381
+ if (restoredPatch) {
382
+ execFileSync('git', ['apply', '--index', '--whitespace=nowarn', '-'], {
383
+ cwd: targetRepo.localPath,
384
+ input: restoredPatch,
385
+ encoding: 'utf8',
386
+ stdio: 'pipe',
387
+ });
388
+ logger.info('Autofix checkpoint restored', {
389
+ baseCommit,
390
+ roundsUsed: verification.roundsUsed,
391
+ });
392
+ }
393
+ }
394
+ catch (error) {
395
+ const reason = error instanceof Error ? error.message : String(error);
396
+ logger.warn('Failed to reset repo state before fix attempt', {
397
+ task: 'issue-triage',
398
+ issueNumber: ctx.issueNumber,
399
+ targetRepo: targetRepo.repository.fullName,
400
+ error: reason,
401
+ });
402
+ return {
403
+ ok: false,
404
+ reason: `Failed to reset repository state before fix attempt: ${reason}`,
405
+ };
406
+ }
407
+ const prTemplate = findPRTemplate(targetRepo.localPath);
408
+ if (prTemplate) {
409
+ logger.info('PR template found for fix attempt', {
410
+ task: 'issue-triage',
411
+ issueNumber: ctx.issueNumber,
412
+ targetRepo: targetRepo.repository.fullName,
413
+ templateChars: prTemplate.length,
414
+ });
415
+ }
416
+ // Run Codex to generate the fix
417
+ const prompt = buildFixPromptWithFindings({
418
+ triageOutput,
419
+ fixAssessment,
420
+ issueUrl: input.issueUrl,
421
+ targetRepo,
422
+ prTemplate,
423
+ previousFindings: input.previousFindings,
424
+ workflow,
425
+ }) +
426
+ describeAutofixPolicy(policy) +
427
+ (restoredPatch
428
+ ? `\n## Resumed fix\nThe saved patch is already applied. Continue this fix. Do not start over. ${verification.roundsUsed} verification rounds were used. Saved results follow. Passed checks apply only to the same source inputs.\n${JSON.stringify({ ...verification, results: verification.results.map((result) => ({ ...result, output: result.output.slice(-2000) })) })}\n`
429
+ : '');
430
+ const timeoutMs = Math.max(1, AUTO_FIX_CODEX_TIMEOUT_MS - (Date.now() - attemptStartedAt));
431
+ const statePath = path.join(store.directory, 'verification.json');
432
+ const runtimePath = path.join(store.directory, 'runtime.json');
433
+ saveVerificationState(statePath, verification);
434
+ const runtime = {
435
+ repoPath: targetRepo.localPath,
436
+ statePath,
437
+ policy,
438
+ deadlineMs: attemptStartedAt + AUTO_FIX_CODEX_TIMEOUT_MS - 2000,
439
+ };
440
+ writeFileSync(runtimePath, JSON.stringify(runtime), { mode: 0o600 });
441
+ const samples = [];
442
+ function sample() {
443
+ const resource = sampleAutofixResources(Date.now() - attemptStartedAt);
444
+ samples.push(resource);
445
+ recordEvent('autofix.resources', {
446
+ 'autofix.elapsed_ms': resource.elapsedMs,
447
+ 'autofix.cpu_seconds': resource.cpuSeconds,
448
+ 'autofix.memory_bytes': resource.memoryBytes,
449
+ 'autofix.memory_limit_bytes': resource.memoryLimitBytes,
450
+ 'autofix.oom_kills': resource.oomKills,
451
+ });
452
+ }
453
+ sample();
454
+ const sampler = setInterval(sample, 10_000);
455
+ const codexResult = await invokeTriageModel({
456
+ // Distinct phase name: this is fix generation, not triage analysis.
457
+ // Callers (e.g. issue-fix-retry) override to keep it in their namespace.
458
+ phaseName: input.modelPhaseName ?? 'triage.fix-attempt.model',
459
+ prompt,
460
+ apiKey: ctx.openaiApiKey,
461
+ cwd: targetRepo.localPath,
462
+ thinkingLevel: 'max',
463
+ timeoutMs,
464
+ capabilityPreset: 'autofix',
465
+ extraEnv: {
466
+ GH_TOKEN: input.ghToken,
467
+ ...capabilityEnvResult.env,
468
+ [AUTOFIX_RUNTIME_ENV]: runtimePath,
469
+ },
470
+ })
471
+ .catch((error) => ({
472
+ ok: false,
473
+ error: error instanceof Error ? error.message : String(error),
474
+ durationMs: Date.now() - attemptStartedAt,
475
+ timedOut: Date.now() >= runtime.deadlineMs,
476
+ }))
477
+ .finally(() => clearInterval(sampler));
478
+ sample();
479
+ verification = readVerificationState(statePath);
480
+ for (const result of verification.results) {
481
+ if (result.status === 'running') {
482
+ result.durationMs = Date.now() - result.startedAtMs;
483
+ if (result.buildStartedAtMs !== null)
484
+ result.buildDurationMs = Date.now() - result.buildStartedAtMs;
485
+ result.timedOut = !codexResult.ok && codexResult.timedOut;
486
+ }
487
+ }
488
+ const diff = captureGitDiff(targetRepo.localPath);
489
+ const resourceSummary = summarizeAutofixResources(samples);
490
+ const finalFingerprint = hashAutofixInputs(diff);
491
+ function checkStatus(result, check) {
492
+ if (!result)
493
+ return 'not-run';
494
+ if (!isCurrentCheck(result, check, finalFingerprint, verification))
495
+ return 'stale';
496
+ if (result.status !== 'finished')
497
+ return 'incomplete';
498
+ return result.exitCode === 0 && !result.timedOut ? 'passed' : 'failed';
499
+ }
500
+ const finalChecks = policy.verification.checks.map((check) => {
501
+ const result = selectCheckResult(verification, check, finalFingerprint);
502
+ return {
503
+ name: check.name,
504
+ command: check.command,
505
+ testSelectors: result?.testSelectors ?? [],
506
+ status: checkStatus(result, check),
507
+ };
508
+ });
509
+ let failureReason;
510
+ if (codexResult.ok) {
511
+ const cannotFixDetail = extractCannotFixDetail(codexResult.output);
512
+ if (cannotFixDetail !== null) {
513
+ failureReason = cannotFixDetail
514
+ ? `${CODEX_CANNOT_FIX_REASON}\n\n${cannotFixDetail}`
515
+ : CODEX_CANNOT_FIX_REASON;
516
+ }
517
+ else if (!diff.trim()) {
518
+ failureReason = 'Codex produced no file changes.';
519
+ }
520
+ else {
521
+ const validation = validateDiff(diff, {
522
+ forbiddenTestPaths: policy.forbidden_test_paths,
523
+ });
524
+ if (!validation.valid)
525
+ failureReason = `Diff validation failed: ${validation.reason}`;
526
+ }
527
+ }
528
+ const status = !codexResult.ok && codexResult.timedOut
529
+ ? 'timed-out'
530
+ : !codexResult.ok || failureReason
531
+ ? 'failed'
532
+ : 'completed';
533
+ const saved = await (input.saveCheckpoint ?? store.save.bind(store))({
534
+ version: 1,
535
+ identity,
536
+ baseCommit,
537
+ patch: diff,
538
+ verification,
539
+ status,
540
+ savedAt: new Date().toISOString(),
541
+ }, JSON.stringify({
542
+ status,
543
+ baseCommit,
544
+ durationMs: Date.now() - attemptStartedAt,
545
+ verification,
546
+ finalChecks,
547
+ modelWaitMs: codexResult.telemetry?.nonToolDurationMs ?? null,
548
+ modelWaitDefinition: 'Elapsed invocation time outside tool calls, including model and transport wait.',
549
+ resources: resourceSummary,
550
+ samples,
551
+ }));
552
+ recordEvent('autofix.summary', {
553
+ 'autofix.status': status,
554
+ 'autofix.duration_ms': Date.now() - attemptStartedAt,
555
+ 'autofix.rounds_used': verification.roundsUsed,
556
+ 'autofix.model_wait_ms': codexResult.telemetry?.nonToolDurationMs ?? null,
557
+ 'autofix.sampled_peak_memory_bytes': resourceSummary.sampledPeakMemoryBytes,
558
+ 'autofix.memory_limit_bytes': resourceSummary.memoryLimitBytes,
559
+ 'autofix.cpu_seconds': resourceSummary.cpuSeconds,
560
+ 'autofix.peak_cpu_cores': resourceSummary.peakCpuCores,
561
+ 'autofix.oom_kills': resourceSummary.oomKills,
562
+ });
563
+ if (!codexResult.ok) {
564
+ logger.warn('Fix attempt invocation failed', {
565
+ task: 'issue-triage',
566
+ issueNumber: ctx.issueNumber,
567
+ error: codexResult.error,
568
+ timedOut: codexResult.timedOut,
569
+ durationMs: codexResult.durationMs,
570
+ });
571
+ return {
572
+ ok: false,
573
+ reason: `Fix attempt failed: ${codexResult.error}`,
574
+ ...(codexResult.timedOut ? { interruption: { stage: 'fix', saved } } : {}),
575
+ };
576
+ }
577
+ const fixDescription = codexResult.output;
578
+ logger.info('Fix Codex invocation completed', {
579
+ task: 'issue-triage',
580
+ issueNumber: ctx.issueNumber,
581
+ durationMs: codexResult.durationMs,
582
+ outputChars: fixDescription.length,
583
+ });
584
+ if (failureReason)
585
+ return { ok: false, reason: failureReason };
586
+ const generatedPRTitle = parsePRTitleFromOutput(fixDescription) ??
587
+ (fixAssessment.classification === 'logs-needed' ? 'chore: add diagnostics' : null);
588
+ if (generatedPRTitle) {
589
+ logger.info('Parsed PR title from Codex output', {
590
+ task: 'issue-triage',
591
+ issueNumber: ctx.issueNumber,
592
+ generatedPRTitle,
593
+ });
594
+ }
595
+ const checkSummary = finalChecks.length
596
+ ? `\n\n${RECORDED_CHECKS_HEADING}\n\n` +
597
+ finalChecks
598
+ .map((check) => `- ${check.name}: ${check.status}. Command: \`${check.command}\`.${check.testSelectors.length ? ` Test selectors: ${check.testSelectors.join(', ')}.` : ''}`)
599
+ .join('\n')
600
+ : '';
601
+ const parsedPRBody = parsePRBodyFromOutput(fixDescription);
602
+ const generatedPRBody = parsedPRBody ? parsedPRBody + checkSummary : null;
603
+ if (generatedPRBody) {
604
+ logger.info('Parsed PR body from Codex output', {
605
+ task: 'issue-triage',
606
+ issueNumber: ctx.issueNumber,
607
+ prBodyChars: generatedPRBody.length,
608
+ });
609
+ }
610
+ return {
611
+ ok: true,
612
+ codexOutput: fixDescription + checkSummary,
613
+ diff,
614
+ generatedPRTitle,
615
+ generatedPRBody,
616
+ };
617
+ }
@@ -0,0 +1,5 @@
1
+ export function fixSource(source) {
2
+ return source.sentryIssue
3
+ ? { sentryIssue: source.sentryIssue }
4
+ : { issueNumber: source.issueNumber };
5
+ }