@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,59 @@
1
+ import { repositoryKey, repositorySandboxProfile, SANDBOX_PROFILES } from 'doistbot-issue-routing';
2
+ function readSelection() {
3
+ if (!process.env.SANDBOX_SELECTION)
4
+ return undefined;
5
+ const selection = JSON.parse(process.env.SANDBOX_SELECTION);
6
+ function strings(value) {
7
+ return Array.isArray(value) && value.every((item) => typeof item === 'string');
8
+ }
9
+ if (!selection ||
10
+ !strings(selection.repositories) ||
11
+ !strings(selection.labels) ||
12
+ !strings(selection.verificationLimits) ||
13
+ typeof selection.reason !== 'string' ||
14
+ (selection.profile !== 'legacy' &&
15
+ (!SANDBOX_PROFILES.includes(selection.profile) || typeof selection.image !== 'string')))
16
+ throw new Error('Invalid sandbox selection.');
17
+ return selection;
18
+ }
19
+ /** Check image identity only. Repo setup and required checks belong to issue #524. */
20
+ export function sandboxEnvironmentError(repository) {
21
+ let selection;
22
+ try {
23
+ selection = readSelection();
24
+ }
25
+ catch {
26
+ return 'Invalid sandbox selection.';
27
+ }
28
+ if (!selection)
29
+ return undefined;
30
+ if (!selection.repositories.some((candidate) => repositoryKey(candidate) === repositoryKey(repository)))
31
+ return 'Fix target is outside the recorded sandbox scope.';
32
+ // Mixed scopes deliberately retain the existing image and autofix behavior.
33
+ if (selection.profile === 'legacy')
34
+ return undefined;
35
+ const profile = repositorySandboxProfile(repository, selection.labels);
36
+ if (selection.profile !== profile ||
37
+ process.env.SANDBOX_PROFILE !== profile ||
38
+ process.env.SANDBOX_IMAGE_ID !== selection.image)
39
+ return 'Unsupported fix environment: target needs a different image. Cross-image fix dispatch is not enabled.';
40
+ return undefined;
41
+ }
42
+ export function describeSandboxEnvironment() {
43
+ let selection;
44
+ try {
45
+ selection = readSelection();
46
+ }
47
+ catch {
48
+ return '\n\nSandbox selection is invalid; automatic fixes are unavailable.';
49
+ }
50
+ if (!selection)
51
+ return '';
52
+ const tools = {
53
+ legacy: 'Git, gh, rg, curl, and Node are available. Check availability before using other tools.',
54
+ web: 'Git, gh, rg, curl, pup, sentry, Node, npm, pnpm, Yarn, Bun, Chromium, and Playwright are installed.',
55
+ python: 'Git, gh, rg, curl, pup, sentry, Python, uv, Poetry, pip, and native build tools are installed.',
56
+ android: 'Git, gh, rg, curl, pup, sentry, Java, Kotlin, Android SDK, Chromium, and Playwright are installed.',
57
+ };
58
+ return `\n\n## Sandbox environment\nProfile: ${selection.profile}. ${selection.reason}\n${tools[selection.profile]}\n${selection.verificationLimits.join('\n')}\nReport only checks actually performed.`;
59
+ }
@@ -0,0 +1,86 @@
1
+ import { spawnSync } from 'child_process';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { extractIssueLabelNames } from 'doistbot-issue-routing';
5
+ import { App } from 'octokit';
6
+ import { REPOS_SUBDIR, WORKSPACE_DIR } from '../../core/environment.js';
7
+ import { gitAuthEnv, GITHUB_HTTPS_REWRITE_ARGS } from '../../core/git-auth.js';
8
+ import { logger } from '../../core/logging.js';
9
+ import { resolveWriteOctokit } from '../../core/shared.js';
10
+ import { resolveInstallationIdForRepo } from './pr-creator.js';
11
+ export { TRIAGE_MARKER } from './markers.js';
12
+ export function getIssueUrl(owner, repo, issueNumber) {
13
+ return `https://github.com/${owner}/${repo}/issues/${issueNumber}`;
14
+ }
15
+ export function extractLabelNames(labels) {
16
+ return extractIssueLabelNames(labels);
17
+ }
18
+ export function prepareWorkspaceReposDirectory() {
19
+ const reposDir = path.join(WORKSPACE_DIR, REPOS_SUBDIR);
20
+ fs.mkdirSync(reposDir, { recursive: true });
21
+ return reposDir;
22
+ }
23
+ export async function createGitHubClients(ctx) {
24
+ const app = new App({
25
+ appId: ctx.appId,
26
+ privateKey: ctx.privateKey,
27
+ });
28
+ const readOctokit = await app.getInstallationOctokit(ctx.installationId);
29
+ const writeOctokit = resolveWriteOctokit({
30
+ githubToken: ctx.githubToken,
31
+ fallbackOctokit: readOctokit,
32
+ });
33
+ return { app, readOctokit, writeOctokit };
34
+ }
35
+ export { fetchIssue, fetchIssueComments, } from '../../core/github-issues.js';
36
+ export async function cloneRepository({ app, repository, reposDir, fallbackInstallationId, }) {
37
+ const installationId = await resolveInstallationIdForRepo({
38
+ app,
39
+ owner: repository.owner,
40
+ repo: repository.repo,
41
+ fallbackInstallationId,
42
+ });
43
+ const auth = (await app.octokit.auth({
44
+ type: 'installation',
45
+ installationId,
46
+ }));
47
+ const localPath = path.join(reposDir, repository.repo);
48
+ const repoUrl = `https://github.com/${repository.owner}/${repository.repo}.git`;
49
+ logger.info('Repository clone starting', {
50
+ task: 'issue-triage',
51
+ repository: repository.fullName,
52
+ localPath,
53
+ });
54
+ const result = spawnSync('git', [
55
+ ...GITHUB_HTTPS_REWRITE_ARGS,
56
+ 'clone',
57
+ '--depth',
58
+ '50',
59
+ '--recurse-submodules',
60
+ '--shallow-submodules',
61
+ repoUrl,
62
+ localPath,
63
+ ], {
64
+ encoding: 'utf8',
65
+ stdio: 'pipe',
66
+ env: gitAuthEnv(auth.token),
67
+ });
68
+ if (result.error) {
69
+ throw new Error(`Failed to clone ${repository.fullName}: ${result.error.message}`);
70
+ }
71
+ if (result.status !== 0) {
72
+ const stderrPreview = (result.stderr ?? '').trim().slice(0, 500);
73
+ const details = stderrPreview || `git exited with status ${result.status}`;
74
+ throw new Error(`Failed to clone ${repository.fullName}: ${details}`);
75
+ }
76
+ logger.info('Repository clone completed', {
77
+ task: 'issue-triage',
78
+ repository: repository.fullName,
79
+ stdoutBytes: (result.stdout ?? '').length,
80
+ stderrBytes: (result.stderr ?? '').length,
81
+ });
82
+ return {
83
+ repository,
84
+ localPath,
85
+ };
86
+ }
@@ -0,0 +1,102 @@
1
+ import { logger } from '../../core/logging.js';
2
+ import { withPhase } from '../../core/tracing.js';
3
+ import { MAX_TRIAGE_COMMENT_CHARS, TRIAGE_MARKER } from './markers.js';
4
+ export async function postIssueComment(ctx, octokit, body, options = {}) {
5
+ const marker = options.marker;
6
+ const placeholderCommentId = options.placeholderCommentId;
7
+ const bodyWithMarker = marker && !body.includes(marker) ? `${marker}\n\n${body}` : body;
8
+ if (bodyWithMarker.length > MAX_TRIAGE_COMMENT_CHARS) {
9
+ throw new Error(`Issue comment exceeds ${MAX_TRIAGE_COMMENT_CHARS} characters.`);
10
+ }
11
+ if (ctx.dryRun) {
12
+ logger.info('Issue triage dry run side effect skipped', {
13
+ task: 'issue-triage',
14
+ action: 'would_post_comment',
15
+ issueNumber: ctx.issueNumber,
16
+ marker,
17
+ bodyChars: bodyWithMarker.length,
18
+ placeholderCommentId,
19
+ });
20
+ process.stdout.write(`${bodyWithMarker}\n`);
21
+ return;
22
+ }
23
+ let updated = false;
24
+ if (placeholderCommentId) {
25
+ try {
26
+ await octokit.rest.issues.updateComment({
27
+ owner: ctx.owner,
28
+ repo: ctx.repo,
29
+ comment_id: placeholderCommentId,
30
+ body: bodyWithMarker,
31
+ });
32
+ updated = true;
33
+ }
34
+ catch (error) {
35
+ logger.warn('Failed to update placeholder comment, falling back to new comment', {
36
+ task: 'issue-triage',
37
+ commentId: placeholderCommentId,
38
+ error: error instanceof Error ? error.message : String(error),
39
+ });
40
+ }
41
+ }
42
+ if (!updated) {
43
+ await octokit.rest.issues.createComment({
44
+ owner: ctx.owner,
45
+ repo: ctx.repo,
46
+ issue_number: ctx.issueNumber,
47
+ body: bodyWithMarker,
48
+ });
49
+ }
50
+ }
51
+ export async function postTriageOutput(params) {
52
+ await withPhase('triage.side-effects', { 'comment.body.length': params.body.length }, async () => {
53
+ await postIssueComment(params.ctx, params.octokit, params.body, {
54
+ marker: TRIAGE_MARKER,
55
+ placeholderCommentId: params.ctx.placeholderCommentId,
56
+ });
57
+ });
58
+ }
59
+ export async function deleteIssueComment(ctx, octokit, commentId, options = {}) {
60
+ if (ctx.dryRun) {
61
+ logger.info('Issue triage dry run side effect skipped', {
62
+ task: 'issue-triage',
63
+ action: 'would_delete_comment',
64
+ issueNumber: ctx.issueNumber,
65
+ marker: options.marker,
66
+ commentId,
67
+ reason: options.reason,
68
+ });
69
+ return;
70
+ }
71
+ await octokit.rest.issues.deleteComment({
72
+ owner: ctx.owner,
73
+ repo: ctx.repo,
74
+ comment_id: commentId,
75
+ });
76
+ }
77
+ export async function deleteAutoTriagePlaceholder({ octokit, ctx, reason, }) {
78
+ if (ctx.triggerType !== 'auto' || !ctx.placeholderCommentId) {
79
+ return;
80
+ }
81
+ try {
82
+ await deleteIssueComment(ctx, octokit, ctx.placeholderCommentId, {
83
+ marker: TRIAGE_MARKER,
84
+ reason,
85
+ });
86
+ logger.info('Auto issue triage placeholder comment deleted', {
87
+ task: 'issue-triage',
88
+ issueNumber: ctx.issueNumber,
89
+ placeholderCommentId: ctx.placeholderCommentId,
90
+ reason,
91
+ });
92
+ }
93
+ catch (error) {
94
+ logger.warn('Failed to delete auto issue triage placeholder comment', {
95
+ task: 'issue-triage',
96
+ issueNumber: ctx.issueNumber,
97
+ placeholderCommentId: ctx.placeholderCommentId,
98
+ reason,
99
+ error: error instanceof Error ? error.message : String(error),
100
+ });
101
+ }
102
+ }
@@ -0,0 +1,442 @@
1
+ import { performance } from 'node:perf_hooks';
2
+ import { SpanStatusCode } from '@opentelemetry/api';
3
+ import { recordTaskMetrics } from '../../core/datadog-metrics.js';
4
+ import { WORKSPACE_DIR } from '../../core/environment.js';
5
+ import { logger } from '../../core/logging.js';
6
+ import { removeIssueCommentReactionIfPresent } from '../../core/reactions.js';
7
+ import { withPhase } from '../../core/tracing.js';
8
+ import { cloneTriageRepositories } from './clone.js';
9
+ import { loadTriageContext, parseIssueTriageContext } from './context.js';
10
+ import { postAutoFixFailureComment, shouldAttemptFix, shouldPostAutoFixFailureComment, } from './fix-attempt.js';
11
+ import { runConfiguredFix } from './fix-dispatch.js';
12
+ import { invokeTriageModel } from './model.js';
13
+ import { buildTriageFailureMessage, buildUnresolvedLabelsOutput, validateTriageOutput, } from './output.js';
14
+ import { buildReportCompactionPrompt, buildTriagePrompt } from './prompt.js';
15
+ import { allRepositoriesAllow, createRepoConfigLoader, isAutoTriageEnabled } from './repo-config.js';
16
+ import { readTriageHistory } from './report-history.js';
17
+ import { resolveTriageScope } from './resolution.js';
18
+ import { buildHeroRoutingComment, canRouteToHeroGroup, findAutoRoutedAssignees, postTriageRouting, shouldPostHeroRoutingComment, } from './routing.js';
19
+ import { createGitHubClients, getIssueUrl, prepareWorkspaceReposDirectory } from './shared.js';
20
+ import { deleteAutoTriagePlaceholder, postTriageOutput } from './side-effects.js';
21
+ const TRIAGE_PROVIDER = 'openai';
22
+ export async function runIssueTriage(ctx) {
23
+ if (ctx.triggerType === 'auto') {
24
+ return runAutoIssueTriage(ctx);
25
+ }
26
+ return runManualIssueTriage(ctx);
27
+ }
28
+ export async function runManualIssueTriage(ctx) {
29
+ return runIssueTriagePipeline(ctx, {
30
+ routeUnresolvedLabels: false,
31
+ });
32
+ }
33
+ export async function runAutoIssueTriage(ctx) {
34
+ return runIssueTriagePipeline(ctx, {
35
+ routeUnresolvedLabels: true,
36
+ });
37
+ }
38
+ async function runIssueTriagePipeline(ctx, mode) {
39
+ const startedAt = performance.now();
40
+ let triageMetricOutcome = 'failure';
41
+ const triageMetricRepository = `${ctx.owner}/${ctx.repo}`;
42
+ let triageDurationMs = null;
43
+ let pendingFixMetric = null;
44
+ // Temporary during auto-triage rollout: repo opt-out is an intentional skip,
45
+ // not triage work, so avoid reporting it as a success or failure.
46
+ let shouldRecordTriageMetric = true;
47
+ let triageCommentPosted = false;
48
+ logger.setBaseContext({
49
+ deliveryId: ctx.deliveryId,
50
+ repository: `${ctx.owner}/${ctx.repo}`,
51
+ issueNumber: ctx.issueNumber,
52
+ });
53
+ // The root withPhase wraps the entire run. OTel sets this span active, so
54
+ // every nested withPhase / startToolSpan parents under it automatically.
55
+ // withPhase handles the span lifecycle + ERROR status; the inner try/catch
56
+ // keeps the existing failure log and cleanup side effects.
57
+ return withPhase('triage', {
58
+ task: 'issue-triage',
59
+ 'gen_ai.operation.name': 'invoke_agent',
60
+ 'gen_ai.agent.name': 'doistbot.triage',
61
+ dry_run: ctx.dryRun,
62
+ 'issue.number': ctx.issueNumber,
63
+ 'comment.id': ctx.commentId,
64
+ trigger: ctx.triggerType,
65
+ }, async (rootSpan) => {
66
+ const { app, readOctokit, writeOctokit } = await createGitHubClients(ctx);
67
+ // postTriageOutput adds TRIAGE_MARKER (and edits the auto-triage
68
+ // placeholder) internally — callers pass marker-free bodies.
69
+ async function postFinalTriageComment(body) {
70
+ await postTriageOutput({ octokit: writeOctokit, ctx, body });
71
+ triageCommentPosted = true;
72
+ }
73
+ try {
74
+ const { issue, comments, labelNames } = await loadTriageContext({
75
+ octokit: readOctokit,
76
+ owner: ctx.owner,
77
+ repo: ctx.repo,
78
+ issueNumber: ctx.issueNumber,
79
+ });
80
+ const resolution = await resolveTriageScope({
81
+ triggerType: ctx.triggerType,
82
+ labelNames,
83
+ });
84
+ if (resolution.status === 'unresolved') {
85
+ const unresolvedBody = buildUnresolvedLabelsOutput({
86
+ reason: resolution.reason,
87
+ productLabels: resolution.parsedLabels.productLabels,
88
+ appLabels: resolution.parsedLabels.appLabels,
89
+ teamLabels: resolution.parsedLabels.teamLabels,
90
+ });
91
+ await postFinalTriageComment(unresolvedBody);
92
+ if (mode.routeUnresolvedLabels) {
93
+ await postTriageRouting({
94
+ octokit: writeOctokit,
95
+ ctx,
96
+ body: buildHeroRoutingComment({
97
+ labels: labelNames,
98
+ summary: unresolvedBody,
99
+ }),
100
+ });
101
+ }
102
+ triageDurationMs = performance.now() - startedAt;
103
+ return;
104
+ }
105
+ const getCachedRepoConfig = createRepoConfigLoader({
106
+ octokit: readOctokit,
107
+ logger,
108
+ });
109
+ if (ctx.triggerType === 'auto') {
110
+ const enableAutoTriage = await allRepositoriesAllow({
111
+ repositories: resolution.repositories,
112
+ loadRepoConfig: getCachedRepoConfig,
113
+ isEnabled: isAutoTriageEnabled,
114
+ feature: 'auto_triage',
115
+ logger,
116
+ });
117
+ if (!enableAutoTriage) {
118
+ shouldRecordTriageMetric = false;
119
+ logger.info('Auto issue triage skipped: a resolved repository disabled auto triage', {
120
+ task: 'issue-triage',
121
+ issueNumber: ctx.issueNumber,
122
+ resolvedRepos: resolution.repositories.map((repository) => repository.fullName),
123
+ });
124
+ await deleteAutoTriagePlaceholder({
125
+ octokit: writeOctokit,
126
+ ctx,
127
+ reason: 'auto_triage_disabled',
128
+ });
129
+ return;
130
+ }
131
+ }
132
+ const reposDir = prepareWorkspaceReposDirectory();
133
+ const { clonedRepositories, failedRepositories } = await cloneTriageRepositories({
134
+ app,
135
+ reposDir,
136
+ repositories: resolution.repositories,
137
+ fallbackInstallationId: ctx.installationId,
138
+ });
139
+ if (clonedRepositories.length === 0) {
140
+ await postFinalTriageComment(buildTriageFailureMessage('Repository cloning failed for all resolved repositories.'));
141
+ // Non-throwing failure: mark the root span so the run is
142
+ // not reported as ok in Datadog. withPhase never forces OK,
143
+ // so this survives to the root phase.end.
144
+ rootSpan.setStatus({
145
+ code: SpanStatusCode.ERROR,
146
+ message: 'Repository cloning failed for all resolved repositories.',
147
+ });
148
+ triageDurationMs = performance.now() - startedAt;
149
+ return;
150
+ }
151
+ if (failedRepositories.length > 0) {
152
+ logger.warn('Issue triage running in degraded mode after partial clone failures', {
153
+ task: 'issue-triage',
154
+ issueNumber: ctx.issueNumber,
155
+ cloneSucceeded: clonedRepositories.length,
156
+ cloneFailed: failedRepositories.length,
157
+ failedRepositories,
158
+ });
159
+ }
160
+ const issueInstallationAuth = (await app.octokit.auth({
161
+ type: 'installation',
162
+ installationId: ctx.installationId,
163
+ }));
164
+ const issueUrl = getIssueUrl(ctx.owner, ctx.repo, ctx.issueNumber);
165
+ const history = readTriageHistory(comments, issueUrl);
166
+ const prompt = buildTriagePrompt({
167
+ issueUrl,
168
+ repositories: clonedRepositories,
169
+ history,
170
+ });
171
+ const triageResult = await invokeTriageModel({
172
+ prompt,
173
+ apiKey: ctx.openaiApiKey,
174
+ cwd: WORKSPACE_DIR,
175
+ extraEnv: {
176
+ GH_TOKEN: issueInstallationAuth.token,
177
+ },
178
+ thinkingLevel: 'xhigh',
179
+ });
180
+ if (!triageResult.ok) {
181
+ await postFinalTriageComment(buildTriageFailureMessage(triageResult.error));
182
+ rootSpan.setStatus({ code: SpanStatusCode.ERROR, message: triageResult.error });
183
+ triageDurationMs = performance.now() - startedAt;
184
+ return;
185
+ }
186
+ const validationContext = {
187
+ issueUrl,
188
+ repositories: clonedRepositories.map(({ repository }) => repository.fullName),
189
+ history,
190
+ };
191
+ let validated = await validateTriageOutput({
192
+ ...validationContext,
193
+ rawOutput: triageResult.output,
194
+ });
195
+ if (!validated.ok && validated.reason === 'too-long') {
196
+ const { decision, reinvestigation } = validated.analysis;
197
+ const compacted = await invokeTriageModel({
198
+ prompt: buildReportCompactionPrompt(triageResult.output),
199
+ apiKey: ctx.openaiApiKey,
200
+ cwd: WORKSPACE_DIR,
201
+ thinkingLevel: 'low',
202
+ timeoutMs: 2 * 60 * 1000,
203
+ phaseName: 'triage.report-compaction',
204
+ capabilityPreset: 'none',
205
+ });
206
+ if (compacted.ok) {
207
+ validated = await validateTriageOutput({
208
+ ...validationContext,
209
+ rawOutput: compacted.output,
210
+ expectedMetadata: { decision, reinvestigation },
211
+ });
212
+ }
213
+ }
214
+ if (!validated.ok) {
215
+ await postFinalTriageComment(validated.commentOutput);
216
+ triageDurationMs = performance.now() - startedAt;
217
+ throw new Error(validated.error);
218
+ }
219
+ await postFinalTriageComment(validated.commentOutput);
220
+ triageMetricOutcome = 'success';
221
+ triageDurationMs = performance.now() - startedAt;
222
+ const fixAssessment = validated.assessment;
223
+ logger.info('Fix assessment parsed from triage output', {
224
+ task: 'issue-triage',
225
+ issueNumber: ctx.issueNumber,
226
+ classification: fixAssessment.classification,
227
+ confidence: fixAssessment.confidence,
228
+ scope: fixAssessment.scope,
229
+ });
230
+ if (shouldPostHeroRoutingComment(fixAssessment)) {
231
+ const primaryRepo = resolution.repositories.length === 1
232
+ ? resolution.repositories[0]
233
+ : undefined;
234
+ let autoRoutedAssignees = [];
235
+ if (canRouteToHeroGroup(labelNames, primaryRepo)) {
236
+ try {
237
+ autoRoutedAssignees = await findAutoRoutedAssignees({
238
+ octokit: readOctokit,
239
+ ctx,
240
+ });
241
+ }
242
+ catch (error) {
243
+ logger.warn('Failed to read auto-routed assignees; routing to Hero', {
244
+ task: 'issue-triage',
245
+ issueNumber: ctx.issueNumber,
246
+ error: error instanceof Error ? error.message : String(error),
247
+ });
248
+ }
249
+ logger.info('Triage routing target resolved', {
250
+ task: 'issue-triage',
251
+ issueNumber: ctx.issueNumber,
252
+ autoRoutedAssignees,
253
+ });
254
+ }
255
+ const routingComment = buildHeroRoutingComment({
256
+ labels: labelNames,
257
+ primaryRepo,
258
+ summary: validated.report.analysis.summary,
259
+ assessment: fixAssessment,
260
+ resolvedRepos: resolution.repositories.map((repo) => repo.fullName),
261
+ autoRoutedAssignees,
262
+ });
263
+ await postTriageRouting({
264
+ octokit: writeOctokit,
265
+ ctx,
266
+ body: routingComment,
267
+ });
268
+ }
269
+ const loggingPlanAlreadyPending = fixAssessment.classification === 'logs-needed' &&
270
+ validated.report.pendingLoggingReportId !== validated.report.id;
271
+ if (shouldAttemptFix(fixAssessment) && !loggingPlanAlreadyPending) {
272
+ const fixMetricRepository = fixAssessment.targetRepo ??
273
+ (clonedRepositories.length === 1
274
+ ? clonedRepositories[0].repository.fullName
275
+ : `${ctx.owner}/${ctx.repo}`);
276
+ pendingFixMetric = {
277
+ repository: fixMetricRepository,
278
+ startedAt: performance.now(),
279
+ outcome: 'failure',
280
+ };
281
+ const fixResult = await runConfiguredFix({
282
+ ctx,
283
+ octokit: writeOctokit,
284
+ triageOutput: validated.output,
285
+ fixAssessment,
286
+ clonedRepos: clonedRepositories,
287
+ issueTitle: issue.title ?? 'Untitled issue',
288
+ issueUrl: issue.html_url ?? getIssueUrl(ctx.owner, ctx.repo, ctx.issueNumber),
289
+ ghToken: issueInstallationAuth.token,
290
+ assigneeReview: {
291
+ assignees: (issue.assignees ?? []).map((assignee) => assignee.login),
292
+ membershipOctokit: readOctokit,
293
+ },
294
+ });
295
+ logger.info('triage.fix_attempted', {
296
+ task: 'issue-triage',
297
+ trigger: ctx.triggerType,
298
+ classification: fixAssessment.classification,
299
+ attempted: fixResult.attempted,
300
+ });
301
+ logger.info('Fix attempt result', {
302
+ task: 'issue-triage',
303
+ issueNumber: ctx.issueNumber,
304
+ attempted: fixResult.attempted,
305
+ prCreated: fixResult.attempted && fixResult.prCreated,
306
+ reason: 'reason' in fixResult ? fixResult.reason : undefined,
307
+ prUrl: 'prUrl' in fixResult ? fixResult.prUrl : undefined,
308
+ pr_number: 'prNumber' in fixResult ? fixResult.prNumber : undefined,
309
+ durationMs: performance.now() - startedAt,
310
+ });
311
+ pendingFixMetric = {
312
+ repository: fixResult.repository,
313
+ startedAt: pendingFixMetric.startedAt,
314
+ outcome: fixResult.attempted && fixResult.prCreated ? 'success' : 'failure',
315
+ };
316
+ const shouldPostFailureComment = await shouldPostAutoFixFailureComment(fixResult, {
317
+ dryRun: ctx.dryRun,
318
+ octokit: writeOctokit,
319
+ ctx,
320
+ });
321
+ if (shouldPostFailureComment) {
322
+ try {
323
+ await postAutoFixFailureComment({
324
+ octokit: writeOctokit,
325
+ ctx,
326
+ reason: 'reason' in fixResult ? fixResult.reason : 'Unknown reason',
327
+ targetRepoFullName: fixResult.repository,
328
+ ...('prNumber' in fixResult
329
+ ? { prNumber: fixResult.prNumber }
330
+ : {}),
331
+ ...('interruption' in fixResult
332
+ ? { interruption: fixResult.interruption }
333
+ : {}),
334
+ });
335
+ }
336
+ catch (commentError) {
337
+ logger.warn('Failed to post auto-fix failure comment', {
338
+ task: 'issue-triage',
339
+ issue_number: ctx.issueNumber,
340
+ error: commentError instanceof Error
341
+ ? commentError.message
342
+ : String(commentError),
343
+ });
344
+ }
345
+ }
346
+ }
347
+ else {
348
+ logger.info('triage.fix_attempted', {
349
+ task: 'issue-triage',
350
+ trigger: ctx.triggerType,
351
+ classification: fixAssessment.classification,
352
+ attempted: false,
353
+ });
354
+ }
355
+ }
356
+ catch (error) {
357
+ // withPhase records the exception + sets the root span ERROR status
358
+ // when we re-throw. This block keeps the existing failure log and
359
+ // user-visible cleanup side effects.
360
+ logger.error('Issue triage task failed', {
361
+ task: 'issue-triage',
362
+ issueNumber: ctx.issueNumber,
363
+ trigger: ctx.triggerType,
364
+ error: error instanceof Error ? error.message : String(error),
365
+ durationMs: performance.now() - startedAt,
366
+ });
367
+ if (ctx.triggerType === 'auto' && !triageCommentPosted) {
368
+ await deleteAutoTriagePlaceholder({
369
+ octokit: writeOctokit,
370
+ ctx,
371
+ reason: 'auto_triage_failed_before_final_comment',
372
+ });
373
+ }
374
+ if (!ctx.dryRun && ctx.triggerType !== 'auto' && !triageCommentPosted) {
375
+ try {
376
+ const reason = error instanceof Error ? error.message : String(error);
377
+ await postFinalTriageComment(buildTriageFailureMessage(reason));
378
+ }
379
+ catch (commentError) {
380
+ logger.warn('Failed to post issue triage failure response', {
381
+ task: 'issue-triage',
382
+ issueNumber: ctx.issueNumber,
383
+ error: commentError instanceof Error
384
+ ? commentError.message
385
+ : String(commentError),
386
+ });
387
+ }
388
+ }
389
+ throw error;
390
+ }
391
+ finally {
392
+ await removeIssueCommentReactionIfPresent({
393
+ octokit: writeOctokit,
394
+ owner: ctx.owner,
395
+ repo: ctx.repo,
396
+ commentId: ctx.commentId,
397
+ reactionId: ctx.reactionId,
398
+ warnMessage: 'Failed to remove issue triage reaction',
399
+ successMessage: 'Issue triage reaction removed',
400
+ logMeta: {
401
+ task: 'issue-triage',
402
+ reactionId: ctx.reactionId,
403
+ },
404
+ });
405
+ if (pendingFixMetric) {
406
+ await recordTaskMetrics({
407
+ type: 'fix',
408
+ repository: pendingFixMetric.repository,
409
+ model: TRIAGE_PROVIDER,
410
+ outcome: pendingFixMetric.outcome,
411
+ durationMs: performance.now() - pendingFixMetric.startedAt,
412
+ sandboxVariant: ctx.sandboxVariant,
413
+ });
414
+ }
415
+ if (shouldRecordTriageMetric) {
416
+ await recordTaskMetrics({
417
+ type: 'triage',
418
+ repository: triageMetricRepository,
419
+ model: TRIAGE_PROVIDER,
420
+ outcome: triageMetricOutcome,
421
+ durationMs: triageDurationMs ?? performance.now() - startedAt,
422
+ sandboxVariant: ctx.sandboxVariant,
423
+ });
424
+ }
425
+ }
426
+ });
427
+ }
428
+ async function main() {
429
+ try {
430
+ await runIssueTriage(parseIssueTriageContext());
431
+ }
432
+ catch (error) {
433
+ logger.error('Issue triage task failed in main', {
434
+ task: 'issue-triage',
435
+ error: error instanceof Error ? error.message : String(error),
436
+ });
437
+ process.exitCode = 1;
438
+ }
439
+ }
440
+ if (process.env.VITEST !== 'true' && process.env.TASK_TYPE === 'issue-triage') {
441
+ main();
442
+ }