@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,291 @@
1
+ import { SpanStatusCode } from '@opentelemetry/api';
2
+ import { logger } from '../../../core/logging.js';
3
+ import { invokePiPrompt } from '../../../core/pi.js';
4
+ import { logPiProgressEvent } from '../../../core/pi-progress-logging.js';
5
+ import { genAiProviderName, withPhase } from '../../../core/tracing.js';
6
+ import { resolveReviewProviderCredential } from '../../../providers/credentials.js';
7
+ import { extractJsonFromResponse, readProviderErrorDetails } from '../../../providers/helpers.js';
8
+ import { isDedupeResponse } from '../../../providers/schemas.js';
9
+ const DEDUPE_TIMEOUT_MS = 5 * 60 * 1000;
10
+ const DEDUPE_PROVIDER = 'gemini';
11
+ const DEDUPE_MODEL_BY_PI_PROVIDER = {
12
+ google: 'gemini-3-flash-preview',
13
+ openrouter: 'google/gemini-3-flash-preview',
14
+ };
15
+ const DEDUPE_THINKING_LEVEL = 'medium';
16
+ const DEDUPE_MAX_ATTEMPTS = 2;
17
+ const DEDUPE_RETRY_DELAY_MS = 1_000;
18
+ const DEDUPE_SYSTEM_PROMPT = 'You are a JSON-only duplicate-finding classifier. Use only the findings JSON in the user prompt. Do not inspect files, do not search the repository, and do not use tools. Return only valid JSON matching the requested schema.';
19
+ const DEDUPE_PHASE = 'review.dedupe';
20
+ const DEDUPE_PROVIDER_UNAVAILABLE_LOG_TYPE = 'review.dedupe.provider_unavailable';
21
+ function getDedupeProviderRoute(providerConfig) {
22
+ return providerConfig.piProviderId ?? 'google';
23
+ }
24
+ function toDedupeProviderConfig(credential, piProviderId) {
25
+ return {
26
+ provider: DEDUPE_PROVIDER,
27
+ apiKey: credential.apiKey,
28
+ ...(piProviderId === 'google' ? {} : { piProviderId }),
29
+ modelName: DEDUPE_MODEL_BY_PI_PROVIDER[piProviderId] ?? DEDUPE_MODEL_BY_PI_PROVIDER.google,
30
+ };
31
+ }
32
+ function buildOpenRouterDedupeProviderConfig(credential) {
33
+ const resolvedCredential = resolveReviewProviderCredential(credential);
34
+ if (!resolvedCredential) {
35
+ return undefined;
36
+ }
37
+ return toDedupeProviderConfig(resolvedCredential, 'openrouter');
38
+ }
39
+ function buildDedupeProviderConfigs({ geminiCredential, openRouterCredential, }) {
40
+ const primaryPiProviderId = geminiCredential.piProviderId ?? 'google';
41
+ const primaryConfig = toDedupeProviderConfig(geminiCredential, primaryPiProviderId);
42
+ const openRouterConfig = primaryPiProviderId === 'openrouter'
43
+ ? undefined
44
+ : buildOpenRouterDedupeProviderConfig(openRouterCredential);
45
+ return openRouterConfig ? [primaryConfig, openRouterConfig] : [primaryConfig];
46
+ }
47
+ function logDedupeProviderUnavailable({ providerConfig, attempt, error, shouldRetry, timedOut, }) {
48
+ const details = readProviderErrorDetails(error);
49
+ function log(message, meta) {
50
+ if (shouldRetry) {
51
+ logger.warn(message, meta);
52
+ }
53
+ else {
54
+ logger.error(message, meta);
55
+ }
56
+ }
57
+ log('Multi-focus dedupe provider unavailable', {
58
+ type: DEDUPE_PROVIDER_UNAVAILABLE_LOG_TYPE,
59
+ provider: providerConfig.provider,
60
+ provider_route: getDedupeProviderRoute(providerConfig),
61
+ model: providerConfig.modelName,
62
+ attempt,
63
+ attempts_total: DEDUPE_MAX_ATTEMPTS,
64
+ retrying: shouldRetry,
65
+ error_code: details.errorCode,
66
+ status: details.status,
67
+ reason: details.reason,
68
+ error: details.rawError,
69
+ timed_out: timedOut ?? false,
70
+ });
71
+ }
72
+ function buildDedupePrompt(findings) {
73
+ const payload = {
74
+ findings: findings.map((finding) => ({
75
+ id: finding.id,
76
+ focus: finding.focus,
77
+ model: finding.sourceModel,
78
+ path: finding.path,
79
+ line: finding.line,
80
+ side: finding.side,
81
+ body: finding.body,
82
+ })),
83
+ };
84
+ return `You are identifying duplicate code review findings produced by multiple focused review passes.
85
+
86
+ Input findings JSON:
87
+ ${JSON.stringify(payload, null, 2)}
88
+
89
+ Group findings only when they describe the same underlying issue and would lead to materially the same fix.
90
+
91
+ Guidelines:
92
+ - Treat wording differences as duplicates if the root cause is the same.
93
+ - Treat nearby line references in the same file as duplicates if they refer to the same issue.
94
+ - Do not group related-but-distinct concerns.
95
+ - Every finding id may appear in at most one group.
96
+ - Do not include singleton groups.
97
+ - If there are no duplicates, return {"groups":[]}.
98
+
99
+ Return only valid JSON in this exact shape:
100
+ {"groups":[{"ids":["finding-1","finding-2"],"reason":"same issue"}]}`;
101
+ }
102
+ function parseDedupeResponse(raw) {
103
+ const parsed = JSON.parse(extractJsonFromResponse(raw));
104
+ if (!isDedupeResponse(parsed)) {
105
+ throw new Error('Dedupe output did not match schema');
106
+ }
107
+ return {
108
+ groups: parsed.groups
109
+ .filter((group) => Array.isArray(group?.ids))
110
+ .map((group) => ({
111
+ ids: Array.from(new Set(group.ids.filter((id) => typeof id === 'string' && id.length > 0))),
112
+ reason: typeof group.reason === 'string' ? group.reason : undefined,
113
+ }))
114
+ .filter((group) => group.ids.length >= 2),
115
+ };
116
+ }
117
+ function logDedupeProgress(providerConfig, event) {
118
+ logPiProgressEvent({
119
+ event,
120
+ messages: {
121
+ agentStart: 'Multi-focus dedupe agent started',
122
+ agentEnd: 'Multi-focus dedupe agent finished',
123
+ thinking: 'Multi-focus dedupe still thinking',
124
+ responding: 'Multi-focus dedupe streaming response',
125
+ toolStart: 'Multi-focus dedupe attempted to use a tool',
126
+ toolUpdate: 'Multi-focus dedupe tool still running',
127
+ toolEnd: 'Multi-focus dedupe tool finished',
128
+ },
129
+ meta: { provider: providerConfig.provider, model: providerConfig.modelName },
130
+ logResponding: true,
131
+ logTools: true,
132
+ toolLogLevel: 'warn',
133
+ });
134
+ }
135
+ function hasToolUsage(toolUsage) {
136
+ return toolUsage.some((tool) => tool.calls > 0);
137
+ }
138
+ async function waitForDedupeRetry(attempt) {
139
+ await new Promise((resolve) => setTimeout(resolve, DEDUPE_RETRY_DELAY_MS * attempt));
140
+ }
141
+ export function applyDedupeGroups(findings, groups) {
142
+ if (groups.length === 0) {
143
+ return findings;
144
+ }
145
+ const byId = new Map(findings.map((finding) => [finding.id, finding]));
146
+ const dropped = new Set();
147
+ const assigned = new Set();
148
+ for (const group of groups) {
149
+ const existingIds = Array.from(new Set(group.ids)).filter((id) => byId.has(id) && !assigned.has(id));
150
+ if (existingIds.length < 2) {
151
+ continue;
152
+ }
153
+ const [keepId, ...rest] = existingIds;
154
+ assigned.add(keepId);
155
+ for (const id of rest) {
156
+ dropped.add(id);
157
+ assigned.add(id);
158
+ }
159
+ }
160
+ logger.info('Applying multi-focus dedupe groups', {
161
+ originalCount: findings.length,
162
+ groupsCount: groups.length,
163
+ droppedCount: dropped.size,
164
+ assignedCount: assigned.size,
165
+ });
166
+ return findings.filter((finding) => !dropped.has(finding.id));
167
+ }
168
+ export async function dedupeMultiFocusFindings({ findings, availableModels, workspaceDir, geminiCredential, openRouterCredential, }) {
169
+ if (findings.length <= 1) {
170
+ return findings;
171
+ }
172
+ if (!availableModels.includes(DEDUPE_PROVIDER)) {
173
+ logger.error('Multi-focus dedupe unavailable because Gemini is not enabled', {
174
+ provider: DEDUPE_PROVIDER,
175
+ model: DEDUPE_MODEL_BY_PI_PROVIDER.google,
176
+ });
177
+ throw new Error('Multi-focus dedupe requires Gemini to be enabled');
178
+ }
179
+ const credential = resolveReviewProviderCredential(geminiCredential);
180
+ if (!credential) {
181
+ logger.error('Multi-focus dedupe unavailable because Gemini review credential is unavailable', {
182
+ provider: DEDUPE_PROVIDER,
183
+ model: DEDUPE_MODEL_BY_PI_PROVIDER.google,
184
+ });
185
+ throw new Error('Multi-focus dedupe requires a Gemini review credential');
186
+ }
187
+ return runDedupeAttempts({
188
+ findings,
189
+ workspaceDir,
190
+ providerConfigs: buildDedupeProviderConfigs({
191
+ geminiCredential: credential,
192
+ openRouterCredential,
193
+ }),
194
+ });
195
+ }
196
+ async function runDedupeAttempts({ findings, workspaceDir, providerConfigs, }) {
197
+ for (let attempt = 1; attempt <= DEDUPE_MAX_ATTEMPTS; attempt += 1) {
198
+ const providerConfig = providerConfigs[Math.min(attempt - 1, providerConfigs.length - 1)];
199
+ let result;
200
+ try {
201
+ // One span per actual invoke: a fail-then-succeed retry shows up as a
202
+ // separate error span and ok span, not one inflated span. Provider/
203
+ // model are known here, so the span carries full gen_ai.*.
204
+ result = await withPhase(DEDUPE_PHASE, {
205
+ 'gen_ai.operation.name': 'chat',
206
+ 'gen_ai.provider.name': genAiProviderName(providerConfig.provider),
207
+ 'gen_ai.request.model': providerConfig.modelName,
208
+ 'review.provider_route': getDedupeProviderRoute(providerConfig),
209
+ }, async (span) => {
210
+ const invocation = await invokePiPrompt({
211
+ provider: providerConfig.provider,
212
+ apiKey: providerConfig.apiKey,
213
+ ...(providerConfig.piProviderId
214
+ ? { piProviderId: providerConfig.piProviderId }
215
+ : {}),
216
+ prompt: buildDedupePrompt(findings),
217
+ cwd: workspaceDir,
218
+ timeoutMs: DEDUPE_TIMEOUT_MS,
219
+ capabilityPreset: 'read-only',
220
+ modelName: providerConfig.modelName,
221
+ thinkingLevel: DEDUPE_THINKING_LEVEL,
222
+ systemPrompt: DEDUPE_SYSTEM_PROMPT,
223
+ onProgress: (event) => {
224
+ logDedupeProgress(providerConfig, event);
225
+ },
226
+ });
227
+ if (!invocation.ok) {
228
+ span.setStatus({
229
+ code: SpanStatusCode.ERROR,
230
+ message: invocation.error ?? 'dedupe request failed',
231
+ });
232
+ }
233
+ return invocation;
234
+ });
235
+ }
236
+ catch (error) {
237
+ const shouldRetry = attempt < DEDUPE_MAX_ATTEMPTS;
238
+ logDedupeProviderUnavailable({ providerConfig, attempt, error, shouldRetry });
239
+ if (shouldRetry) {
240
+ await waitForDedupeRetry(attempt);
241
+ continue;
242
+ }
243
+ throw new Error(`Multi-focus dedupe request threw: ${error instanceof Error ? error.message : String(error)}`);
244
+ }
245
+ if (!result.ok) {
246
+ const shouldRetry = attempt < DEDUPE_MAX_ATTEMPTS;
247
+ logDedupeProviderUnavailable({
248
+ providerConfig,
249
+ attempt,
250
+ error: result.error ?? 'dedupe request failed',
251
+ shouldRetry,
252
+ timedOut: result.timedOut,
253
+ });
254
+ if (shouldRetry) {
255
+ await waitForDedupeRetry(attempt);
256
+ continue;
257
+ }
258
+ throw new Error(`Multi-focus dedupe request failed: ${result.error}`);
259
+ }
260
+ if (hasToolUsage(result.toolUsage)) {
261
+ logger.error('Multi-focus dedupe used tools', {
262
+ provider: providerConfig.provider,
263
+ model: providerConfig.modelName,
264
+ tool_usage: result.toolUsage,
265
+ });
266
+ throw new Error('Multi-focus dedupe used tools unexpectedly');
267
+ }
268
+ let parsed;
269
+ try {
270
+ parsed = parseDedupeResponse(result.output);
271
+ }
272
+ catch (error) {
273
+ logger.error('Multi-focus dedupe parsing failed', {
274
+ provider: providerConfig.provider,
275
+ model: providerConfig.modelName,
276
+ error: error instanceof Error ? error.message : String(error),
277
+ });
278
+ throw new Error(`Multi-focus dedupe parsing failed: ${error instanceof Error ? error.message : String(error)}`);
279
+ }
280
+ const deduped = applyDedupeGroups(findings, parsed.groups);
281
+ logger.info('Multi-focus dedupe completed', {
282
+ provider: providerConfig.provider,
283
+ model: providerConfig.modelName,
284
+ originalCount: findings.length,
285
+ dedupedCount: deduped.length,
286
+ groupsCount: parsed.groups.length,
287
+ });
288
+ return deduped;
289
+ }
290
+ throw new Error('Multi-focus dedupe exhausted attempts without a result');
291
+ }