@doist/doistbot-cli 1.0.5 → 1.0.7

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 (61) hide show
  1. package/dist/actions/auth.js +78 -24
  2. package/dist/actions/doctor.js +14 -7
  3. package/dist/actions/review.js +67 -10
  4. package/dist/auth.js +51 -6
  5. package/dist/config.js +77 -5
  6. package/dist/env.js +1 -0
  7. package/dist/runtime.js +1 -1
  8. package/dist/terminal.js +2 -0
  9. package/package.json +4 -4
  10. package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +4 -8
  11. package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +8 -20
  12. package/sandbox/dist/core/check-run.js +16 -0
  13. package/sandbox/dist/core/datadog-metrics.js +6 -52
  14. package/sandbox/dist/core/logger-span-processor.js +78 -0
  15. package/sandbox/dist/core/pi.js +285 -174
  16. package/sandbox/dist/core/prompt-template.js +45 -3
  17. package/sandbox/dist/core/repository-map.js +24 -0
  18. package/sandbox/dist/core/tracing-setup.js +15 -0
  19. package/sandbox/dist/core/tracing.js +80 -0
  20. package/sandbox/dist/main.js +4 -1
  21. package/sandbox/dist/providers/credentials.js +12 -2
  22. package/sandbox/dist/providers/gemini.js +5 -1
  23. package/sandbox/dist/providers/helpers.js +63 -17
  24. package/sandbox/dist/providers/index.js +4 -0
  25. package/sandbox/dist/providers/openrouter-pi.js +25 -0
  26. package/sandbox/dist/providers/pi-review.js +19 -2
  27. package/sandbox/dist/tasks/chat/chat.js +173 -102
  28. package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +210 -185
  29. package/sandbox/dist/tasks/issue-summarize/model.js +120 -49
  30. package/sandbox/dist/tasks/issue-summarize/prompt.js +2 -2
  31. package/sandbox/dist/tasks/issue-summarize/summarize.js +104 -88
  32. package/sandbox/dist/tasks/issue-triage/clone.js +40 -0
  33. package/sandbox/dist/tasks/issue-triage/context.js +25 -1
  34. package/sandbox/dist/tasks/issue-triage/fix-attempt.js +33 -10
  35. package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +154 -101
  36. package/sandbox/dist/tasks/issue-triage/fix-loop/runners.js +15 -6
  37. package/sandbox/dist/tasks/issue-triage/hero-group-map.js +2 -0
  38. package/sandbox/dist/tasks/issue-triage/model.js +120 -36
  39. package/sandbox/dist/tasks/issue-triage/output.js +27 -0
  40. package/sandbox/dist/tasks/issue-triage/pr-creator.js +5 -4
  41. package/sandbox/dist/tasks/issue-triage/prompt.js +2 -2
  42. package/sandbox/dist/tasks/issue-triage/resolution.js +26 -0
  43. package/sandbox/dist/tasks/issue-triage/routing.js +9 -9
  44. package/sandbox/dist/tasks/issue-triage/side-effects.js +9 -0
  45. package/sandbox/dist/tasks/issue-triage/triage.js +290 -420
  46. package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +3 -3
  47. package/sandbox/dist/tasks/review/check-run.js +2 -1
  48. package/sandbox/dist/tasks/review/conversation-context.js +2 -1
  49. package/sandbox/dist/tasks/review/engines/dedupe.js +110 -44
  50. package/sandbox/dist/tasks/review/engines/multi-focus.js +254 -113
  51. package/sandbox/dist/tasks/review/engines/shared.js +43 -6
  52. package/sandbox/dist/tasks/review/github-comment-format.js +72 -10
  53. package/sandbox/dist/tasks/review/local.js +7 -4
  54. package/sandbox/dist/tasks/review/review-summary.js +48 -7
  55. package/sandbox/dist/tasks/review/review.js +267 -186
  56. package/sandbox/dist/tasks/review/runner.js +33 -7
  57. package/sandbox/dist/tasks/review/summary-model.js +189 -20
  58. package/sandbox/dist/tasks/review/thinking-level.js +4 -0
  59. package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +4 -8
  60. package/sandbox/node_modules/doistbot-repo-config/dist/index.js +8 -20
  61. package/sandbox/src/tasks/review/prompts/review-multi-focus-base-prompt.md +1 -0
@@ -3,17 +3,17 @@ import path from 'path';
3
3
  import { logger } from '../../core/logging.js';
4
4
  import { invokePiPrompt } from '../../core/pi.js';
5
5
  import { scrubPii } from '../../core/pii-scrubber.js';
6
- import { loadPromptTemplateFile } from '../../core/prompt-template.js';
6
+ import { loadPromptTemplateFile, resolvePromptFile } from '../../core/prompt-template.js';
7
7
  import { startDdProxy } from './dd-proxy.js';
