@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,230 @@
1
+ import { SpanStatusCode } from '@opentelemetry/api';
2
+ import { logger } from '../../core/logging.js';
3
+ import { buildPiImageContent, createPiInvoker, DEFAULT_GEMINI_FLASH_MODEL, DEFAULT_OPENROUTER_MODELS, } from '../../core/pi.js';
4
+ import { withPhase } from '../../core/tracing.js';
5
+ const ISSUE_SUMMARIZE_TIMEOUT_MS = 5 * 60 * 1000;
6
+ const ISSUE_SUMMARIZE_MULTIMODAL_MODEL = DEFAULT_GEMINI_FLASH_MODEL;
7
+ const SUMMARIZE_TEXT_PROVIDER = 'zai';
8
+ // Single source of truth: inherit the repo-wide Z.ai default so a default-model
9
+ // bump in core/pi.ts moves summarize too, and the executed model matches the
10
+ // recorded gen_ai.request.model.
11
+ const SUMMARIZE_TEXT_MODEL = DEFAULT_OPENROUTER_MODELS.zai;
12
+ const SUMMARIZE_MULTIMODAL_PROVIDER = 'gcp.gemini';
13
+ // One model span per actual model.invoke. The final summary uses
14
+ // `issue-summarize.model`; each chunk summary uses `issue-summarize.chunk`.
15
+ const SUMMARIZE_MODEL_PHASE = 'issue-summarize.model';
16
+ const SUMMARIZE_CHUNK_PHASE = 'issue-summarize.chunk';
17
+ export function createIssueSummarizeTextModel(apiKey) {
18
+ return createPiInvoker({
19
+ provider: 'zai',
20
+ apiKey,
21
+ piProviderId: 'openrouter',
22
+ cwd: process.cwd(),
23
+ capabilityPreset: 'read-only',
24
+ });
25
+ }
26
+ export function createIssueSummarizeMultimodalModel(apiKey) {
27
+ return createPiInvoker({
28
+ provider: 'gemini',
29
+ apiKey,
30
+ piProviderId: 'google',
31
+ cwd: process.cwd(),
32
+ capabilityPreset: 'read-only',
33
+ });
34
+ }
35
+ async function invokeIssueSummarizeTextModel({ model, prompt, phaseName, }) {
36
+ return withPhase(phaseName, {
37
+ 'gen_ai.operation.name': 'chat',
38
+ 'gen_ai.provider.name': SUMMARIZE_TEXT_PROVIDER,
39
+ 'gen_ai.request.model': SUMMARIZE_TEXT_MODEL,
40
+ }, async (span) => {
41
+ const result = await model.invoke({
42
+ prompt,
43
+ modelName: SUMMARIZE_TEXT_MODEL,
44
+ timeoutMs: ISSUE_SUMMARIZE_TIMEOUT_MS,
45
+ });
46
+ if (!result.ok) {
47
+ span.setStatus({ code: SpanStatusCode.ERROR, message: result.error });
48
+ logger.warn('Issue summarize model request failed', {
49
+ error: result.error,
50
+ timedOut: result.timedOut,
51
+ });
52
+ return null;
53
+ }
54
+ const output = result.output.trim() || null;
55
+ if (!output) {
56
+ span.setStatus({
57
+ code: SpanStatusCode.ERROR,
58
+ message: 'summarize model produced no output',
59
+ });
60
+ }
61
+ return output;
62
+ });
63
+ }
64
+ // One model span for the multimodal model.invoke (provider/model known at
65
+ // start). On failure it returns null and the caller falls back to a separate
66
+ // text request — which emits its own model span, so a fallback shows two real
67
+ // model calls rather than one.
68
+ async function invokeMultimodalSummarizeModel({ model, prompt, mediaImages, }) {
69
+ const mediaContext = mediaImages
70
+ .map((mediaImage, index) => [
71
+ `Media item ${index + 1} of ${mediaImages.length}`,
72
+ `URL: ${mediaImage.url}`,
73
+ `Source: ${mediaImage.sourceLabel}`,
74
+ `Author: ${mediaImage.author}`,
75
+ '',
76
+ mediaImage.sourceContext,
77
+ ].join('\n'))
78
+ .join('\n\n');
79
+ return withPhase(SUMMARIZE_MODEL_PHASE, {
80
+ 'gen_ai.operation.name': 'chat',
81
+ 'gen_ai.provider.name': SUMMARIZE_MULTIMODAL_PROVIDER,
82
+ 'gen_ai.request.model': ISSUE_SUMMARIZE_MULTIMODAL_MODEL,
83
+ }, async (span) => {
84
+ try {
85
+ const result = await model.invoke({
86
+ modelName: ISSUE_SUMMARIZE_MULTIMODAL_MODEL,
87
+ prompt: `${prompt}\n\n<media_context>\n${mediaContext}\n</media_context>`,
88
+ timeoutMs: ISSUE_SUMMARIZE_TIMEOUT_MS,
89
+ images: mediaImages.map((mediaImage) => buildPiImageContent({
90
+ dataBase64: mediaImage.dataBase64,
91
+ contentType: mediaImage.contentType,
92
+ })),
93
+ });
94
+ if (!result.ok) {
95
+ span.setStatus({ code: SpanStatusCode.ERROR, message: result.error });
96
+ logger.warn('Issue summarize multimodal request failed', {
97
+ error: result.error,
98
+ timedOut: result.timedOut,
99
+ mediaImages: mediaImages.length,
100
+ });
101
+ return null;
102
+ }
103
+ const text = result.output.trim();
104
+ if (!text) {
105
+ span.setStatus({
106
+ code: SpanStatusCode.ERROR,
107
+ message: 'multimodal summarize produced no output',
108
+ });
109
+ logger.warn('Issue summarize multimodal request returned empty output', {
110
+ mediaImages: mediaImages.length,
111
+ });
112
+ return null;
113
+ }
114
+ return text;
115
+ }
116
+ catch (error) {
117
+ const message = error instanceof Error ? error.message : String(error);
118
+ span.setStatus({ code: SpanStatusCode.ERROR, message });
119
+ logger.warn('Issue summarize multimodal request errored', {
120
+ error: message,
121
+ mediaImages: mediaImages.length,
122
+ });
123
+ return null;
124
+ }
125
+ });
126
+ }
127
+ export async function invokeIssueSummarizeModel({ textModel, multimodalModel, prompt, mediaImages, }) {
128
+ if (mediaImages.length === 0) {
129
+ logger.info('Using text-only summarize model request');
130
+ return await invokeIssueSummarizeTextModel({
131
+ model: textModel,
132
+ prompt,
133
+ phaseName: SUMMARIZE_MODEL_PHASE,
134
+ });
135
+ }
136
+ logger.info('Using multimodal summarize model request', {
137
+ mediaImages: mediaImages.length,
138
+ });
139
+ const multimodal = await invokeMultimodalSummarizeModel({
140
+ model: multimodalModel,
141
+ prompt,
142
+ mediaImages,
143
+ });
144
+ if (multimodal) {
145
+ return multimodal;
146
+ }
147
+ // Multimodal failed — fall back to a separate text request (own model span).
148
+ return await invokeIssueSummarizeTextModel({
149
+ model: textModel,
150
+ prompt,
151
+ phaseName: SUMMARIZE_MODEL_PHASE,
152
+ });
153
+ }
154
+ function buildChunkSummaryPrompt({ chunk, chunkIndex, selectedChunkCount, totalChunks, }) {
155
+ return `You are preparing an intermediate summary for a GitHub issue discussion.
156
+
157
+ Chunk ${chunkIndex + 1} of ${selectedChunkCount} selected chunks (from ${totalChunks} total chunks).
158
+
159
+ Summarize this chunk in markdown with these headings:
160
+
161
+ ## Key updates
162
+ ## Decisions
163
+ ## Open questions
164
+ ## Action items
165
+
166
+ Be concise and factual. If a section has no data, use "- None noted in this chunk."
167
+
168
+ <chunk_content>
169
+ ${chunk}
170
+ </chunk_content>`;
171
+ }
172
+ export async function summarizeChunks({ model, chunks, totalChunks, }) {
173
+ // Workflow phase: it orchestrates one model request PER chunk, so it is not
174
+ // itself a single gen-AI operation and carries no gen_ai.* attrs.
175
+ return withPhase('issue-summarize.chunks', {
176
+ 'chunks.selected': chunks.length,
177
+ 'chunks.total': totalChunks,
178
+ }, async (span) => summarizeChunksInner({ model, chunks, totalChunks }, span));
179
+ }
180
+ async function summarizeChunksInner({ model, chunks, totalChunks, }, span) {
181
+ const summaries = [];
182
+ let missingChunkSummaries = 0;
183
+ logger.info('Summarizing issue thread in chunks', {
184
+ selectedChunks: chunks.length,
185
+ totalChunks,
186
+ });
187
+ for (const [index, chunk] of chunks.entries()) {
188
+ const chunkPrompt = buildChunkSummaryPrompt({
189
+ chunk,
190
+ chunkIndex: index,
191
+ selectedChunkCount: chunks.length,
192
+ totalChunks,
193
+ });
194
+ const chunkSummary = await invokeIssueSummarizeTextModel({
195
+ model,
196
+ prompt: chunkPrompt,
197
+ phaseName: SUMMARIZE_CHUNK_PHASE,
198
+ });
199
+ if (chunkSummary?.trim()) {
200
+ summaries.push(`### Chunk ${index + 1}\n${chunkSummary.trim()}`);
201
+ }
202
+ else {
203
+ missingChunkSummaries += 1;
204
+ }
205
+ }
206
+ span.setAttributes({ 'chunks.summarized': summaries.length });
207
+ if (summaries.length === 0) {
208
+ // No chunk produced a model summary — we fall back to raw chunk content.
209
+ span.setStatus({
210
+ code: SpanStatusCode.ERROR,
211
+ message: 'chunk summarization produced no model output',
212
+ });
213
+ logger.warn('Chunk summarization returned no model output, using raw chunk content', {
214
+ selectedChunks: chunks.length,
215
+ totalChunks,
216
+ });
217
+ return chunks.map((chunk, index) => `### Chunk ${index + 1}\n${chunk}`).join('\n\n---\n\n');
218
+ }
219
+ if (missingChunkSummaries > 0) {
220
+ logger.warn('Some chunk summaries returned empty output', {
221
+ missingChunkSummaries,
222
+ selectedChunks: chunks.length,
223
+ });
224
+ }
225
+ logger.info('Chunk summarization completed', {
226
+ selectedChunks: chunks.length,
227
+ chunkSummaries: summaries.length,
228
+ });
229
+ return summaries.join('\n\n---\n\n');
230
+ }
@@ -0,0 +1,45 @@
1
+ export function buildFallbackSummary({ issue, entryCount, coverageNote, }) {
2
+ return [
3
+ '## Current state',
4
+ `Issue #${issue.number} (“${issue.title ?? 'Untitled issue'}”) was analyzed across ${entryCount} thread entries.`,
5
+ `${coverageNote}`,
6
+ '',
7
+ '## Key decisions',
8
+ '- No reliable decision extraction was available from the model output. Review the issue thread directly for decision details.',
9
+ '',
10
+ '## Open questions',
11
+ '- Open questions could not be extracted confidently from the model output.',
12
+ '',
13
+ '## Next action steps',
14
+ '- Re-run `/summarize` or review recent issue comments manually for a precise status update.',
15
+ ].join('\n');
16
+ }
17
+ export function stripVisualContextSection(summary) {
18
+ const withoutVisualContext = summary.replace(/\n*##\s*Visual context\b[\s\S]*?(?=\n##\s|$)/gi, '\n');
19
+ return withoutVisualContext.trim();
20
+ }
21
+ export function ensureStructuredSummary(summary) {
22
+ const requiredHeadings = [
23
+ '## Current state',
24
+ '## Key decisions',
25
+ '## Open questions',
26
+ '## Next action steps',
27
+ ];
28
+ const hasAllHeadings = requiredHeadings.every((heading) => summary.includes(heading));
29
+ if (hasAllHeadings) {
30
+ return summary.trim();
31
+ }
32
+ return [
33
+ '## Current state',
34
+ summary.trim(),
35
+ '',
36
+ '## Key decisions',
37
+ '- Not explicitly identified.',
38
+ '',
39
+ '## Open questions',
40
+ '- Not explicitly identified.',
41
+ '',
42
+ '## Next action steps',
43
+ '- Review the issue thread for concrete next actions.',
44
+ ].join('\n');
45
+ }
@@ -0,0 +1,11 @@
1
+ import { fillPromptTemplate, loadPromptTemplateFile, resolvePromptFile, } from '../../core/prompt-template.js';
2
+ const PROMPT_FILE = 'issue-summarize-prompt.md';
3
+ export function loadPromptTemplate() {
4
+ return loadPromptTemplateFile({
5
+ fileName: resolvePromptFile(import.meta.url, PROMPT_FILE),
6
+ promptLabel: 'issue summarize',
7
+ });
8
+ }
9
+ export function fillTemplate(template, variables) {
10
+ return fillPromptTemplate(template, variables);
11
+ }
@@ -0,0 +1,238 @@
1
+ import { SpanStatusCode } from '@opentelemetry/api';
2
+ import { App } from 'octokit';
3
+ import { fetchIssue, fetchIssueComments } from '../../core/github-issues.js';
4
+ import { logger } from '../../core/logging.js';
5
+ import { removeIssueCommentReactionIfPresent } from '../../core/reactions.js';
6
+ import { resolveWriteOctokit } from '../../core/shared.js';
7
+ import { withPhase } from '../../core/tracing.js';
8
+ import { shutdownTracing } from '../../core/tracing-setup.js';
9
+ import { parseSummarizeContext } from './context.js';
10
+ import { buildMediaPromptImages, extractMediaReferences, formatMediaContext, inspectMediaReferences, } from './media.js';
11
+ import { createIssueSummarizeMultimodalModel, createIssueSummarizeTextModel, invokeIssueSummarizeModel, summarizeChunks, } from './model.js';
12
+ import { buildFallbackSummary, ensureStructuredSummary, stripVisualContextSection, } from './output.js';
13
+ import { fillTemplate, loadPromptTemplate } from './prompt.js';
14
+ import { buildThreadEntries, prepareThreadPrompt } from './thread.js';
15
+ const SUMMARY_MARKER = '<!--doistbot-issue-summary-->';
16
+ export { parseSummarizeContext } from './context.js';
17
+ export { extractMediaReferences, inspectMediaReferences } from './media.js';
18
+ export { buildThreadEntries, prepareThreadPrompt } from './thread.js';
19
+ function summarizeMediaInspectionStats(mediaInspections) {
20
+ const stats = {
21
+ fetched: 0,
22
+ fetchFailed: 0,
23
+ notMedia: 0,
24
+ };
25
+ for (const inspection of mediaInspections) {
26
+ switch (inspection.status) {
27
+ case 'fetched':
28
+ stats.fetched += 1;
29
+ break;
30
+ case 'fetch_failed':
31
+ stats.fetchFailed += 1;
32
+ break;
33
+ case 'not_media':
34
+ stats.notMedia += 1;
35
+ break;
36
+ default:
37
+ break;
38
+ }
39
+ }
40
+ return stats;
41
+ }
42
+ async function summarizeIssue({ ctx, issue, entries, mediaReferences, mediaInspections, }) {
43
+ const template = loadPromptTemplate();
44
+ const threadPrompt = prepareThreadPrompt(entries);
45
+ const textModel = createIssueSummarizeTextModel(ctx.openRouterApiKey);
46
+ const multimodalModel = createIssueSummarizeMultimodalModel(ctx.geminiApiKey);
47
+ logger.info('Prepared summarize thread prompt', {
48
+ entryCount: entries.length,
49
+ chunked: threadPrompt.chunked,
50
+ selectedChunks: threadPrompt.chunks.length,
51
+ totalChunks: threadPrompt.totalChunks,
52
+ });
53
+ const threadContent = threadPrompt.chunked
54
+ ? await summarizeChunks({
55
+ model: textModel,
56
+ chunks: threadPrompt.chunks,
57
+ totalChunks: threadPrompt.totalChunks,
58
+ })
59
+ : (threadPrompt.chunks[0] ?? '');
60
+ const prompt = fillTemplate(template, {
61
+ REPO_FULL: `${ctx.owner}/${ctx.repo}`,
62
+ ISSUE_NUMBER: String(ctx.issueNumber),
63
+ ISSUE_TITLE: issue.title ?? '(No title)',
64
+ ISSUE_AUTHOR: issue.user?.login ?? 'unknown',
65
+ ISSUE_STATE: issue.state ?? 'unknown',
66
+ ISSUE_URL: issue.html_url ??
67
+ `https://github.com/${ctx.owner}/${ctx.repo}/issues/${ctx.issueNumber}`,
68
+ ISSUE_BODY: issue.body?.trim() || '(No issue description provided.)',
69
+ THREAD_COVERAGE: threadPrompt.coverageNote,
70
+ THREAD_CONTENT: threadContent,
71
+ MEDIA_CONTEXT: formatMediaContext(mediaInspections),
72
+ });
73
+ const mediaPromptImages = await buildMediaPromptImages(mediaReferences, mediaInspections);
74
+ logger.info('Prepared summarize media payload', {
75
+ mediaReferences: mediaReferences.length,
76
+ mediaInspections: mediaInspections.length,
77
+ mediaImagesIncluded: mediaPromptImages.length,
78
+ });
79
+ const summary = await invokeIssueSummarizeModel({
80
+ textModel,
81
+ multimodalModel,
82
+ prompt,
83
+ mediaImages: mediaPromptImages,
84
+ });
85
+ if (!summary?.trim()) {
86
+ logger.warn('Issue summarize model returned empty output, using fallback summary', {
87
+ issueNumber: ctx.issueNumber,
88
+ entryCount: entries.length,
89
+ });
90
+ return buildFallbackSummary({
91
+ issue,
92
+ entryCount: entries.length,
93
+ coverageNote: threadPrompt.coverageNote,
94
+ });
95
+ }
96
+ return ensureStructuredSummary(stripVisualContextSection(summary));
97
+ }
98
+ async function createGitHubClients(ctx) {
99
+ const app = new App({
100
+ appId: ctx.appId,
101
+ privateKey: ctx.privateKey,
102
+ });
103
+ const readOctokit = await app.getInstallationOctokit(ctx.installationId);
104
+ const writeOctokit = resolveWriteOctokit({
105
+ githubToken: ctx.githubToken,
106
+ fallbackOctokit: readOctokit,
107
+ });
108
+ return { readOctokit, writeOctokit };
109
+ }
110
+ async function main() {
111
+ const ctx = parseSummarizeContext();
112
+ logger.setBaseContext({
113
+ deliveryId: ctx.deliveryId ?? 'unknown',
114
+ repository: `${ctx.owner}/${ctx.repo}`,
115
+ issueNumber: ctx.issueNumber,
116
+ });
117
+ let exitCode = 0;
118
+ await withPhase('issue-summarize', {
119
+ task: 'issue-summarize',
120
+ 'gen_ai.operation.name': 'invoke_agent',
121
+ 'gen_ai.agent.name': 'doistbot.issue-summarize',
122
+ 'issue.number': ctx.issueNumber,
123
+ 'comment.id': ctx.commentId,
124
+ dry_run: ctx.dryRun,
125
+ }, async (span) => {
126
+ // Inside the span so auth/setup failures are captured by the root span.
127
+ const { readOctokit, writeOctokit } = await createGitHubClients(ctx);
128
+ try {
129
+ const issueParams = {
130
+ owner: ctx.owner,
131
+ repo: ctx.repo,
132
+ issueNumber: ctx.issueNumber,
133
+ mediaType: { format: 'full' },
134
+ };
135
+ const [issue, comments] = await Promise.all([
136
+ fetchIssue(readOctokit, issueParams),
137
+ fetchIssueComments(readOctokit, issueParams),
138
+ ]);
139
+ logger.info('Loaded issue summarize context', {
140
+ issueState: issue.state ?? 'unknown',
141
+ commentsCount: comments.length,
142
+ });
143
+ const entries = buildThreadEntries({
144
+ issue,
145
+ comments,
146
+ triggerCommentId: ctx.commentId,
147
+ });
148
+ logger.info('Prepared issue summarize thread entries', {
149
+ issueNumber: issue.number,
150
+ entryCount: entries.length,
151
+ });
152
+ const mediaReferences = extractMediaReferences(entries);
153
+ const mediaInspections = await inspectMediaReferences(mediaReferences);
154
+ const mediaInspectionStats = summarizeMediaInspectionStats(mediaInspections);
155
+ logger.info('Inspected issue summarize media references', {
156
+ mediaReferences: mediaReferences.length,
157
+ mediaInspections: mediaInspections.length,
158
+ mediaFetched: mediaInspectionStats.fetched,
159
+ mediaFetchFailed: mediaInspectionStats.fetchFailed,
160
+ mediaNotImage: mediaInspectionStats.notMedia,
161
+ });
162
+ const summary = await summarizeIssue({
163
+ ctx,
164
+ issue,
165
+ entries,
166
+ mediaReferences,
167
+ mediaInspections,
168
+ });
169
+ logger.info('Generated issue summary', {
170
+ issueNumber: ctx.issueNumber,
171
+ summaryChars: summary.length,
172
+ });
173
+ const summaryComment = `${SUMMARY_MARKER}\n\n${summary}`;
174
+ if (ctx.dryRun) {
175
+ logger.info('Dry run enabled, printing summarize output instead of posting comment', { issueNumber: ctx.issueNumber });
176
+ process.stdout.write(`${summaryComment}\n`);
177
+ }
178
+ else {
179
+ await writeOctokit.rest.issues.createComment({
180
+ owner: ctx.owner,
181
+ repo: ctx.repo,
182
+ issue_number: ctx.issueNumber,
183
+ body: summaryComment,
184
+ });
185
+ logger.info('Posted issue summary comment', {
186
+ issueNumber: ctx.issueNumber,
187
+ mediaReferences: mediaReferences.length,
188
+ });
189
+ }
190
+ }
191
+ catch (error) {
192
+ const message = error instanceof Error ? error.message : String(error);
193
+ logger.error('Issue summarize task failed', {
194
+ issueNumber: ctx.issueNumber,
195
+ error: message,
196
+ });
197
+ // Set the span status before the run exits; deferring process.exit
198
+ // until after withPhase lets phase.end flush.
199
+ span.setStatus({ code: SpanStatusCode.ERROR, message });
200
+ if (!ctx.dryRun) {
201
+ try {
202
+ await writeOctokit.rest.issues.createComment({
203
+ owner: ctx.owner,
204
+ repo: ctx.repo,
205
+ issue_number: ctx.issueNumber,
206
+ body: 'Sorry, I encountered an error while summarizing this issue. Please try again or contact the team maintaining the bot.',
207
+ });
208
+ }
209
+ catch (replyError) {
210
+ logger.warn('Failed to post summarize error response', {
211
+ issueNumber: ctx.issueNumber,
212
+ error: replyError instanceof Error
213
+ ? replyError.message
214
+ : String(replyError),
215
+ });
216
+ }
217
+ }
218
+ exitCode = 1;
219
+ }
220
+ finally {
221
+ await removeIssueCommentReactionIfPresent({
222
+ octokit: writeOctokit,
223
+ owner: ctx.owner,
224
+ repo: ctx.repo,
225
+ commentId: ctx.commentId,
226
+ reactionId: ctx.reactionId,
227
+ warnMessage: 'Failed to remove summarize reaction',
228
+ });
229
+ }
230
+ });
231
+ if (exitCode !== 0) {
232
+ await shutdownTracing();
233
+ process.exit(exitCode);
234
+ }
235
+ }
236
+ if (process.env.VITEST !== 'true') {
237
+ main();
238
+ }
@@ -0,0 +1,123 @@
1
+ import { isKnownDoistbotLogin } from '../../core/bot-logins.js';
2
+ const SUMMARY_MARKER = '<!--doistbot-issue-summary-->';
3
+ const SUMMARIZE_COMMAND_REGEX = /^@doistbot \/summarize$/i;
4
+ const MAX_SINGLE_PASS_CHARS = 50_000;
5
+ const CHUNK_TARGET_CHARS = 15_000;
6
+ const MAX_CHUNKS = 6;
7
+ const MAX_ENTRY_BODY_CHARS = 4_000;
8
+ function isBotLogin(login) {
9
+ if (!login) {
10
+ return false;
11
+ }
12
+ const normalized = login.toLowerCase();
13
+ return isKnownDoistbotLogin(normalized) || normalized.endsWith('[bot]');
14
+ }
15
+ export function buildThreadEntries({ issue, comments, triggerCommentId, }) {
16
+ const entries = [];
17
+ const issueBody = issue.body?.trim() || '(No issue description provided.)';
18
+ entries.push({
19
+ source: 'issue_body',
20
+ id: issue.number,
21
+ author: issue.user?.login ?? 'unknown',
22
+ createdAt: issue.created_at ?? 'unknown',
23
+ url: issue.html_url ?? '',
24
+ body: issueBody,
25
+ bodyHtml: issue.body_html ?? undefined,
26
+ });
27
+ for (const comment of comments) {
28
+ const body = comment.body ?? '';
29
+ const trimmedBody = body.trim();
30
+ if (!trimmedBody) {
31
+ continue;
32
+ }
33
+ if (triggerCommentId != null && comment.id === triggerCommentId) {
34
+ continue;
35
+ }
36
+ if (trimmedBody.includes(SUMMARY_MARKER)) {
37
+ continue;
38
+ }
39
+ if (SUMMARIZE_COMMAND_REGEX.test(trimmedBody)) {
40
+ continue;
41
+ }
42
+ if (isBotLogin(comment.user?.login)) {
43
+ continue;
44
+ }
45
+ entries.push({
46
+ source: 'comment',
47
+ id: comment.id,
48
+ author: comment.user?.login ?? 'unknown',
49
+ createdAt: comment.created_at ?? 'unknown',
50
+ url: comment.html_url ?? '',
51
+ body: trimmedBody,
52
+ bodyHtml: comment.body_html ?? undefined,
53
+ });
54
+ }
55
+ return entries;
56
+ }
57
+ function trimEntryBody(body) {
58
+ if (body.length <= MAX_ENTRY_BODY_CHARS) {
59
+ return body;
60
+ }
61
+ return `${body.slice(0, MAX_ENTRY_BODY_CHARS)}\n\n[...truncated for prompt size...]`;
62
+ }
63
+ function formatThreadEntry(entry, index) {
64
+ const source = entry.source === 'issue_body' ? 'Issue description' : `Comment #${entry.id}`;
65
+ const urlLine = entry.url ? `- URL: ${entry.url}\n` : '';
66
+ return [
67
+ `### Entry ${index + 1}`,
68
+ `- Source: ${source}`,
69
+ `- Author: ${entry.author}`,
70
+ `- Created: ${entry.createdAt}`,
71
+ `${urlLine}Body:`,
72
+ trimEntryBody(entry.body),
73
+ ].join('\n');
74
+ }
75
+ function chunkBlocks(blocks, targetChars) {
76
+ const chunks = [];
77
+ let current = '';
78
+ for (const block of blocks) {
79
+ const next = current ? `${current}\n\n---\n\n${block}` : block;
80
+ if (next.length > targetChars && current) {
81
+ chunks.push(current);
82
+ current = block;
83
+ }
84
+ else {
85
+ current = next;
86
+ }
87
+ }
88
+ if (current) {
89
+ chunks.push(current);
90
+ }
91
+ return chunks;
92
+ }
93
+ export function prepareThreadPrompt(entries, options = {}) {
94
+ const maxSinglePassChars = options.maxSinglePassChars ?? MAX_SINGLE_PASS_CHARS;
95
+ const chunkTargetChars = options.chunkTargetChars ?? CHUNK_TARGET_CHARS;
96
+ const maxChunks = options.maxChunks ?? MAX_CHUNKS;
97
+ const blocks = entries.map((entry, index) => formatThreadEntry(entry, index));
98
+ const fullContent = blocks.join('\n\n---\n\n');
99
+ if (fullContent.length <= maxSinglePassChars) {
100
+ return {
101
+ chunked: false,
102
+ coverageNote: 'Full issue history was included in a single summarization pass.',
103
+ chunks: [fullContent],
104
+ totalChunks: 1,
105
+ };
106
+ }
107
+ const generatedChunks = chunkBlocks(blocks, chunkTargetChars);
108
+ if (generatedChunks.length <= maxChunks) {
109
+ return {
110
+ chunked: true,
111
+ coverageNote: `Thread exceeded single-pass size limits, so it was summarized across ${generatedChunks.length} chunks before final synthesis.`,
112
+ chunks: generatedChunks,
113
+ totalChunks: generatedChunks.length,
114
+ };
115
+ }
116
+ const selectedChunks = [generatedChunks[0], ...generatedChunks.slice(-(maxChunks - 1))];
117
+ return {
118
+ chunked: true,
119
+ coverageNote: `Thread exceeded size limits (${generatedChunks.length} chunks). Summary synthesis used chunk 1 and the latest ${maxChunks - 1} chunks.`,
120
+ chunks: selectedChunks,
121
+ totalChunks: generatedChunks.length,
122
+ };
123
+ }
@@ -0,0 +1 @@
1
+ export {};