@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.
- package/dist/actions/auth.js +78 -24
- package/dist/actions/doctor.js +14 -7
- package/dist/actions/review.js +67 -10
- package/dist/auth.js +51 -6
- package/dist/config.js +77 -5
- package/dist/env.js +1 -0
- package/dist/runtime.js +1 -1
- package/dist/terminal.js +2 -0
- package/package.json +4 -4
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +4 -8
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +8 -20
- package/sandbox/dist/core/check-run.js +16 -0
- package/sandbox/dist/core/datadog-metrics.js +6 -52
- package/sandbox/dist/core/logger-span-processor.js +78 -0
- package/sandbox/dist/core/pi.js +285 -174
- package/sandbox/dist/core/prompt-template.js +45 -3
- package/sandbox/dist/core/repository-map.js +24 -0
- package/sandbox/dist/core/tracing-setup.js +15 -0
- package/sandbox/dist/core/tracing.js +80 -0
- package/sandbox/dist/main.js +4 -1
- package/sandbox/dist/providers/credentials.js +12 -2
- package/sandbox/dist/providers/gemini.js +5 -1
- package/sandbox/dist/providers/helpers.js +63 -17
- package/sandbox/dist/providers/index.js +4 -0
- package/sandbox/dist/providers/openrouter-pi.js +25 -0
- package/sandbox/dist/providers/pi-review.js +19 -2
- package/sandbox/dist/tasks/chat/chat.js +173 -102
- package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +210 -185
- package/sandbox/dist/tasks/issue-summarize/model.js +120 -49
- package/sandbox/dist/tasks/issue-summarize/prompt.js +2 -2
- package/sandbox/dist/tasks/issue-summarize/summarize.js +104 -88
- package/sandbox/dist/tasks/issue-triage/clone.js +40 -0
- package/sandbox/dist/tasks/issue-triage/context.js +25 -1
- package/sandbox/dist/tasks/issue-triage/fix-attempt.js +33 -10
- package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +154 -101
- package/sandbox/dist/tasks/issue-triage/fix-loop/runners.js +15 -6
- package/sandbox/dist/tasks/issue-triage/hero-group-map.js +2 -0
- package/sandbox/dist/tasks/issue-triage/model.js +120 -36
- package/sandbox/dist/tasks/issue-triage/output.js +27 -0
- package/sandbox/dist/tasks/issue-triage/pr-creator.js +5 -4
- package/sandbox/dist/tasks/issue-triage/prompt.js +2 -2
- package/sandbox/dist/tasks/issue-triage/resolution.js +26 -0
- package/sandbox/dist/tasks/issue-triage/routing.js +9 -9
- package/sandbox/dist/tasks/issue-triage/side-effects.js +9 -0
- package/sandbox/dist/tasks/issue-triage/triage.js +290 -420
- package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +3 -3
- package/sandbox/dist/tasks/review/check-run.js +2 -1
- package/sandbox/dist/tasks/review/conversation-context.js +2 -1
- package/sandbox/dist/tasks/review/engines/dedupe.js +110 -44
- package/sandbox/dist/tasks/review/engines/multi-focus.js +254 -113
- package/sandbox/dist/tasks/review/engines/shared.js +43 -6
- package/sandbox/dist/tasks/review/github-comment-format.js +72 -10
- package/sandbox/dist/tasks/review/local.js +7 -4
- package/sandbox/dist/tasks/review/review-summary.js +48 -7
- package/sandbox/dist/tasks/review/review.js +267 -186
- package/sandbox/dist/tasks/review/runner.js +33 -7
- package/sandbox/dist/tasks/review/summary-model.js +189 -20
- package/sandbox/dist/tasks/review/thinking-level.js +4 -0
- package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +4 -8
- package/sandbox/node_modules/doistbot-repo-config/dist/index.js +8 -20
- 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.
|
|
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.
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
: 'Multi-focus dedupe request failed', {
|
|
181
|
-
provider: providerConfig.provider,
|
|
182
|
-
model: providerConfig.modelName,
|
|
247
|
+
logDedupeProviderUnavailable({
|
|
248
|
+
providerConfig,
|
|
183
249
|
attempt,
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
250
|
+
error: result.error ?? 'dedupe request failed',
|
|
251
|
+
shouldRetry,
|
|
252
|
+
timedOut: result.timedOut,
|
|
187
253
|
});
|
|
188
254
|
if (shouldRetry) {
|
|
189
255
|
await waitForDedupeRetry(attempt);
|