8
8
  export const PERSIST_LOGS_PROVIDER = 'openai';
9
9
  const TIMEOUT_MS = 20 * 60 * 1000; // 20 minutes — investigation can take a while
10
10
  function buildPrompt(params, proxyPort) {
11
11
  const ddApiReference = loadPromptTemplateFile({
12
- fileName: 'persist-logs-dd-api-reference.md',
12
+ fileName: resolvePromptFile(import.meta.url, 'persist-logs-dd-api-reference.md'),
13
13
  promptLabel: 'persist-logs DD API reference',
14
14
  });
15
15
  const template = loadPromptTemplateFile({
16
- fileName: 'persist-logs-investigation-prompt.md',
16
+ fileName: resolvePromptFile(import.meta.url, 'persist-logs-investigation-prompt.md'),
17
17
  promptLabel: 'persist-logs investigation prompt',
18
18
  });
19
19
  const scrubbedTitle = scrubPii(params.issueTitle);
@@ -135,7 +135,7 @@ export function buildSuccessSummary(review, pullRequestUrl, counts = countFindin
135
135
  * PR conversation comment.
136
136
  */
137
137
  async function postReviewCheckRun(params) {
138
- const { octokit, owner, repo, sha, pullNumber, deliveryId, title, summaryBody } = params;
138
+ const { octokit, owner, repo, sha, pullNumber, deliveryId, checkRunId, title, summaryBody } = params;
139
139
  const pullRequestUrl = `https://github.com/${owner}/${repo}/pull/${pullNumber}`;
140
140
  const summary = appendReviewFeedbackLink(summaryBody, {
141
141
  owner,
@@ -153,6 +153,7 @@ async function postReviewCheckRun(params) {
153
153
  detailsUrl: pullRequestUrl,
154
154
  name: CHECK_RUN_NAME,
155
155
  externalId: deliveryId,
156
+ checkRunId,
156
157
  };
157
158
  if (params.status === 'in_progress') {
158
159
  await postCheckRun({ ...sharedArgs, status: 'in_progress' });
@@ -4,6 +4,7 @@ import { logger } from '../../core/logging.js';
4
4
  import { invokePiPrompt } from '../../core/pi.js';
5
5
  import { WORKSPACE_DIR } from '../../core/shared.js';
6
6
  import { resolveReviewProviderCredential } from '../../providers/credentials.js';
7
+ import { normalizeGithubPriorityBadges } from './github-comment-format.js';
7
8
  import { stripReviewFeedbackFooter } from './review-feedback.js';
8
9
  // Roughly 6k tokens: large enough for substantive PR discussions while still
9
10
  // keeping conversation context secondary to the diff.
@@ -36,7 +37,7 @@ function shouldMaskPrAuthor(login, prAuthor) {
36
37
  return Boolean(normalizedLogin && normalizedPrAuthor && normalizedLogin === normalizedPrAuthor);
37
38
  }
38
39
  function cleanBody(body) {
39
- return stripReviewFeedbackFooter(body);
40
+ return normalizeGithubPriorityBadges(stripReviewFeedbackFooter(body));
40
41
  }
41
42
  function toIssueCommentEntry(comment) {
42
43
  const author = comment.user?.login ?? 'unknown';
@@ -1,18 +1,74 @@
1
+ import { SpanStatusCode } from '@opentelemetry/api';
1
2
  import { logger } from '../../../core/logging.js';
2
3
  import { invokePiPrompt } from '../../../core/pi.js';
3
4
  import { logPiProgressEvent } from '../../../core/pi-progress-logging.js';
5
+ import { genAiProviderName, withPhase } from '../../../core/tracing.js';
4
6
  import { resolveReviewProviderCredential } from '../../../providers/credentials.js';
5
- import { extractJsonFromResponse } from '../../../providers/helpers.js';
7
+ import { extractJsonFromResponse, readProviderErrorDetails } from '../../../providers/helpers.js';
6
8
  import { isDedupeResponse } from '../../../providers/schemas.js';
7
9
  const DEDUPE_TIMEOUT_MS = 5 * 60 * 1000;
8
10
  const DEDUPE_PROVIDER = 'gemini';
9
11
  const DEDUPE_MODEL_BY_PI_PROVIDER = {
12
+ google: 'gemini-3-flash-preview',
10
13
  openrouter: 'google/gemini-3-flash-preview',
11
14
  };
12
15
  const DEDUPE_THINKING_LEVEL = 'medium';
13
16
  const DEDUPE_MAX_ATTEMPTS = 2;
14
17
  const DEDUPE_RETRY_DELAY_MS = 1_000;
15
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
+ }
16
72
  function buildDedupePrompt(findings) {
17
73
  const payload = {
18
74
  findings: findings.map((finding) => ({
@@ -109,14 +165,14 @@ export function applyDedupeGroups(findings, groups) {
109
165
  });
110
166
  return findings.filter((finding) => !dropped.has(finding.id));
111
167
  }
112
- export async function dedupeMultiFocusFindings({ findings, availableModels, workspaceDir, geminiCredential, }) {
168
+ export async function dedupeMultiFocusFindings({ findings, availableModels, workspaceDir, geminiCredential, openRouterCredential, }) {
113
169
  if (findings.length <= 1) {
114
170
  return findings;
115
171
  }
116
172
  if (!availableModels.includes(DEDUPE_PROVIDER)) {
117
173
  logger.error('Multi-focus dedupe unavailable because Gemini is not enabled', {
118
174
  provider: DEDUPE_PROVIDER,
119
- model: DEDUPE_MODEL_BY_PI_PROVIDER.openrouter,
175
+ model: DEDUPE_MODEL_BY_PI_PROVIDER.google,
120
176
  });
121
177
  throw new Error('Multi-focus dedupe requires Gemini to be enabled');
122
178
  }
@@ -124,49 +180,62 @@ export async function dedupeMultiFocusFindings({ findings, availableModels, work
124
180
  if (!credential) {
125
181
  logger.error('Multi-focus dedupe unavailable because Gemini review credential is unavailable', {
126
182
  provider: DEDUPE_PROVIDER,
127
- model: DEDUPE_MODEL_BY_PI_PROVIDER.openrouter,
183
+ model: DEDUPE_MODEL_BY_PI_PROVIDER.google,
128
184
  });
129
185
  throw new Error('Multi-focus dedupe requires a Gemini review credential');
130
186
  }
131
- const piProviderId = credential.piProviderId ?? 'openrouter';
132
- const providerConfig = {
133
- provider: DEDUPE_PROVIDER,
134
- apiKey: credential.apiKey,
135
- ...(credential.piProviderId ? { piProviderId: credential.piProviderId } : {}),
136
- modelName: DEDUPE_MODEL_BY_PI_PROVIDER[piProviderId] ?? DEDUPE_MODEL_BY_PI_PROVIDER.openrouter,
137
- };
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, }) {
138
197
  for (let attempt = 1; attempt <= DEDUPE_MAX_ATTEMPTS; attempt += 1) {
198
+ const providerConfig = providerConfigs[Math.min(attempt - 1, providerConfigs.length - 1)];
139
199
  let result;
140
200
  try {
141
- result = await invokePiPrompt({
142
- provider: providerConfig.provider,
143
- apiKey: providerConfig.apiKey,
144
- ...(providerConfig.piProviderId
145
- ? { piProviderId: providerConfig.piProviderId }
146
- : {}),
147
- prompt: buildDedupePrompt(findings),
148
- cwd: workspaceDir,
149
- timeoutMs: DEDUPE_TIMEOUT_MS,
150
- capabilityPreset: 'read-only',
151
- modelName: providerConfig.modelName,
152
- thinkingLevel: DEDUPE_THINKING_LEVEL,
153
- systemPrompt: DEDUPE_SYSTEM_PROMPT,
154
- onProgress: (event) => {
155
- logDedupeProgress(providerConfig, event);
156
- },
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;
157
234
  });
158
235
  }
159
236
  catch (error) {
160
237
  const shouldRetry = attempt < DEDUPE_MAX_ATTEMPTS;
161
- logger.warn(shouldRetry
162
- ? 'Multi-focus dedupe request threw, retrying'
163
- : 'Multi-focus dedupe request threw', {
164
- provider: providerConfig.provider,
165
- model: providerConfig.modelName,
166
- attempt,
167
- attempts_total: DEDUPE_MAX_ATTEMPTS,
168
- error: error instanceof Error ? error.message : String(error),
169
- });
238
+ logDedupeProviderUnavailable({ providerConfig, attempt, error, shouldRetry });
170
239
  if (shouldRetry) {
171
240
  await waitForDedupeRetry(attempt);
172
241
  continue;
@@ -175,15 +244,12 @@ export async function dedupeMultiFocusFindings({ findings, availableModels, work
175
244
  }
176
245
  if (!result.ok) {
177
246
  const shouldRetry = attempt < DEDUPE_MAX_ATTEMPTS;
178
- logger.warn(shouldRetry
179
- ? 'Multi-focus dedupe request failed, retrying'
180
- : 'Multi-focus dedupe request failed', {
181
- provider: providerConfig.provider,
182
- model: providerConfig.modelName,
247
+ logDedupeProviderUnavailable({
248
+ providerConfig,
183
249
  attempt,
184
- attempts_total: DEDUPE_MAX_ATTEMPTS,
185
- error: result.error,
186
- timed_out: result.timedOut,
250
+ error: result.error ?? 'dedupe request failed',
251
+ shouldRetry,
252
+ timedOut: result.timedOut,
187
253
  });
188
254
  if (shouldRetry) {
189
255
  await waitForDedupeRetry(attempt);