@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,491 @@
1
+ import fs from 'fs';
2
+ import { performance } from 'node:perf_hooks';
3
+ import path from 'path';
4
+ import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
5
+ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
6
+ import { buildDatadogLink } from 'doistbot-shared-contracts';
7
+ import { App } from 'octokit';
8
+ import { recordTaskMetrics, } from '../../core/datadog-metrics.js';
9
+ import { REPOS_SUBDIR, WORKSPACE_DIR } from '../../core/environment.js';
10
+ import { logger } from '../../core/logging.js';
11
+ import { scrubPii } from '../../core/pii-scrubber.js';
12
+ import { removeIssueCommentReactionIfPresent } from '../../core/reactions.js';
13
+ import { resolveIssueRepositories } from '../../core/repository-resolver.js';
14
+ import { cloneDefaultBranchToPath } from '../../core/shared.js';
15
+ import { LOG_SNAPSHOT_MARKER, PRESIGN_EXPIRY_SECONDS } from '../../core/snapshot-helpers.js';
16
+ import { shutdownTracing } from '../../core/tracing-setup.js';
17
+ import { parsePersistLogsContext } from './context.js';
18
+ import { PERSIST_LOGS_PROVIDER, runInvestigation } from './llm-query-planner.js';
19
+ async function createInstallationOctokit(ctx) {
20
+ const app = new App({
21
+ appId: ctx.appId,
22
+ privateKey: ctx.privateKey,
23
+ });
24
+ const octokit = await app.getInstallationOctokit(ctx.installationId);
25
+ return { app, octokit };
26
+ }
27
+ async function fetchIssue(octokit, ctx) {
28
+ const response = await octokit.rest.issues.get({
29
+ owner: ctx.owner,
30
+ repo: ctx.repo,
31
+ issue_number: ctx.issueNumber,
32
+ });
33
+ return response.data;
34
+ }
35
+ async function fetchIssueComments(octokit, ctx) {
36
+ return (await octokit.paginate(octokit.rest.issues.listComments, {
37
+ owner: ctx.owner,
38
+ repo: ctx.repo,
39
+ issue_number: ctx.issueNumber,
40
+ per_page: 100,
41
+ }));
42
+ }
43
+ async function uploadToS3({ bucket, issueNumber, analysisContent, csvContent, rawJsonContent, worklogContent, }) {
44
+ const s3 = new S3Client({});
45
+ const now = new Date().toISOString().replace(/[:.]/g, '-');
46
+ const prefix = `issues/${issueNumber}/${now}`;
47
+ const files = [
48
+ {
49
+ name: 'analysis',
50
+ key: `${prefix}/analysis.md`,
51
+ body: analysisContent,
52
+ contentType: 'text/markdown',
53
+ },
54
+ {
55
+ name: 'csv',
56
+ key: `${prefix}/logs.csv`,
57
+ body: csvContent,
58
+ contentType: 'text/csv',
59
+ },
60
+ {
61
+ name: 'rawLogs',
62
+ key: `${prefix}/raw-logs.json`,
63
+ body: rawJsonContent,
64
+ contentType: 'application/json',
65
+ },
66
+ {
67
+ name: 'worklog',
68
+ key: `${prefix}/worklog.md`,
69
+ body: worklogContent,
70
+ contentType: 'text/markdown',
71
+ },
72
+ ];
73
+ const presignedUrls = { analysis: '', csv: '', rawLogs: '', worklog: '' };
74
+ const s3Keys = { analysis: '', csv: '', rawLogs: '', worklog: '' };
75
+ for (const file of files) {
76
+ if (!file.body)
77
+ continue;
78
+ await s3.send(new PutObjectCommand({
79
+ Bucket: bucket,
80
+ Key: file.key,
81
+ Body: file.body,
82
+ ContentType: file.contentType,
83
+ }));
84
+ s3Keys[file.name] = file.key;
85
+ presignedUrls[file.name] = await getSignedUrl(s3, new GetObjectCommand({ Bucket: bucket, Key: file.key }), { expiresIn: PRESIGN_EXPIRY_SECONDS });
86
+ }
87
+ return {
88
+ s3Path: `s3://${bucket}/${prefix}/`,
89
+ s3Keys,
90
+ presignedUrls,
91
+ };
92
+ }
93
+ /**
94
+ * Strip presigned S3 URLs from comment text to avoid leaking them into the LLM prompt.
95
+ * Replaces any URL containing `X-Amz-` query params with a placeholder.
96
+ */
97
+ function stripPresignedUrls(text) {
98
+ return text.replace(/https:\/\/[^\s)]+X-Amz-[^\s)]+/g, '[presigned-url-redacted]');
99
+ }
100
+ async function deleteComment(octokit, ctx, commentId) {
101
+ try {
102
+ await octokit.rest.issues.deleteComment({
103
+ owner: ctx.owner,
104
+ repo: ctx.repo,
105
+ comment_id: commentId,
106
+ });
107
+ }
108
+ catch {
109
+ // Best effort — don't fail the task over a cleanup issue
110
+ }
111
+ }
112
+ async function postComment({ octokit, ctx, body, }) {
113
+ await octokit.rest.issues.createComment({
114
+ owner: ctx.owner,
115
+ repo: ctx.repo,
116
+ issue_number: ctx.issueNumber,
117
+ body,
118
+ });
119
+ }
120
+ export async function main() {
121
+ const startedAt = performance.now();
122
+ const ctx = parsePersistLogsContext();
123
+ let exitCode = 0;
124
+ let metricOutcome = 'failure';
125
+ let metricReason;
126
+ logger.setBaseContext({
127
+ deliveryId: ctx.deliveryId,
128
+ repository: `${ctx.owner}/${ctx.repo}`,
129
+ issueNumber: ctx.issueNumber,
130
+ });
131
+ logger.info('Persist-logs task started', {
132
+ task: 'persist-logs',
133
+ issueNumber: ctx.issueNumber,
134
+ dryRun: ctx.dryRun,
135
+ });
136
+ let octokit;
137
+ let cleanedUp = false;
138
+ async function cleanup() {
139
+ if (cleanedUp)
140
+ return;
141
+ cleanedUp = true;
142
+ if (octokit) {
143
+ await removeIssueCommentReactionIfPresent({
144
+ octokit,
145
+ owner: ctx.owner,
146
+ repo: ctx.repo,
147
+ commentId: ctx.commentId,
148
+ reactionId: ctx.reactionId,
149
+ warnMessage: 'Failed to remove persist-logs reaction',
150
+ successMessage: 'Persist-logs reaction removed',
151
+ logMeta: {
152
+ task: 'persist-logs',
153
+ reactionId: ctx.reactionId,
154
+ },
155
+ });
156
+ }
157
+ logger.info('Persist-logs task finalized', {
158
+ task: 'persist-logs',
159
+ issueNumber: ctx.issueNumber,
160
+ durationMs: performance.now() - startedAt,
161
+ });
162
+ }
163
+ try {
164
+ const clients = await createInstallationOctokit(ctx);
165
+ const app = clients.app;
166
+ octokit = clients.octokit;
167
+ const [issue, comments] = await Promise.all([
168
+ fetchIssue(octokit, ctx),
169
+ fetchIssueComments(octokit, ctx),
170
+ ]);
171
+ const issueTitle = issue.title ?? 'Untitled issue';
172
+ const issueBody = issue.body ?? '';
173
+ logger.info('Issue context loaded', {
174
+ task: 'persist-logs',
175
+ issueNumber: ctx.issueNumber,
176
+ commentsCount: comments.length,
177
+ });
178
+ // Post initial progress comment with DD link (skip in dry-run)
179
+ let progressCommentId;
180
+ if (!ctx.dryRun) {
181
+ const datadogLink = buildDatadogLink(ctx.deliveryId);
182
+ const progressBody = datadogLink
183
+ ? `Investigating Datadog logs for this issue. Follow along [in Datadog](${datadogLink}).`
184
+ : 'Investigating Datadog logs for this issue.';
185
+ const progressComment = await octokit.rest.issues.createComment({
186
+ owner: ctx.owner,
187
+ repo: ctx.repo,
188
+ issue_number: ctx.issueNumber,
189
+ body: progressBody,
190
+ });
191
+ progressCommentId = progressComment.data.id;
192
+ }
193
+ // Resolve which repos to clone based on issue labels
194
+ const issueLabels = (issue.labels ?? [])
195
+ .map((l) => (typeof l === 'string' ? l : (l.name ?? '')))
196
+ .filter(Boolean);
197
+ const resolution = resolveIssueRepositories(issueLabels);
198
+ if (resolution.status === 'unresolved') {
199
+ logger.warn('Repository resolution failed', {
200
+ task: 'persist-logs',
201
+ reason: resolution.reason,
202
+ labels: issueLabels,
203
+ });
204
+ const msg = `Unable to determine which repository to investigate: ${resolution.reason}\n\nPlease add the appropriate labels (e.g. \`Product:Todoist\`, \`App:Backend\`) and try again.`;
205
+ if (ctx.dryRun) {
206
+ process.stdout.write(`${msg}\n`);
207
+ }
208
+ else {
209
+ if (progressCommentId)
210
+ await deleteComment(octokit, ctx, progressCommentId);
211
+ await postComment({ octokit, ctx, body: msg });
212
+ }
213
+ metricReason = 'no_repo';
214
+ return;
215
+ }
216
+ const repos = resolution.repositories;
217
+ logger.info('Repository resolution', {
218
+ task: 'persist-logs',
219
+ status: resolution.status,
220
+ reason: resolution.reason,
221
+ repos: repos.map((r) => r.fullName),
222
+ });
223
+ // Clone resolved repos — backend repos are most useful for log investigation
224
+ logger.info('Cloning repositories for code context', {
225
+ task: 'persist-logs',
226
+ repos: repos.map((r) => r.fullName),
227
+ });
228
+ const reposDir = path.join(WORKSPACE_DIR, REPOS_SUBDIR);
229
+ const clonedRepoDirs = [];
230
+ for (const repo of repos) {
231
+ const repoPath = path.join(reposDir, repo.repo);
232
+ try {
233
+ await cloneDefaultBranchToPath({
234
+ app,
235
+ owner: repo.owner,
236
+ repo: repo.repo,
237
+ installationId: ctx.installationId,
238
+ targetPath: repoPath,
239
+ depth: 1,
240
+ });
241
+ logger.info('Repository cloned', {
242
+ task: 'persist-logs',
243
+ repo: repo.fullName,
244
+ });
245
+ clonedRepoDirs.push(repoPath);
246
+ }
247
+ catch (cloneError) {
248
+ logger.warn('Failed to clone repo, continuing without it', {
249
+ task: 'persist-logs',
250
+ repo: repo.fullName,
251
+ error: cloneError instanceof Error ? cloneError.message : String(cloneError),
252
+ });
253
+ }
254
+ }
255
+ // Run LLM-driven investigation — pass the first cloned repo as primary context
256
+ logger.info('Starting LLM investigation against Datadog', {
257
+ task: 'persist-logs',
258
+ repoCount: clonedRepoDirs.length,
259
+ });
260
+ const primaryRepoDir = clonedRepoDirs[0] ?? WORKSPACE_DIR;
261
+ const outputDir = path.join(WORKSPACE_DIR, 'persist-logs-output');
262
+ const result = await runInvestigation({
263
+ openaiApiKey: ctx.openaiApiKey,
264
+ datadogApiKey: ctx.datadogApiKey,
265
+ datadogAppKey: ctx.datadogAppKey,
266
+ issueNumber: ctx.issueNumber,
267
+ issueTitle,
268
+ issueBody,
269
+ comments: comments.map((c) => ({
270
+ author: c.user?.login ?? 'unknown',
271
+ body: stripPresignedUrls(c.body ?? ''),
272
+ })),
273
+ repoDir: primaryRepoDir,
274
+ outputDir,
275
+ });
276
+ logger.info('LLM investigation finished', {
277
+ task: 'persist-logs',
278
+ success: result.success,
279
+ totalLogs: result.totalLogs,
280
+ });
281
+ if (!result.success) {
282
+ const msg = `Sorry, the log investigation failed: ${result.error ?? 'unknown error'}. Please try again or contact the team maintaining the bot.`;
283
+ logger.error('LLM investigation failed', {
284
+ task: 'persist-logs',
285
+ error: result.error,
286
+ });
287
+ if (ctx.dryRun) {
288
+ process.stdout.write(`${msg}\n`);
289
+ }
290
+ else {
291
+ if (progressCommentId)
292
+ await deleteComment(octokit, ctx, progressCommentId);
293
+ await postComment({ octokit, ctx, body: msg });
294
+ }
295
+ metricReason = 'llm_failed';
296
+ return;
297
+ }
298
+ if (result.totalLogs === 0) {
299
+ const msg = 'No Datadog logs found matching the issue context. The investigation ran but found no relevant logs in the 14-day retention window.';
300
+ if (ctx.dryRun) {
301
+ process.stdout.write(`${msg}\n`);
302
+ }
303
+ else {
304
+ if (progressCommentId)
305
+ await deleteComment(octokit, ctx, progressCommentId);
306
+ await postComment({ octokit, ctx, body: msg });
307
+ }
308
+ metricOutcome = 'success';
309
+ return;
310
+ }
311
+ // Read output files and scrub any residual PII
312
+ const analysisContent = scrubPii(result.analysisPath ? fs.readFileSync(result.analysisPath, 'utf-8') : '');
313
+ const csvContent = scrubPii(result.csvPath ? fs.readFileSync(result.csvPath, 'utf-8') : '');
314
+ const rawJsonContent = result.rawLogsPath
315
+ ? fs.readFileSync(result.rawLogsPath, 'utf-8')
316
+ : '{}';
317
+ const worklogContent = scrubPii(result.worklogPath ? fs.readFileSync(result.worklogPath, 'utf-8') : '');
318
+ if (ctx.dryRun) {
319
+ // Print outputs locally instead of uploading/posting
320
+ process.stdout.write('\n========== ANALYSIS ==========\n');
321
+ process.stdout.write(analysisContent || '(empty)\n');
322
+ process.stdout.write('\n========== WORKLOG ==========\n');
323
+ process.stdout.write(worklogContent || '(empty)\n');
324
+ const csvLines = csvContent.split('\n');
325
+ process.stdout.write(`\n========== CSV (${csvLines.length} lines, first 20) ==========\n`);
326
+ process.stdout.write(csvLines.slice(0, 20).join('\n') + '\n');
327
+ process.stdout.write(`\n========== RAW LOGS (${rawJsonContent.length} bytes) ==========\n`);
328
+ process.stdout.write(`(saved to ${result.rawLogsPath ?? 'N/A'})\n`);
329
+ logger.info('Persist-logs dry run completed', {
330
+ task: 'persist-logs',
331
+ issueNumber: ctx.issueNumber,
332
+ totalLogs: result.totalLogs,
333
+ durationMs: performance.now() - startedAt,
334
+ });
335
+ metricOutcome = 'success';
336
+ }
337
+ else {
338
+ // Upload to S3 and post snapshot comment
339
+ logger.info('Uploading results to S3', { task: 'persist-logs' });
340
+ const { s3Path, s3Keys, presignedUrls } = await uploadToS3({
341
+ bucket: ctx.logSnapshotBucket,
342
+ issueNumber: ctx.issueNumber,
343
+ analysisContent,
344
+ csvContent,
345
+ rawJsonContent,
346
+ worklogContent,
347
+ });
348
+ logger.info('Logs uploaded to S3', {
349
+ task: 'persist-logs',
350
+ issueNumber: ctx.issueNumber,
351
+ s3Path,
352
+ });
353
+ const snapshotComment = buildSnapshotComment({
354
+ analysisContent,
355
+ csvContent,
356
+ worklogContent,
357
+ totalLogs: result.totalLogs,
358
+ bucket: ctx.logSnapshotBucket,
359
+ s3Keys,
360
+ presignedUrls,
361
+ deliveryId: ctx.deliveryId,
362
+ });
363
+ await postComment({ octokit, ctx, body: snapshotComment });
364
+ // Clean up the progress comment now that the snapshot is posted
365
+ if (progressCommentId)
366
+ await deleteComment(octokit, ctx, progressCommentId);
367
+ logger.info('Persist-logs task completed', {
368
+ task: 'persist-logs',
369
+ issueNumber: ctx.issueNumber,
370
+ totalLogs: result.totalLogs,
371
+ s3Path,
372
+ durationMs: performance.now() - startedAt,
373
+ });
374
+ metricOutcome = 'success';
375
+ }
376
+ }
377
+ catch (error) {
378
+ metricReason = 'exception';
379
+ logger.error('Persist-logs task failed', {
380
+ task: 'persist-logs',
381
+ issueNumber: ctx.issueNumber,
382
+ error: error instanceof Error ? error.message : String(error),
383
+ durationMs: performance.now() - startedAt,
384
+ });
385
+ if (!ctx.dryRun && octokit) {
386
+ try {
387
+ await postComment({
388
+ octokit,
389
+ ctx,
390
+ body: 'Sorry, I encountered an error while capturing logs. Please try again or contact the team maintaining the bot.',
391
+ });
392
+ }
393
+ catch (commentError) {
394
+ logger.warn('Failed to post persist-logs failure response', {
395
+ task: 'persist-logs',
396
+ issueNumber: ctx.issueNumber,
397
+ error: commentError instanceof Error ? commentError.message : String(commentError),
398
+ });
399
+ }
400
+ }
401
+ exitCode = 1;
402
+ }
403
+ finally {
404
+ // Independent network targets (GitHub vs Datadog) — safe to run concurrently.
405
+ await Promise.all([
406
+ cleanup(),
407
+ recordTaskMetrics({
408
+ type: 'logs',
409
+ repository: `${ctx.owner}/${ctx.repo}`,
410
+ model: PERSIST_LOGS_PROVIDER,
411
+ outcome: metricOutcome,
412
+ durationMs: performance.now() - startedAt,
413
+ reason: metricReason,
414
+ }),
415
+ ]);
416
+ }
417
+ if (exitCode !== 0) {
418
+ await shutdownTracing();
419
+ process.exit(exitCode);
420
+ }
421
+ }
422
+ const MAX_CSV_PREVIEW_ROWS = 50;
423
+ export function buildSnapshotComment({ analysisContent, csvContent, worklogContent, totalLogs, bucket, s3Keys, presignedUrls, deliveryId, }) {
424
+ // Hidden metadata for /refresh-s3-urls to parse — flat list of all S3 keys
425
+ const allKeys = Object.values(s3Keys).filter(Boolean).join(',');
426
+ const s3Metadata = `<!-- s3-keys:${bucket}:${allKeys} -->`;
427
+ const linkItems = [];
428
+ if (presignedUrls.analysis) {
429
+ linkItems.push(`- [Full Analysis Report](${presignedUrls.analysis})`);
430
+ }
431
+ if (presignedUrls.csv) {
432
+ linkItems.push(`- [Full Logs CSV](${presignedUrls.csv}) (${totalLogs} entries)`);
433
+ }
434
+ if (presignedUrls.rawLogs) {
435
+ linkItems.push(`- [Raw Logs JSON](${presignedUrls.rawLogs})`);
436
+ }
437
+ if (presignedUrls.worklog) {
438
+ linkItems.push(`- [Investigation Worklog](${presignedUrls.worklog})`);
439
+ }
440
+ const linksSection = [
441
+ '### Archived Logs',
442
+ '',
443
+ ...linkItems,
444
+ '',
445
+ '_Links expire in 7 days. Use `@doistbot /refresh-s3-urls` to generate new links._',
446
+ ].join('\n');
447
+ const csvPreview = buildCsvPreview(csvContent);
448
+ // Always prepend the marker; model output won't contain it.
449
+ const analysis = analysisContent
450
+ ? `${LOG_SNAPSHOT_MARKER}\n${analysisContent}`
451
+ : `${LOG_SNAPSHOT_MARKER}\n## Datadog Log Snapshot\n\n**${totalLogs} log entries** captured from Datadog.`;
452
+ const sections = [s3Metadata, analysis];
453
+ if (worklogContent) {
454
+ sections.push('');
455
+ sections.push('<details>');
456
+ sections.push('<summary>Investigation worklog (queries and reasoning)</summary>');
457
+ sections.push('');
458
+ sections.push(worklogContent);
459
+ sections.push('');
460
+ sections.push('</details>');
461
+ }
462
+ if (csvPreview) {
463
+ sections.push('');
464
+ sections.push('<details>');
465
+ sections.push(`<summary>Top log entries (${Math.min(MAX_CSV_PREVIEW_ROWS, totalLogs)} of ${totalLogs})</summary>`);
466
+ sections.push('');
467
+ sections.push('```csv');
468
+ sections.push(csvPreview);
469
+ sections.push('```');
470
+ sections.push('</details>');
471
+ }
472
+ sections.push('');
473
+ sections.push(linksSection);
474
+ // Footer with DD logs link
475
+ const ddLink = buildDatadogLink(deliveryId);
476
+ if (ddLink) {
477
+ sections.push('');
478
+ sections.push(`<sub>[Investigation Logs](${ddLink})</sub>`);
479
+ }
480
+ return sections.join('\n');
481
+ }
482
+ function buildCsvPreview(csvContent) {
483
+ if (!csvContent)
484
+ return null;
485
+ const lines = csvContent.split('\n').filter((l) => l.trim());
486
+ if (lines.length <= 1)
487
+ return null; // header only or empty
488
+ // Keep header + up to MAX_CSV_PREVIEW_ROWS data rows
489
+ const preview = lines.slice(0, MAX_CSV_PREVIEW_ROWS + 1);
490
+ return preview.join('\n');
491
+ }