@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,133 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
3
+ import { isRecord, isSentryRetryableStatus, isSentryRunRecord, sentryArtifactsPrefix, sentryRunKey, } from 'doistbot-issue-routing';
4
+ export async function currentTaskArn() {
5
+ const uri = process.env.ECS_CONTAINER_METADATA_URI_V4;
6
+ if (!uri)
7
+ throw new Error('Sentry triage requires ECS task metadata');
8
+ const response = await fetch(`${uri}/task`, { signal: AbortSignal.timeout(5000) });
9
+ if (!response.ok)
10
+ throw new Error(`Cannot read ECS task metadata: ${response.status}`);
11
+ const metadata = await response.json();
12
+ if (!isRecord(metadata) ||
13
+ typeof metadata.TaskARN !== 'string' ||
14
+ !metadata.TaskARN.startsWith('arn:aws:ecs:'))
15
+ throw new Error('ECS task metadata has no valid TaskARN');
16
+ return metadata.TaskARN;
17
+ }
18
+ /** Conditional writes give exactly one sandbox ownership of each saved run. */
19
+ export class SentryRunStore {
20
+ bucket;
21
+ envelope;
22
+ client;
23
+ key;
24
+ etag;
25
+ record;
26
+ savedReport;
27
+ constructor(bucket, envelope, client = new S3Client({ maxAttempts: 2 })) {
28
+ this.bucket = bucket;
29
+ this.envelope = envelope;
30
+ this.client = client;
31
+ this.key = sentryRunKey(envelope.issue);
32
+ }
33
+ get reportBody() {
34
+ return this.savedReport;
35
+ }
36
+ get runId() {
37
+ return this.record?.runId;
38
+ }
39
+ get fixStarted() {
40
+ return this.record?.fixStarted ?? false;
41
+ }
42
+ async claim(route, taskArn) {
43
+ const retentionDays = Number(process.env.SENTRY_ARTIFACT_RETENTION_DAYS ?? '1');
44
+ if (!Number.isInteger(retentionDays) || retentionDays < 1 || retentionDays > 7)
45
+ throw new Error('SENTRY_ARTIFACT_RETENTION_DAYS must be an integer from 1 to 7');
46
+ let previous;
47
+ if (this.envelope.retryOf) {
48
+ const object = await this.client.send(new GetObjectCommand({ Bucket: this.bucket, Key: this.key }), { abortSignal: AbortSignal.timeout(15_000) });
49
+ const parsed = JSON.parse(await object.Body.transformToString());
50
+ if (!isSentryRunRecord(parsed) || !object.ETag)
51
+ throw new Error('Invalid saved Sentry run');
52
+ if (this.envelope.retryOf !== parsed.runId)
53
+ return false;
54
+ // Completed runs are cleaned up. Retrying them could create a second PR.
55
+ if (!isSentryRetryableStatus(parsed.status))
56
+ throw new Error(`Cannot retry Sentry run in ${parsed.status} state`);
57
+ if (Date.now() >= Date.parse(parsed.retryUntil))
58
+ throw new Error('Sentry retry window has expired; inspect the PR and logs manually');
59
+ if (parsed.route.product !== route.product || parsed.route.routeId !== route.routeId)
60
+ throw new Error('Cannot resume a Sentry run with changed team routing');
61
+ previous = parsed;
62
+ this.etag = object.ETag;
63
+ if (parsed.hasReport) {
64
+ const report = await this.client.send(new GetObjectCommand({
65
+ Bucket: this.bucket,
66
+ Key: `${sentryArtifactsPrefix(this.envelope.issue)}report.md`,
67
+ }), { abortSignal: AbortSignal.timeout(15_000) });
68
+ // Missing retry data must fail closed, never start a second fix.
69
+ this.savedReport = await report.Body.transformToString();
70
+ }
71
+ }
72
+ const now = new Date().toISOString();
73
+ const record = {
74
+ version: 1,
75
+ runId: randomUUID(),
76
+ taskArn,
77
+ envelope: this.envelope,
78
+ route,
79
+ status: 'running',
80
+ startedAt: now,
81
+ updatedAt: now,
82
+ retryUntil: previous?.retryUntil ??
83
+ new Date(Date.now() + retentionDays * 86_400_000).toISOString(),
84
+ hasReport: previous?.hasReport ?? false,
85
+ // Old records cannot prove that no fix started. Keep recovery strict for them.
86
+ fixStarted: previous?.fixStarted ?? previous?.hasReport ?? false,
87
+ pr: previous?.pr,
88
+ };
89
+ try {
90
+ await this.write(record);
91
+ return true;
92
+ }
93
+ catch (error) {
94
+ if (error.$metadata?.httpStatusCode ===
95
+ 412)
96
+ return false;
97
+ throw error;
98
+ }
99
+ }
100
+ async save(result) {
101
+ if (!this.record)
102
+ throw new Error('Sentry run has not been claimed');
103
+ const { reportBody, ...outcome } = result;
104
+ if (reportBody !== undefined && reportBody !== this.savedReport) {
105
+ await this.client.send(new PutObjectCommand({
106
+ Bucket: this.bucket,
107
+ Key: `${sentryArtifactsPrefix(this.envelope.issue)}report.md`,
108
+ Body: reportBody,
109
+ ContentType: 'text/markdown; charset=utf-8',
110
+ }), { abortSignal: AbortSignal.timeout(15_000) });
111
+ this.savedReport = reportBody;
112
+ }
113
+ await this.write({
114
+ ...this.record,
115
+ ...outcome,
116
+ hasReport: this.savedReport !== undefined,
117
+ updatedAt: new Date().toISOString(),
118
+ });
119
+ }
120
+ async write(record) {
121
+ const result = await this.client.send(new PutObjectCommand({
122
+ Bucket: this.bucket,
123
+ Key: this.key,
124
+ Body: JSON.stringify(record),
125
+ ContentType: 'application/json',
126
+ ...(this.etag ? { IfMatch: this.etag } : { IfNoneMatch: '*' }),
127
+ }), { abortSignal: AbortSignal.timeout(15_000) });
128
+ if (!result.ETag)
129
+ throw new Error('S3 did not return an ETag for the Sentry run');
130
+ this.etag = result.ETag;
131
+ this.record = record;
132
+ }
133
+ }
@@ -0,0 +1,217 @@
1
+ import { findIssueRoute, isRecord, isSentryEnvelope, isSentryRouteSnapshot, parseSandboxVariant, repositoryKey, resolveSentryRoute, sentryIssueUrl, } from 'doistbot-issue-routing';
2
+ import { App } from 'octokit';
3
+ import { requiredEnv, WORKSPACE_DIR } from '../../core/environment.js';
4
+ import { logger } from '../../core/logging.js';
5
+ import { resolveWriteOctokit } from '../../core/shared.js';
6
+ import { recordDecision, withPhase } from '../../core/tracing.js';
7
+ import { cloneTriageRepositories } from '../issue-triage/clone.js';
8
+ import { runConfiguredFix } from '../issue-triage/fix-dispatch.js';
9
+ import { invokeTriageModel } from '../issue-triage/model.js';
10
+ import { validateTriageOutput } from '../issue-triage/output.js';
11
+ import { buildTriagePrompt } from '../issue-triage/prompt.js';
12
+ import { createRepoConfigLoader, isAutoTriageEnabled } from '../issue-triage/repo-config.js';
13
+ import { readTriageReport, renderTriageReport, reportAssessment, reportForFix, } from '../issue-triage/report.js';
14
+ import { prepareWorkspaceReposDirectory } from '../issue-triage/shared.js';
15
+ import { currentTaskArn, SentryRunStore } from './store.js';
16
+ function parseSentryTask(raw) {
17
+ const task = JSON.parse(raw);
18
+ if (!isRecord(task) || !isSentryEnvelope(task.envelope) || !isSentryRouteSnapshot(task.route))
19
+ throw new Error('Invalid SENTRY_TASK');
20
+ const route = resolveSentryRoute(task.envelope, [task.route]);
21
+ if (!route)
22
+ throw new Error('Sentry task does not match project route');
23
+ return { envelope: task.envelope, route };
24
+ }
25
+ export async function runSentryTriage() {
26
+ const { envelope, route } = parseSentryTask(requiredEnv('SENTRY_TASK'));
27
+ const scope = findIssueRoute(route.product, route.routeId);
28
+ const issue = envelope.issue;
29
+ const issueUrl = sentryIssueUrl(issue);
30
+ const startedAt = Date.now();
31
+ const store = new SentryRunStore(requiredEnv('LOG_SNAPSHOT_BUCKET'), envelope);
32
+ logger.setBaseContext({ source: 'sentry', ...issue, deliveryId: envelope.deliveryId });
33
+ if (!(await store.claim(route, await currentTaskArn()))) {
34
+ logger.info('Sentry issue skipped: already claimed', { runKey: store.key });
35
+ return;
36
+ }
37
+ let reportBody = store.reportBody;
38
+ let fix;
39
+ async function finish(status, reason) {
40
+ await store.save({
41
+ status,
42
+ reportBody,
43
+ ...(fix?.attempted && fix.prNumber !== undefined
44
+ ? { pr: { repository: fix.repository, number: fix.prNumber } }
45
+ : {}),
46
+ });
47
+ logger.info('Sentry triage result', {
48
+ status,
49
+ reason,
50
+ mode: route.mode,
51
+ team: scope.ownerTeam,
52
+ runKey: store.key,
53
+ runId: store.runId,
54
+ durationMs: Date.now() - startedAt,
55
+ fix,
56
+ });
57
+ }
58
+ await withPhase('sentry.triage', {
59
+ 'sentry.issue_id': issue.issueId,
60
+ 'sentry.project_id': issue.projectId,
61
+ 'gen_ai.operation.name': 'invoke_agent',
62
+ 'gen_ai.agent.name': 'doistbot.sentry-triage',
63
+ }, async () => {
64
+ try {
65
+ requiredEnv('SENTRY_AUTH_TOKEN');
66
+ if (process.env.SENTRY_ORG !== route.organization)
67
+ throw new Error('Sentry organization does not match sandbox configuration');
68
+ const primary = scope.repositories[0];
69
+ const app = new App({
70
+ appId: requiredEnv('APP_ID'),
71
+ privateKey: requiredEnv('PRIVATE_KEY'),
72
+ });
73
+ const installation = await app.octokit.rest.apps.getRepoInstallation({
74
+ owner: primary.owner,
75
+ repo: primary.repo,
76
+ });
77
+ const installationId = installation.data.id;
78
+ const readOctokit = await app.getInstallationOctokit(installationId);
79
+ const githubToken = requiredEnv('GITHUB_TOKEN');
80
+ const writeOctokit = resolveWriteOctokit({
81
+ githubToken,
82
+ fallbackOctokit: readOctokit,
83
+ });
84
+ const loadRepoConfig = createRepoConfigLoader({ octokit: readOctokit, logger });
85
+ for (const repository of scope.repositories) {
86
+ // Load errors reach the failure handler and remain retryable.
87
+ if (!isAutoTriageEnabled(await loadRepoConfig(repository))) {
88
+ await finish('skipped', 'repository-opt-out');
89
+ return;
90
+ }
91
+ }
92
+ let report;
93
+ let repositories = scope.repositories;
94
+ if (reportBody !== undefined) {
95
+ const saved = readTriageReport(reportBody);
96
+ const targetRepo = saved && 'targetRepo' in saved.analysis.decision
97
+ ? saved.analysis.decision.targetRepo
98
+ : undefined;
99
+ if (saved?.issueUrl !== issueUrl ||
100
+ (targetRepo &&
101
+ !scope.repositories.some((repo) => repositoryKey(repo.fullName) === repositoryKey(targetRepo))))
102
+ throw new Error('Saved Sentry report does not match the current scope');
103
+ report = saved;
104
+ repositories = targetRepo
105
+ ? scope.repositories.filter((repo) => repositoryKey(repo.fullName) === repositoryKey(targetRepo))
106
+ : [];
107
+ }
108
+ const { clonedRepositories, failedRepositories } = await cloneTriageRepositories({
109
+ app,
110
+ repositories,
111
+ reposDir: prepareWorkspaceReposDirectory(),
112
+ fallbackInstallationId: installationId,
113
+ });
114
+ if (failedRepositories.length)
115
+ throw new Error('Could not clone the required Sentry repositories');
116
+ const auth = (await app.octokit.auth({ type: 'installation', installationId }));
117
+ const history = { latest: null, pending: null };
118
+ const ctx = {
119
+ owner: primary.owner,
120
+ repo: primary.repo,
121
+ sentryIssue: { ...issue, product: route.product, routeId: route.routeId },
122
+ installationId,
123
+ appId: requiredEnv('APP_ID'),
124
+ privateKey: requiredEnv('PRIVATE_KEY'),
125
+ githubToken,
126
+ openaiApiKey: requiredEnv('OPENAI_KEY'),
127
+ geminiApiKey: process.env.GEMINI_API_KEY,
128
+ deliveryId: envelope.deliveryId,
129
+ dryRun: false,
130
+ triggerType: envelope.fixCommentId ? 'manual' : 'auto',
131
+ sandboxVariant: parseSandboxVariant(process.env.SANDBOX_VARIANT),
132
+ };
133
+ if (!report) {
134
+ const triage = await invokeTriageModel({
135
+ prompt: buildTriagePrompt({
136
+ issueUrl,
137
+ repositories: clonedRepositories,
138
+ history,
139
+ source: 'sentry',
140
+ }),
141
+ apiKey: ctx.openaiApiKey,
142
+ cwd: WORKSPACE_DIR,
143
+ thinkingLevel: 'xhigh',
144
+ extraEnv: { GH_TOKEN: auth.token },
145
+ phaseName: 'sentry.model',
146
+ });
147
+ if (!triage.ok)
148
+ throw new Error(triage.error);
149
+ const validated = await validateTriageOutput({
150
+ rawOutput: triage.output,
151
+ issueUrl,
152
+ repositories: scope.repositories.map((repo) => repo.fullName),
153
+ history,
154
+ // Save the full report in S3; openDraftFixPR limits the GitHub description.
155
+ enforceCommentLimit: false,
156
+ });
157
+ if (!validated.ok)
158
+ throw new Error(validated.error);
159
+ report = validated.report;
160
+ }
161
+ reportBody = renderTriageReport(report);
162
+ const assessment = reportAssessment(report);
163
+ recordDecision('sentry.fix-eligibility', {
164
+ mode: route.mode,
165
+ classification: assessment.classification,
166
+ confidence: assessment.confidence,
167
+ });
168
+ if (route.mode === 'triage' ||
169
+ assessment.classification !== 'auto-fixable' ||
170
+ assessment.confidence !== 'high') {
171
+ await finish('triaged', assessment.classification);
172
+ return;
173
+ }
174
+ // Persist the exact report before effects; recovery hashes this report.
175
+ await store.save({ status: 'running', reportBody });
176
+ fix = await runConfiguredFix({
177
+ ctx,
178
+ octokit: writeOctokit,
179
+ triageOutput: reportForFix(report),
180
+ fixAssessment: assessment,
181
+ clonedRepos: clonedRepositories,
182
+ issueTitle: issue.shortId ?? `Sentry issue ${issue.issueId}`,
183
+ issueUrl,
184
+ ghToken: auth.token,
185
+ retry: store.fixStarted,
186
+ onRecoverySaved: async () => {
187
+ if (!store.fixStarted)
188
+ await store.save({ status: 'running', fixStarted: true });
189
+ },
190
+ });
191
+ await finish(fix.attempted && fix.prCreated ? 'awaiting-ci' : 'needs-human', 'reason' in fix ? fix.reason : undefined);
192
+ }
193
+ catch (error) {
194
+ try {
195
+ await finish('failed', error instanceof Error ? error.message : String(error));
196
+ }
197
+ catch (saveError) {
198
+ logger.error('Could not save Sentry failure', {
199
+ runKey: store.key,
200
+ error: String(saveError),
201
+ });
202
+ }
203
+ throw error;
204
+ }
205
+ });
206
+ }
207
+ export async function main() {
208
+ try {
209
+ await runSentryTriage();
210
+ }
211
+ catch (error) {
212
+ logger.error('Sentry triage failed', {
213
+ error: error instanceof Error ? error.message : String(error),
214
+ });
215
+ process.exitCode = 1;
216
+ }
217
+ }
@@ -0,0 +1,23 @@
1
+ export type LogLevel = 'info' | 'warn' | 'error';
2
+ export type LogMeta = Record<string, unknown>;
3
+ export type LoggerOptions = {
4
+ baseContext?: LogMeta;
5
+ getContext?: () => LogMeta | undefined;
6
+ getTraceInfo?: () => LogMeta | undefined;
7
+ suppressLogs?: (level: LogLevel) => boolean;
8
+ suppressInfoLogs?: () => boolean;
9
+ includeLogField?: boolean;
10
+ /** Receives serialized JSON without a trailing newline. The writer must add one. */
11
+ write?: (line: string) => void;
12
+ buildInfo?: {
13
+ version: number;
14
+ commit: string;
15
+ };
16
+ };
17
+ export type Logger = {
18
+ info: (message: string, meta?: LogMeta) => void;
19
+ warn: (message: string, meta?: LogMeta) => void;
20
+ error: (message: string, meta?: LogMeta) => void;
21
+ setBaseContext: (meta: LogMeta) => void;
22
+ };
23
+ export declare function createLogger(options?: LoggerOptions): Logger;
@@ -0,0 +1,51 @@
1
+ export function createLogger(options = {}) {
2
+ let baseContext = options.baseContext ?? {};
3
+ const includeLogField = options.includeLogField ?? false;
4
+ const buildInfo = options.buildInfo;
5
+ function setBaseContext(meta) {
6
+ baseContext = meta;
7
+ }
8
+ function log(level, message, meta) {
9
+ if (options.suppressLogs?.(level) === true) {
10
+ return;
11
+ }
12
+ if (level === 'info' && options.suppressInfoLogs?.() === true) {
13
+ return;
14
+ }
15
+ const context = options.getContext?.() ?? {};
16
+ const traceInfo = options.getTraceInfo?.() ?? {};
17
+ const metaInfo = {
18
+ ...context,
19
+ ...baseContext,
20
+ ...(meta ?? {}),
21
+ };
22
+ const payload = {
23
+ level,
24
+ message,
25
+ timestamp: new Date().toISOString(),
26
+ };
27
+ if (includeLogField) {
28
+ payload.log = message;
29
+ }
30
+ if (buildInfo) {
31
+ payload.version = buildInfo.version;
32
+ payload.commit = buildInfo.commit;
33
+ }
34
+ Object.assign(payload, traceInfo);
35
+ payload.meta = metaInfo;
36
+ const line = JSON.stringify(payload);
37
+ if (options.write) {
38
+ options.write(line);
39
+ }
40
+ else {
41
+ // eslint-disable-next-line no-console
42
+ console.log(line);
43
+ }
44
+ }
45
+ return {
46
+ info: (message, meta) => log('info', message, meta),
47
+ warn: (message, meta) => log('warn', message, meta),
48
+ error: (message, meta) => log('error', message, meta),
49
+ setBaseContext,
50
+ };
51
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "doistbot-logging",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./src/index.ts",
9
+ "default": "./dist/index.js"
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,93 @@
1
+ import type { Logger } from 'doistbot-logging';
2
+ import type { Octokit } from 'octokit';
3
+ export declare const CONFIG_FILE_PATH = ".github/doistbot.yml";
4
+ export declare const DEFAULT_REVIEW_MODELS: readonly ["openai", "deepseek", "zai"];
5
+ export type DefaultReviewModel = (typeof DEFAULT_REVIEW_MODELS)[number];
6
+ export type AutofixPolicy = {
7
+ guidance?: string;
8
+ forbidden_test_paths?: string[];
9
+ verification: {
10
+ checks: {
11
+ name: string;
12
+ command: string;
13
+ use_test_selectors?: boolean;
14
+ }[];
15
+ };
16
+ };
17
+ export type ReviewModeConfig = {
18
+ guidance_path?: string;
19
+ labels?: {
20
+ ask?: string[];
21
+ show?: string[];
22
+ ship?: string[];
23
+ };
24
+ };
25
+ export type RepoConfig = {
26
+ review?: {
27
+ skip_forks?: boolean;
28
+ skip_drafts?: boolean;
29
+ skip_title_pattern?: string;
30
+ skip_branch_pattern?: string;
31
+ mode?: ReviewModeConfig;
32
+ /**
33
+ * @deprecated Ignored as of #274. Doistbot now posts a GitHub Check
34
+ * Run for every `/review` (always-on; no opt-in) when `HEAD_SHA` is
35
+ * available. Kept here for back-compat: any existing
36
+ * `.github/doistbot.yml` setting this field will parse fine but the
37
+ * value has no effect.
38
+ */
39
+ post_commit_status?: boolean;
40
+ };
41
+ issue_triage?: {
42
+ enable_auto_triage?: boolean;
43
+ prompts?: {
44
+ triage?: string;
45
+ autofix?: string;
46
+ };
47
+ /** @deprecated Ignored. Every actionable triage report starts a fix. */
48
+ enable_auto_fix?: boolean;
49
+ /** @deprecated Ignored. Every issue fix uses the review and CI loop. */
50
+ use_fix_loop?: boolean;
51
+ max_changed_files?: number;
52
+ max_changed_lines?: number;
53
+ autofix?: AutofixPolicy;
54
+ };
55
+ };
56
+ type FetchFileParams = {
57
+ octokit: Octokit;
58
+ owner: string;
59
+ repo: string;
60
+ path: string;
61
+ ref?: string;
62
+ signal?: AbortSignal;
63
+ logger?: Logger;
64
+ strict?: boolean;
65
+ maxBytes?: number;
66
+ };
67
+ export declare class RepoFileContentError extends Error {
68
+ readonly reason: 'not_file' | 'too_large' | 'unavailable';
69
+ constructor(reason: 'not_file' | 'too_large' | 'unavailable');
70
+ }
71
+ /** Review-mode validation is opt-in so it cannot block unrelated config callers. */
72
+ export declare function parseRepoConfigContent(content: string, { validateReviewMode }?: {
73
+ validateReviewMode?: boolean;
74
+ }): RepoConfig;
75
+ export declare function fetchFileContent({ octokit, owner, repo, path, ref, signal, logger, strict, maxBytes, }: FetchFileParams): Promise<string | null>;
76
+ /**
77
+ * Fetches the repository-specific configuration from .github/doistbot.yml.
78
+ * Returns null when the file is genuinely absent (404) or the path is not a file.
79
+ * Throws on transport / authorization errors so callers can distinguish "no
80
+ * config" from "could not load config" — important when an opt-out flag would
81
+ * otherwise be silently ignored.
82
+ */
83
+ export declare function getRepoConfig({ octokit, owner, repo, ref, signal, logger, }: Omit<FetchFileParams, 'path' | 'strict' | 'maxBytes'> & {
84
+ logger: Logger;
85
+ }): Promise<RepoConfig | null>;
86
+ export declare function getLocalRepoConfig({ workspaceDir, logger, strict, }: {
87
+ workspaceDir: string;
88
+ logger: Logger;
89
+ strict?: boolean;
90
+ }): RepoConfig | null;
91
+ export declare function getDefaultReviewModels(): DefaultReviewModel[];
92
+ export declare function parseReviewModelsEnabled(value: string | undefined): string[];
93
+ export {};