@doist/doistbot-cli 1.0.4 → 1.0.6
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 +62 -28
- package/dist/actions/doctor.js +16 -11
- package/dist/actions/review.js +77 -11
- package/dist/auth.js +53 -8
- package/dist/config.js +40 -6
- package/dist/env.js +1 -1
- package/dist/file-store.js +1 -1
- package/dist/runtime.js +1 -1
- package/dist/terminal.js +2 -7
- package/package.json +7 -7
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +5 -23
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +9 -85
- package/sandbox/dist/core/bot-logins.js +0 -13
- package/sandbox/dist/core/check-run.js +16 -0
- package/sandbox/dist/core/datadog-metrics.js +6 -54
- package/sandbox/dist/core/logger-span-processor.js +78 -0
- package/sandbox/dist/core/pi.js +270 -186
- package/sandbox/dist/core/tracing-setup.js +15 -0
- package/sandbox/dist/core/tracing.js +71 -0
- package/sandbox/dist/main.js +4 -1
- package/sandbox/dist/providers/config.js +1 -1
- package/sandbox/dist/providers/credentials.js +40 -0
- package/sandbox/dist/providers/gemini.js +8 -10
- package/sandbox/dist/providers/helpers.js +18 -38
- package/sandbox/dist/providers/index.js +4 -3
- package/sandbox/dist/providers/openai.js +1 -8
- package/sandbox/dist/providers/openrouter-pi.js +25 -0
- package/sandbox/dist/providers/pi-review.js +28 -55
- package/sandbox/dist/providers/schemas.js +0 -39
- package/sandbox/dist/tasks/chat/chat.js +162 -101
- package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +210 -185
- package/sandbox/dist/tasks/issue-summarize/context.js +1 -1
- package/sandbox/dist/tasks/issue-summarize/model.js +4 -2
- package/sandbox/dist/tasks/issue-summarize/summarize.js +1 -1
- package/sandbox/dist/tasks/issue-triage/clone.js +40 -0
- package/sandbox/dist/tasks/issue-triage/context.js +25 -2
- package/sandbox/dist/tasks/issue-triage/fix-attempt.js +31 -8
- 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/model.js +120 -36
- package/sandbox/dist/tasks/issue-triage/output.js +27 -0
- package/sandbox/dist/tasks/issue-triage/pr-creator.js +3 -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 +288 -418
- package/sandbox/dist/tasks/persist-logs/dd-proxy.js +2 -2
- package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +1 -1
- package/sandbox/dist/tasks/persist-logs/persist-logs.js +1 -1
- package/sandbox/dist/tasks/review/check-run.js +2 -1
- package/sandbox/dist/tasks/review/conversation-context.js +15 -8
- package/sandbox/dist/tasks/review/engines/dedupe.js +19 -9
- package/sandbox/dist/tasks/review/engines/multi-focus.js +62 -36
- package/sandbox/dist/tasks/review/engines/shared.js +15 -61
- package/sandbox/dist/tasks/review/github-comment-format.js +64 -10
- package/sandbox/dist/tasks/review/local.js +2 -29
- package/sandbox/dist/tasks/review/prompt.js +2 -49
- package/sandbox/dist/tasks/review/review-summary.js +120 -0
- package/sandbox/dist/tasks/review/review.js +49 -69
- package/sandbox/dist/tasks/review/runner.js +57 -52
- package/sandbox/dist/tasks/review/summary-model.js +172 -16
- package/sandbox/dist/tasks/review/thinking-level.js +4 -2
- package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +5 -23
- package/sandbox/node_modules/doistbot-repo-config/dist/index.js +9 -85
- package/sandbox/_node_modules/doistbot-repo-config/dist/review-engine.d.ts +0 -11
- package/sandbox/_node_modules/doistbot-repo-config/dist/review-engine.js +0 -41
- package/sandbox/dist/providers/anthropic.js +0 -30
- package/sandbox/dist/tasks/review/author-format.js +0 -14
- package/sandbox/dist/tasks/review/author-profile.js +0 -67
- package/sandbox/dist/tasks/review/council/normalizer.js +0 -104
- package/sandbox/dist/tasks/review/council/orchestrator.js +0 -194
- package/sandbox/dist/tasks/review/council/provider-health.js +0 -31
- package/sandbox/dist/tasks/review/council/summary.js +0 -192
- package/sandbox/dist/tasks/review/council/types.js +0 -4
- package/sandbox/dist/tasks/review/council/voter.js +0 -384
- package/sandbox/dist/tasks/review/engines/council.js +0 -126
- package/sandbox/dist/tasks/review/engines/index.js +0 -12
- package/sandbox/dist/tasks/review/review-engine.js +0 -5
- package/sandbox/node_modules/doistbot-repo-config/dist/review-engine.d.ts +0 -11
- package/sandbox/node_modules/doistbot-repo-config/dist/review-engine.js +0 -41
- package/sandbox/review-prompt.md +0 -209
- package/sandbox/vote-prompt.md +0 -69
|
@@ -7,8 +7,8 @@ const DD_HOST = 'api.datadoghq.com';
|
|
|
7
7
|
* Starts a localhost HTTP proxy that forwards requests to the Datadog API,
|
|
8
8
|
* scrubs PII from responses, and logs each query for auditability.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
* The proxy injects DD auth headers so
|
|
10
|
+
* The agent hits http://localhost:{port}/api/v2/... instead of the DD API directly.
|
|
11
|
+
* The proxy injects DD auth headers so the agent never sees the API keys.
|
|
12
12
|
*/
|
|
13
13
|
export function startDdProxy({ port, datadogApiKey, datadogAppKey }) {
|
|
14
14
|
return new Promise((resolve, reject) => {
|
|
@@ -49,7 +49,7 @@ export async function runInvestigation(params) {
|
|
|
49
49
|
});
|
|
50
50
|
// Ensure output directory exists
|
|
51
51
|
fs.mkdirSync(params.outputDir, { recursive: true });
|
|
52
|
-
// Start the DD proxy
|
|
52
|
+
// Start the DD proxy so the agent queries localhost; the proxy injects auth and scrubs PII.
|
|
53
53
|
const proxy = await startDdProxy({
|
|
54
54
|
port: DD_PROXY_PORT,
|
|
55
55
|
datadogApiKey: params.datadogApiKey,
|
|
@@ -441,7 +441,7 @@ export function buildSnapshotComment({ analysisContent, csvContent, worklogConte
|
|
|
441
441
|
'_Links expire in 7 days. Use `@doistbot /refresh-s3-urls` to generate new links._',
|
|
442
442
|
].join('\n');
|
|
443
443
|
const csvPreview = buildCsvPreview(csvContent);
|
|
444
|
-
// Always prepend the marker
|
|
444
|
+
// Always prepend the marker; model output won't contain it.
|
|
445
445
|
const analysis = analysisContent
|
|
446
446
|
? `${LOG_SNAPSHOT_MARKER}\n${analysisContent}`
|
|
447
447
|
: `${LOG_SNAPSHOT_MARKER}\n## Datadog Log Snapshot\n\n**${totalLogs} log entries** captured from Datadog.`;
|
|
@@ -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' });
|
|
@@ -3,6 +3,8 @@ import { fetchIssueComments } from '../../core/github-issues.js';
|
|
|
3
3
|
import { logger } from '../../core/logging.js';
|
|
4
4
|
import { invokePiPrompt } from '../../core/pi.js';
|
|
5
5
|
import { WORKSPACE_DIR } from '../../core/shared.js';
|
|
6
|
+
import { resolveReviewProviderCredential } from '../../providers/credentials.js';
|
|
7
|
+
import { normalizeGithubPriorityBadges } from './github-comment-format.js';
|
|
6
8
|
import { stripReviewFeedbackFooter } from './review-feedback.js';
|
|
7
9
|
// Roughly 6k tokens: large enough for substantive PR discussions while still
|
|
8
10
|
// keeping conversation context secondary to the diff.
|
|
@@ -14,11 +16,8 @@ const GITHUB_ACTIONS_BOT_LOGIN = 'github-actions[bot]';
|
|
|
14
16
|
const MASKED_PR_AUTHOR_LABEL = 'PR author';
|
|
15
17
|
function getSummaryProviders(apiKeys) {
|
|
16
18
|
return [
|
|
17
|
-
...(apiKeys?.gemini ? [{ provider: 'gemini',
|
|
18
|
-
...(apiKeys?.openai ? [{ provider: 'openai',
|
|
19
|
-
...(apiKeys?.anthropic
|
|
20
|
-
? [{ provider: 'anthropic', apiKey: apiKeys.anthropic }]
|
|
21
|
-
: []),
|
|
19
|
+
...(apiKeys?.gemini ? [{ provider: 'gemini', credential: apiKeys.gemini }] : []),
|
|
20
|
+
...(apiKeys?.openai ? [{ provider: 'openai', credential: apiKeys.openai }] : []),
|
|
22
21
|
];
|
|
23
22
|
}
|
|
24
23
|
function isRelevantBotLogin(login) {
|
|
@@ -38,7 +37,7 @@ function shouldMaskPrAuthor(login, prAuthor) {
|
|
|
38
37
|
return Boolean(normalizedLogin && normalizedPrAuthor && normalizedLogin === normalizedPrAuthor);
|
|
39
38
|
}
|
|
40
39
|
function cleanBody(body) {
|
|
41
|
-
return stripReviewFeedbackFooter(body);
|
|
40
|
+
return normalizeGithubPriorityBadges(stripReviewFeedbackFooter(body));
|
|
42
41
|
}
|
|
43
42
|
function toIssueCommentEntry(comment) {
|
|
44
43
|
const author = comment.user?.login ?? 'unknown';
|
|
@@ -253,7 +252,7 @@ If a section has no relevant information, use "- None."
|
|
|
253
252
|
${rawContext}
|
|
254
253
|
</pr_conversation>`;
|
|
255
254
|
}
|
|
256
|
-
async function summarizeConversationContextWithModel({ provider,
|
|
255
|
+
async function summarizeConversationContextWithModel({ provider, credential, rawContext, workspaceDir, }) {
|
|
257
256
|
const startedAt = Date.now();
|
|
258
257
|
logger.info('PR conversation summarization started', {
|
|
259
258
|
task: 'review',
|
|
@@ -261,9 +260,17 @@ async function summarizeConversationContextWithModel({ provider, apiKey, rawCont
|
|
|
261
260
|
raw_chars: rawContext.length,
|
|
262
261
|
});
|
|
263
262
|
try {
|
|
263
|
+
const resolvedCredential = resolveReviewProviderCredential(credential);
|
|
264
|
+
if (!resolvedCredential) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
264
267
|
const result = await invokePiPrompt({
|
|
265
268
|
provider,
|
|
266
|
-
apiKey,
|
|
269
|
+
apiKey: resolvedCredential.apiKey,
|
|
270
|
+
...(resolvedCredential.piProviderId
|
|
271
|
+
? { piProviderId: resolvedCredential.piProviderId }
|
|
272
|
+
: {}),
|
|
273
|
+
...(resolvedCredential.modelName ? { modelName: resolvedCredential.modelName } : {}),
|
|
267
274
|
prompt: buildSummaryPrompt(rawContext),
|
|
268
275
|
cwd: workspaceDir ?? WORKSPACE_DIR,
|
|
269
276
|
timeoutMs: CONVERSATION_SUMMARY_TIMEOUT_MS,
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { logger } from '../../../core/logging.js';
|
|
2
2
|
import { invokePiPrompt } from '../../../core/pi.js';
|
|
3
3
|
import { logPiProgressEvent } from '../../../core/pi-progress-logging.js';
|
|
4
|
+
import { resolveReviewProviderCredential } from '../../../providers/credentials.js';
|
|
4
5
|
import { extractJsonFromResponse } from '../../../providers/helpers.js';
|
|
5
6
|
import { isDedupeResponse } from '../../../providers/schemas.js';
|
|
6
7
|
const DEDUPE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
7
8
|
const DEDUPE_PROVIDER = 'gemini';
|
|
8
|
-
const
|
|
9
|
+
const DEDUPE_MODEL_BY_PI_PROVIDER = {
|
|
10
|
+
google: 'gemini-3-flash-preview',
|
|
11
|
+
openrouter: 'google/gemini-3-flash-preview',
|
|
12
|
+
};
|
|
9
13
|
const DEDUPE_THINKING_LEVEL = 'medium';
|
|
10
14
|
const DEDUPE_MAX_ATTEMPTS = 2;
|
|
11
15
|
const DEDUPE_RETRY_DELAY_MS = 1_000;
|
|
@@ -106,28 +110,31 @@ export function applyDedupeGroups(findings, groups) {
|
|
|
106
110
|
});
|
|
107
111
|
return findings.filter((finding) => !dropped.has(finding.id));
|
|
108
112
|
}
|
|
109
|
-
export async function dedupeMultiFocusFindings({ findings, availableModels, workspaceDir,
|
|
113
|
+
export async function dedupeMultiFocusFindings({ findings, availableModels, workspaceDir, geminiCredential, }) {
|
|
110
114
|
if (findings.length <= 1) {
|
|
111
115
|
return findings;
|
|
112
116
|
}
|
|
113
117
|
if (!availableModels.includes(DEDUPE_PROVIDER)) {
|
|
114
118
|
logger.error('Multi-focus dedupe unavailable because Gemini is not enabled', {
|
|
115
119
|
provider: DEDUPE_PROVIDER,
|
|
116
|
-
model:
|
|
120
|
+
model: DEDUPE_MODEL_BY_PI_PROVIDER.google,
|
|
117
121
|
});
|
|
118
122
|
throw new Error('Multi-focus dedupe requires Gemini to be enabled');
|
|
119
123
|
}
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
const credential = resolveReviewProviderCredential(geminiCredential);
|
|
125
|
+
if (!credential) {
|
|
126
|
+
logger.error('Multi-focus dedupe unavailable because Gemini review credential is unavailable', {
|
|
122
127
|
provider: DEDUPE_PROVIDER,
|
|
123
|
-
model:
|
|
128
|
+
model: DEDUPE_MODEL_BY_PI_PROVIDER.google,
|
|
124
129
|
});
|
|
125
|
-
throw new Error('Multi-focus dedupe requires a Gemini
|
|
130
|
+
throw new Error('Multi-focus dedupe requires a Gemini review credential');
|
|
126
131
|
}
|
|
132
|
+
const piProviderId = credential.piProviderId ?? 'google';
|
|
127
133
|
const providerConfig = {
|
|
128
134
|
provider: DEDUPE_PROVIDER,
|
|
129
|
-
apiKey:
|
|
130
|
-
|
|
135
|
+
apiKey: credential.apiKey,
|
|
136
|
+
...(credential.piProviderId ? { piProviderId: credential.piProviderId } : {}),
|
|
137
|
+
modelName: DEDUPE_MODEL_BY_PI_PROVIDER[piProviderId] ?? DEDUPE_MODEL_BY_PI_PROVIDER.google,
|
|
131
138
|
};
|
|
132
139
|
for (let attempt = 1; attempt <= DEDUPE_MAX_ATTEMPTS; attempt += 1) {
|
|
133
140
|
let result;
|
|
@@ -135,6 +142,9 @@ export async function dedupeMultiFocusFindings({ findings, availableModels, work
|
|
|
135
142
|
result = await invokePiPrompt({
|
|
136
143
|
provider: providerConfig.provider,
|
|
137
144
|
apiKey: providerConfig.apiKey,
|
|
145
|
+
...(providerConfig.piProviderId
|
|
146
|
+
? { piProviderId: providerConfig.piProviderId }
|
|
147
|
+
: {}),
|
|
138
148
|
prompt: buildDedupePrompt(findings),
|
|
139
149
|
cwd: workspaceDir,
|
|
140
150
|
timeoutMs: DEDUPE_TIMEOUT_MS,
|
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
import { logger } from '../../../core/logging.js';
|
|
2
2
|
import { convertFindingsToComments } from '../comment-mapper.js';
|
|
3
|
-
import { buildFallbackSummary, summarizeCouncilReviews, withReviewSummaryGreeting, } from '../council/summary.js';
|
|
4
3
|
import { buildReviewOutputFindings } from '../finding-output.js';
|
|
5
4
|
import { buildMultiFocusPrompt, REVIEW_FOCUS_DEFINITIONS, } from '../multi-focus-prompt.js';
|
|
5
|
+
import { buildFallbackSummary, summarizeReviewFindings } from '../review-summary.js';
|
|
6
6
|
import { parseSeverity } from '../severity.js';
|
|
7
|
+
import { buildGeminiFlashReviewSummaryModel } from '../summary-model.js';
|
|
7
8
|
import { dedupeMultiFocusFindings } from './dedupe.js';
|
|
8
9
|
import { getApiKeys, getAvailableModels, getProviderFailures, requestModelReview, } from './shared.js';
|
|
10
|
+
const DEFAULT_REVIEW_MODELS_BY_FOCUS = {
|
|
11
|
+
general: ['gemini', 'openai'],
|
|
12
|
+
quality: ['gemini', 'openai'],
|
|
13
|
+
efficiency: ['gemini', 'openai'],
|
|
14
|
+
tests: ['deepseek', 'zai'],
|
|
15
|
+
standards: ['deepseek', 'zai'],
|
|
16
|
+
};
|
|
9
17
|
const LOW_PRIORITY_SUMMARY_NOTICE = 'I also included a few optional follow-up notes in the details below.';
|
|
10
|
-
async function runFocusPass({ focus, model, basePrompt, standardsSection, apiKeys, workspaceDir, onProgress, }) {
|
|
18
|
+
async function runFocusPass({ focus, model, basePrompt, standardsSection, apiKeys, workspaceDir, openRouterSessionId, onProgress, }) {
|
|
11
19
|
const prompt = buildMultiFocusPrompt(basePrompt, focus, standardsSection);
|
|
12
20
|
const result = await requestModelReview({
|
|
13
21
|
model,
|
|
14
22
|
prompt,
|
|
15
23
|
apiKeys,
|
|
16
24
|
workspaceDir,
|
|
17
|
-
|
|
25
|
+
openRouterSessionId,
|
|
18
26
|
onProgress,
|
|
19
27
|
});
|
|
20
28
|
if (!result) {
|
|
@@ -70,10 +78,10 @@ function routeLowPriorityFindings(findings, placement = 'summary') {
|
|
|
70
78
|
}
|
|
71
79
|
return { inlineFindings, summaryFindings };
|
|
72
80
|
}
|
|
73
|
-
function buildNoInlineSummaryWithNotes(prTitle
|
|
81
|
+
function buildNoInlineSummaryWithNotes(prTitle) {
|
|
74
82
|
const safeTitle = prTitle.trim();
|
|
75
83
|
const base = safeTitle ? `This PR updates ${safeTitle}.` : 'This PR updates the codebase.';
|
|
76
|
-
return
|
|
84
|
+
return `${base} No inline issues were flagged.`;
|
|
77
85
|
}
|
|
78
86
|
function collectReviewObservations(focusRuns) {
|
|
79
87
|
const observations = [];
|
|
@@ -95,6 +103,10 @@ function collectReviewObservations(focusRuns) {
|
|
|
95
103
|
}
|
|
96
104
|
return observations.slice(0, 8);
|
|
97
105
|
}
|
|
106
|
+
function getModelsForFocus(focus, availableModels) {
|
|
107
|
+
const preferredModels = DEFAULT_REVIEW_MODELS_BY_FOCUS[focus.name];
|
|
108
|
+
return preferredModels.filter((model) => availableModels.includes(model));
|
|
109
|
+
}
|
|
98
110
|
function formatSummaryOnlyFinding(finding) {
|
|
99
111
|
const severity = parseSeverity(finding.body);
|
|
100
112
|
const bodyWithoutSeverity = finding.body.replace(/^\s*\[P\d\]\s*/i, '').trim();
|
|
@@ -116,34 +128,40 @@ function appendSummaryOnlyFindings(summary, summaryFindings) {
|
|
|
116
128
|
const title = `Optional follow-up note${notes.length === 1 ? '' : 's'} (${notes.length})`;
|
|
117
129
|
return `${summary.trim()}\n\n${LOW_PRIORITY_SUMMARY_NOTICE}\n\n<details>\n<summary>${title}</summary>\n\n${notes.join('\n')}\n\n</details>`;
|
|
118
130
|
}
|
|
119
|
-
|
|
131
|
+
function buildSummarySynthesisFindings({ inlineFindings, summaryFindings, }) {
|
|
132
|
+
return Array.from(new Set([
|
|
133
|
+
...inlineFindings.map((finding) => finding.body.trim()),
|
|
134
|
+
...summaryFindings.map((finding) => formatSummaryOnlyFinding(finding)),
|
|
135
|
+
].filter(Boolean)));
|
|
136
|
+
}
|
|
137
|
+
async function buildMultiFocusReviewOutput({ inlineFindings, summaryFindings, lineLookup, prTitle, prBody, reviewObservations, summaryCredential, summaryFallbackModels, workspaceDir, }) {
|
|
120
138
|
const comments = convertFindingsToComments(inlineFindings, lineLookup);
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
139
|
+
const inlineCommentFindings = Array.from(new Set(comments.map((comment) => comment.body.trim()).filter(Boolean)));
|
|
140
|
+
const findings = buildSummarySynthesisFindings({ inlineFindings, summaryFindings });
|
|
141
|
+
const hasReviewFindings = inlineFindings.length > 0 || summaryFindings.length > 0;
|
|
142
|
+
if (!hasReviewFindings) {
|
|
143
|
+
logger.info('Review summary synthesis skipped for clean review', {
|
|
144
|
+
inlineFindingsCount: inlineFindings.length,
|
|
145
|
+
summaryFindingsCount: summaryFindings.length,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const synthesizedSummary = summaryCredential == null || !hasReviewFindings
|
|
126
149
|
? null
|
|
127
|
-
: await
|
|
128
|
-
|
|
150
|
+
: await summarizeReviewFindings({
|
|
151
|
+
credential: summaryCredential,
|
|
129
152
|
workspaceDir,
|
|
130
153
|
prTitle,
|
|
131
154
|
prBody,
|
|
132
|
-
prAuthor,
|
|
133
|
-
prAuthorGreetingName: resolvedPrAuthorGreetingName,
|
|
134
|
-
excludePrAuthorFromSummary,
|
|
135
155
|
findings,
|
|
136
156
|
reviewObservations,
|
|
157
|
+
fallbackModels: summaryFallbackModels,
|
|
137
158
|
});
|
|
138
159
|
const summaryWithoutNotes = synthesizedSummary ||
|
|
139
|
-
(comments.length === 0 &&
|
|
140
|
-
? buildNoInlineSummaryWithNotes(prTitle
|
|
160
|
+
(comments.length === 0 && hasReviewFindings
|
|
161
|
+
? buildNoInlineSummaryWithNotes(prTitle)
|
|
141
162
|
: buildFallbackSummary({
|
|
142
163
|
prTitle,
|
|
143
|
-
findings,
|
|
144
|
-
prAuthor,
|
|
145
|
-
prAuthorGreetingName: resolvedPrAuthorGreetingName,
|
|
146
|
-
excludePrAuthorFromSummary,
|
|
164
|
+
findings: inlineCommentFindings,
|
|
147
165
|
}));
|
|
148
166
|
return {
|
|
149
167
|
summary: appendSummaryOnlyFindings(summaryWithoutNotes, summaryFindings),
|
|
@@ -155,10 +173,10 @@ async function buildMultiFocusReviewOutput({ inlineFindings, summaryFindings, li
|
|
|
155
173
|
}),
|
|
156
174
|
};
|
|
157
175
|
}
|
|
158
|
-
export async function runMultiFocusReviewEngine({ ctx, prompt, lineLookup, prTitle, prBody,
|
|
176
|
+
export async function runMultiFocusReviewEngine({ ctx, prompt, lineLookup, prTitle, prBody, lowPriorityFindingPlacement, standardsSection, onProgress, }) {
|
|
159
177
|
const apiKeys = getApiKeys(ctx);
|
|
160
178
|
const availableModels = getAvailableModels(apiKeys);
|
|
161
|
-
const metricModels = availableModels.length > 0 ? [...availableModels] : [...ctx.
|
|
179
|
+
const metricModels = availableModels.length > 0 ? [...availableModels] : [...ctx.reviewModelsEnabled];
|
|
162
180
|
if (availableModels.length === 0) {
|
|
163
181
|
return {
|
|
164
182
|
review: null,
|
|
@@ -168,13 +186,14 @@ export async function runMultiFocusReviewEngine({ ctx, prompt, lineLookup, prTit
|
|
|
168
186
|
providerFailures: [],
|
|
169
187
|
};
|
|
170
188
|
}
|
|
171
|
-
const focusRuns = await Promise.all(REVIEW_FOCUS_DEFINITIONS.flatMap((focus) => availableModels.map((model) => runFocusPass({
|
|
189
|
+
const focusRuns = await Promise.all(REVIEW_FOCUS_DEFINITIONS.flatMap((focus) => getModelsForFocus(focus, availableModels).map((model) => runFocusPass({
|
|
172
190
|
focus,
|
|
173
191
|
model,
|
|
174
192
|
basePrompt: prompt,
|
|
175
193
|
standardsSection,
|
|
176
194
|
apiKeys,
|
|
177
195
|
workspaceDir: ctx.workspaceDir,
|
|
196
|
+
openRouterSessionId: ctx.openRouterSessionId,
|
|
178
197
|
onProgress,
|
|
179
198
|
}))));
|
|
180
199
|
const successfulRuns = focusRuns.filter((run) => run.status === 'succeeded');
|
|
@@ -198,25 +217,32 @@ export async function runMultiFocusReviewEngine({ ctx, prompt, lineLookup, prTit
|
|
|
198
217
|
}
|
|
199
218
|
const allFindings = successfulRuns.flatMap((run) => run.findings);
|
|
200
219
|
const reviewObservations = collectReviewObservations(successfulRuns);
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
220
|
+
let dedupedFindings = allFindings;
|
|
221
|
+
try {
|
|
222
|
+
dedupedFindings = await dedupeMultiFocusFindings({
|
|
223
|
+
findings: allFindings,
|
|
224
|
+
availableModels,
|
|
225
|
+
workspaceDir: ctx.workspaceDir,
|
|
226
|
+
geminiCredential: apiKeys.gemini,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
logger.warn('Multi-focus dedupe failed, continuing with raw findings', {
|
|
231
|
+
findingsCount: allFindings.length,
|
|
232
|
+
error: error instanceof Error ? error.message : String(error),
|
|
233
|
+
});
|
|
234
|
+
}
|
|
207
235
|
const routedFindings = routeLowPriorityFindings(dedupedFindings, lowPriorityFindingPlacement);
|
|
236
|
+
const geminiSummaryFallback = buildGeminiFlashReviewSummaryModel(apiKeys.gemini);
|
|
208
237
|
const review = await buildMultiFocusReviewOutput({
|
|
209
238
|
inlineFindings: routedFindings.inlineFindings,
|
|
210
239
|
summaryFindings: routedFindings.summaryFindings,
|
|
211
240
|
lineLookup,
|
|
212
241
|
prTitle,
|
|
213
242
|
prBody,
|
|
214
|
-
prAuthor,
|
|
215
|
-
prAuthorGreetingName,
|
|
216
|
-
resolvePrAuthorGreetingName,
|
|
217
|
-
excludePrAuthorFromSummary,
|
|
218
243
|
reviewObservations,
|
|
219
|
-
|
|
244
|
+
summaryCredential: apiKeys.zai,
|
|
245
|
+
summaryFallbackModels: geminiSummaryFallback ? [geminiSummaryFallback] : [],
|
|
220
246
|
workspaceDir: ctx.workspaceDir,
|
|
221
247
|
});
|
|
222
248
|
logger.info('Multi-focus review completed', {
|
|
@@ -1,29 +1,23 @@
|
|
|
1
|
+
import { getReviewProviderCredential, hasReviewProviderCredential, } from '../../../providers/credentials.js';
|
|
1
2
|
import { getProvider } from '../../../providers/index.js';
|
|
2
|
-
import { convertFindingsToComments } from '../comment-mapper.js';
|
|
3
|
-
import { buildFallbackSummary, summarizeCouncilReviews, withReviewSummaryGreeting, } from '../council/summary.js';
|
|
4
|
-
import { buildReviewOutputFindings } from '../finding-output.js';
|
|
5
3
|
import { getReviewThinkingLevel } from '../thinking-level.js';
|
|
6
4
|
export function getApiKeys(ctx) {
|
|
7
5
|
const apiKeys = {};
|
|
8
|
-
|
|
9
|
-
apiKeys
|
|
10
|
-
}
|
|
11
|
-
if (ctx.councilModelsEnabled.includes('openai')) {
|
|
12
|
-
apiKeys.openai = ctx.openaiApiKey;
|
|
13
|
-
}
|
|
14
|
-
if (ctx.councilModelsEnabled.includes('anthropic')) {
|
|
15
|
-
apiKeys.anthropic = ctx.anthropicApiKey;
|
|
6
|
+
for (const model of ctx.reviewModelsEnabled) {
|
|
7
|
+
apiKeys[model] = ctx.apiKeys[model];
|
|
16
8
|
}
|
|
17
9
|
return apiKeys;
|
|
18
10
|
}
|
|
19
11
|
export function getAvailableModels(apiKeys) {
|
|
20
12
|
const models = [];
|
|
21
|
-
if (apiKeys.gemini)
|
|
13
|
+
if (hasReviewProviderCredential(apiKeys.gemini))
|
|
22
14
|
models.push('gemini');
|
|
23
|
-
if (apiKeys.openai)
|
|
15
|
+
if (hasReviewProviderCredential(apiKeys.openai))
|
|
24
16
|
models.push('openai');
|
|
25
|
-
if (apiKeys.
|
|
26
|
-
models.push('
|
|
17
|
+
if (hasReviewProviderCredential(apiKeys.deepseek))
|
|
18
|
+
models.push('deepseek');
|
|
19
|
+
if (hasReviewProviderCredential(apiKeys.zai))
|
|
20
|
+
models.push('zai');
|
|
27
21
|
return models;
|
|
28
22
|
}
|
|
29
23
|
export function getProviderFailures(results) {
|
|
@@ -34,63 +28,23 @@ export function getProviderFailures(results) {
|
|
|
34
28
|
error: result.error,
|
|
35
29
|
}));
|
|
36
30
|
}
|
|
37
|
-
export async function requestModelReview({ model, prompt, apiKeys, workspaceDir,
|
|
38
|
-
const
|
|
39
|
-
if (!
|
|
31
|
+
export async function requestModelReview({ model, prompt, apiKeys, workspaceDir, openRouterSessionId, onProgress, }) {
|
|
32
|
+
const credential = getReviewProviderCredential(apiKeys, model);
|
|
33
|
+
if (!credential) {
|
|
40
34
|
return null;
|
|
41
35
|
}
|
|
42
36
|
const provider = getProvider(model);
|
|
43
37
|
const config = {
|
|
44
|
-
|
|
38
|
+
...credential,
|
|
45
39
|
workspaceDir,
|
|
46
40
|
thinkingLevel: getReviewThinkingLevel(model),
|
|
41
|
+
openRouterSessionId,
|
|
47
42
|
onProgress,
|
|
48
43
|
};
|
|
49
44
|
try {
|
|
50
|
-
return await provider.review(prompt, config
|
|
45
|
+
return await provider.review(prompt, config);
|
|
51
46
|
}
|
|
52
47
|
catch {
|
|
53
48
|
return null;
|
|
54
49
|
}
|
|
55
50
|
}
|
|
56
|
-
export async function buildReviewOutputFromFindings({ commentsSource, lineLookup, prTitle, prBody, prAuthor, prAuthorGreetingName, resolvePrAuthorGreetingName, excludePrAuthorFromSummary, summaryApiKey, workspaceDir, preferredSummary, reviewObservations, }) {
|
|
57
|
-
const comments = convertFindingsToComments(commentsSource, lineLookup);
|
|
58
|
-
const findings = Array.from(new Set(comments.map((comment) => comment.body.trim()).filter(Boolean)));
|
|
59
|
-
const preferredSummaryText = preferredSummary?.trim();
|
|
60
|
-
const hasPreferredSummary = Boolean(preferredSummaryText);
|
|
61
|
-
const resolvedPrAuthorGreetingName = excludePrAuthorFromSummary === true
|
|
62
|
-
? undefined
|
|
63
|
-
: (prAuthorGreetingName ?? (await resolvePrAuthorGreetingName?.()));
|
|
64
|
-
const synthesizedSummary = hasPreferredSummary || summaryApiKey == null
|
|
65
|
-
? null
|
|
66
|
-
: await summarizeCouncilReviews({
|
|
67
|
-
apiKey: summaryApiKey,
|
|
68
|
-
workspaceDir,
|
|
69
|
-
prTitle,
|
|
70
|
-
prBody,
|
|
71
|
-
prAuthor,
|
|
72
|
-
prAuthorGreetingName: resolvedPrAuthorGreetingName,
|
|
73
|
-
excludePrAuthorFromSummary,
|
|
74
|
-
findings,
|
|
75
|
-
reviewObservations,
|
|
76
|
-
});
|
|
77
|
-
const summary = preferredSummaryText ||
|
|
78
|
-
synthesizedSummary ||
|
|
79
|
-
buildFallbackSummary({
|
|
80
|
-
prTitle,
|
|
81
|
-
findings,
|
|
82
|
-
prAuthor,
|
|
83
|
-
prAuthorGreetingName: resolvedPrAuthorGreetingName,
|
|
84
|
-
excludePrAuthorFromSummary,
|
|
85
|
-
});
|
|
86
|
-
return {
|
|
87
|
-
summary: hasPreferredSummary
|
|
88
|
-
? withReviewSummaryGreeting(summary, prAuthor, resolvedPrAuthorGreetingName, excludePrAuthorFromSummary)
|
|
89
|
-
: summary,
|
|
90
|
-
comments,
|
|
91
|
-
findings: buildReviewOutputFindings({
|
|
92
|
-
inlineFindings: commentsSource,
|
|
93
|
-
lineLookup,
|
|
94
|
-
}),
|
|
95
|
-
};
|
|
96
|
-
}
|
|
@@ -1,28 +1,82 @@
|
|
|
1
1
|
import { parseSeverity } from './severity.js';
|
|
2
|
-
const
|
|
3
|
-
P0: '
|
|
4
|
-
P1: '
|
|
5
|
-
P2: '
|
|
2
|
+
export const PRIORITY_BADGE_URLS = {
|
|
3
|
+
P0: 'https://res.cloudinary.com/dqrcfnzm9/image/upload/v1779779477/p0_xezwrq.svg',
|
|
4
|
+
P1: 'https://res.cloudinary.com/dqrcfnzm9/image/upload/v1779779477/p1_iqydtr.svg',
|
|
5
|
+
P2: 'https://res.cloudinary.com/dqrcfnzm9/image/upload/v1779779477/p2_skkn6d.svg',
|
|
6
|
+
P3: 'https://res.cloudinary.com/dqrcfnzm9/image/upload/v1782200379/p3_syntlm.svg',
|
|
6
7
|
};
|
|
7
8
|
const PRIORITY_PREFIX_PATTERN = /^\s*\[(P[0-3])\]([^\S\r\n]*)(\r?\n)?/i;
|
|
9
|
+
const PRIORITY_TAG_PATTERN = /\[(P[0-3])\]/gi;
|
|
10
|
+
const PRIORITY_METADATA_PATTERN = /<!--\s*\[(P[0-3])\]\s*-->\s*(?:\[\1\]\s*)?/gi;
|
|
11
|
+
const PRIORITY_BADGE_HTML_PATTERN = /(?:<a\b[^>]*>\s*)?<img\b(?=[^>]*\balt=["']P[0-3]["'])(?=[^>]*\bsrc=["']https:\/\/res\.cloudinary\.com\/dqrcfnzm9\/image\/upload\/v\d+\/p[0-3]_[^"']+\.svg["'])[^>]*>(?:\s*<\/a>)?([^\S\r\n]*)/gi;
|
|
12
|
+
const PRIORITY_BADGE_ALT_PATTERN = /\balt=["'](P[0-3])["']/i;
|
|
8
13
|
const INLINE_BADGE_SEPARATOR = ' ';
|
|
9
14
|
export function renderPriorityBadge(severity) {
|
|
10
|
-
const
|
|
11
|
-
|
|
15
|
+
const badgeUrl = PRIORITY_BADGE_URLS[severity];
|
|
16
|
+
// GitHub auto-links bare images to the asset, so keep an explicit anchor around the badge.
|
|
17
|
+
return `<a href="#"><img alt="${severity}" src="${badgeUrl}" align="top"></a>`;
|
|
18
|
+
}
|
|
19
|
+
function renderPriorityMetadata(severity) {
|
|
20
|
+
return `<!--[${severity}] -->`;
|
|
21
|
+
}
|
|
22
|
+
function renderPriorityBadgeWithMetadata(severity) {
|
|
23
|
+
return `${renderPriorityMetadata(severity)}${renderPriorityBadge(severity)}`;
|
|
24
|
+
}
|
|
25
|
+
function getOpeningFence(line) {
|
|
26
|
+
const fence = line.match(/^ {0,3}(`{3,}|~{3,})/)?.[1];
|
|
27
|
+
if (!fence) {
|
|
12
28
|
return null;
|
|
13
29
|
}
|
|
14
|
-
return
|
|
30
|
+
return {
|
|
31
|
+
marker: fence[0],
|
|
32
|
+
length: fence.length,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function isClosingFence(line, activeFence) {
|
|
36
|
+
const fence = line.match(/^ {0,3}(`{3,}|~{3,})\s*$/)?.[1];
|
|
37
|
+
return fence?.[0] === activeFence.marker && fence.length >= activeFence.length;
|
|
15
38
|
}
|
|
16
39
|
export function formatGithubReviewCommentBody(body) {
|
|
17
40
|
const match = body.match(PRIORITY_PREFIX_PATTERN);
|
|
18
41
|
const severity = match ? parseSeverity(match[0]) : null;
|
|
19
|
-
|
|
20
|
-
if (!match || !badge) {
|
|
42
|
+
if (!match || !severity) {
|
|
21
43
|
return body;
|
|
22
44
|
}
|
|
23
45
|
const separator = match[3] ?? INLINE_BADGE_SEPARATOR;
|
|
24
46
|
const content = body.slice(match[0].length);
|
|
25
|
-
return `${
|
|
47
|
+
return `${renderPriorityBadgeWithMetadata(severity)}${separator}${content}`;
|
|
48
|
+
}
|
|
49
|
+
export function formatGithubReviewSummary(body) {
|
|
50
|
+
const parts = body.split(/(\r?\n)/);
|
|
51
|
+
let activeFence = null;
|
|
52
|
+
return parts
|
|
53
|
+
.map((part) => {
|
|
54
|
+
if (/^\r?\n$/.test(part)) {
|
|
55
|
+
return part;
|
|
56
|
+
}
|
|
57
|
+
if (activeFence) {
|
|
58
|
+
if (isClosingFence(part, activeFence)) {
|
|
59
|
+
activeFence = null;
|
|
60
|
+
}
|
|
61
|
+
return part;
|
|
62
|
+
}
|
|
63
|
+
const openingFence = getOpeningFence(part);
|
|
64
|
+
if (openingFence) {
|
|
65
|
+
activeFence = openingFence;
|
|
66
|
+
return part;
|
|
67
|
+
}
|
|
68
|
+
return normalizeGithubPriorityBadges(part).replace(PRIORITY_TAG_PATTERN, (_tag, severity) => renderPriorityBadgeWithMetadata(severity.toUpperCase()));
|
|
69
|
+
})
|
|
70
|
+
.join('');
|
|
71
|
+
}
|
|
72
|
+
export function normalizeGithubPriorityBadges(body) {
|
|
73
|
+
const normalizedBadges = body.replace(PRIORITY_BADGE_HTML_PATTERN, (badge, separator) => {
|
|
74
|
+
const severity = badge.match(PRIORITY_BADGE_ALT_PATTERN)?.[1]?.toUpperCase();
|
|
75
|
+
return severity ? `[${severity}]${separator ? ' ' : ''}` : badge;
|
|
76
|
+
});
|
|
77
|
+
return normalizedBadges.replace(PRIORITY_METADATA_PATTERN, (_match, severity) => {
|
|
78
|
+
return `[${severity.toUpperCase()}] `;
|
|
79
|
+
});
|
|
26
80
|
}
|
|
27
81
|
export function formatGithubReviewComments(comments) {
|
|
28
82
|
return comments.map((comment) => ({
|
|
@@ -1,24 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getDefaultReviewModels } from 'doistbot-repo-config';
|
|
2
2
|
import { logger, withInfoLogsSuppressed, withLogsSuppressed } from '../../core/logging.js';
|
|
3
3
|
import {} from './diff.js';
|
|
4
4
|
import { createGitRunner, getLocalReviewFiles, resolveLocalAuthor, resolveRepositoryMetadata, tryGit, } from './local-git.js';
|
|
5
|
-
import { loadDefaultReviewPromptTemplate } from './prompt.js';
|
|
6
5
|
import { runSharedReview } from './runner.js';
|
|
7
6
|
const LOCAL_REVIEW_PR_NUMBER = 0;
|
|
8
|
-
function resolveLocalPromptTemplate(workspaceDir, promptFile) {
|
|
9
|
-
if (promptFile) {
|
|
10
|
-
const customPrompt = getLocalCustomPrompt({
|
|
11
|
-
workspaceDir,
|
|
12
|
-
path: promptFile,
|
|
13
|
-
logger,
|
|
14
|
-
});
|
|
15
|
-
if (customPrompt) {
|
|
16
|
-
return customPrompt;
|
|
17
|
-
}
|
|
18
|
-
logger.info('Local custom prompt not found, using default', { path: promptFile });
|
|
19
|
-
}
|
|
20
|
-
return loadDefaultReviewPromptTemplate();
|
|
21
|
-
}
|
|
22
7
|
export async function runLocalReview(input) {
|
|
23
8
|
if (input.suppressLogs === true) {
|
|
24
9
|
return await withLogsSuppressed(['info', 'warn', 'error'], async () => await runLocalReviewWithLogging(input));
|
|
@@ -41,13 +26,6 @@ async function runLocalReviewWithLogging(input) {
|
|
|
41
26
|
source: input.source,
|
|
42
27
|
});
|
|
43
28
|
input.onProgress?.({ type: 'local_files_resolved', fileCount: files.length });
|
|
44
|
-
const repoConfig = getLocalRepoConfig({ workspaceDir: input.workspaceDir, logger });
|
|
45
|
-
const reviewEngine = resolveReviewEngine({
|
|
46
|
-
configuredEngine: repoConfig?.review?.engine,
|
|
47
|
-
envEngine: process.env.REVIEW_ENGINE,
|
|
48
|
-
logger,
|
|
49
|
-
repository: `${owner}/${repo}`,
|
|
50
|
-
});
|
|
51
29
|
const reviewResult = await runSharedReview({
|
|
52
30
|
owner,
|
|
53
31
|
repo,
|
|
@@ -58,13 +36,8 @@ async function runLocalReviewWithLogging(input) {
|
|
|
58
36
|
prBody: 'Local self-review before opening a pull request.',
|
|
59
37
|
prAuthor: author,
|
|
60
38
|
files,
|
|
61
|
-
reviewEngine,
|
|
62
|
-
promptTemplate: reviewEngine === 'council'
|
|
63
|
-
? resolveLocalPromptTemplate(input.workspaceDir, repoConfig?.review?.prompt_file)
|
|
64
|
-
: undefined,
|
|
65
39
|
apiKeys: input.apiKeys,
|
|
66
|
-
|
|
67
|
-
councilModelsEnabled: parseCouncilModelsEnabled(process.env.COUNCIL_MODELS_ENABLED),
|
|
40
|
+
reviewModelsEnabled: getDefaultReviewModels(),
|
|
68
41
|
lowPriorityFindingPlacement: 'inline',
|
|
69
42
|
workspaceDir: input.workspaceDir,
|
|
70
43
|
onProgress: input.onProgress,
|