@doist/doistbot-cli 1.0.3 → 1.0.5
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 +23 -23
- package/dist/actions/doctor.js +3 -5
- package/dist/actions/review-usage.js +6 -1
- package/dist/actions/review.js +7 -11
- package/dist/auth.js +17 -17
- package/dist/config.js +14 -9
- package/dist/env.js +1 -2
- package/dist/file-store.js +1 -1
- package/dist/index.js +19 -1
- package/dist/terminal.js +0 -7
- package/package.json +5 -5
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +7 -21
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +8 -72
- package/sandbox/dist/core/bot-logins.js +0 -13
- package/sandbox/dist/core/datadog-metrics.js +0 -2
- package/sandbox/dist/core/pi.js +16 -13
- package/sandbox/dist/providers/config.js +1 -1
- package/sandbox/dist/providers/credentials.js +30 -0
- package/sandbox/dist/providers/gemini.js +4 -10
- package/sandbox/dist/providers/helpers.js +6 -21
- package/sandbox/dist/providers/index.js +0 -3
- package/sandbox/dist/providers/openai.js +1 -8
- package/sandbox/dist/providers/pi-review.js +16 -54
- package/sandbox/dist/providers/schemas.js +0 -39
- package/sandbox/dist/tasks/chat/chat.js +2 -1
- package/sandbox/dist/tasks/issue-summarize/context.js +1 -1
- package/sandbox/dist/tasks/issue-summarize/model.js +3 -1
- package/sandbox/dist/tasks/issue-summarize/summarize.js +1 -1
- package/sandbox/dist/tasks/issue-triage/context.js +1 -2
- package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +3 -3
- 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/conversation-context.js +13 -7
- package/sandbox/dist/tasks/review/engines/dedupe.js +18 -9
- package/sandbox/dist/tasks/review/engines/multi-focus.js +26 -31
- package/sandbox/dist/tasks/review/engines/shared.js +11 -59
- package/sandbox/dist/tasks/review/local-git.js +3 -1
- package/sandbox/dist/tasks/review/local.js +4 -29
- package/sandbox/dist/tasks/review/prompt.js +2 -49
- package/sandbox/dist/tasks/review/review-summary.js +82 -0
- package/sandbox/dist/tasks/review/review.js +23 -57
- package/sandbox/dist/tasks/review/runner.js +35 -51
- package/sandbox/dist/tasks/review/summary-model.js +11 -2
- package/sandbox/dist/tasks/review/thinking-level.js +0 -2
- package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +7 -21
- package/sandbox/node_modules/doistbot-repo-config/dist/index.js +8 -72
- 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
|
@@ -3,6 +3,7 @@ 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';
|
|
6
7
|
import { stripReviewFeedbackFooter } from './review-feedback.js';
|
|
7
8
|
// Roughly 6k tokens: large enough for substantive PR discussions while still
|
|
8
9
|
// keeping conversation context secondary to the diff.
|
|
@@ -14,11 +15,8 @@ const GITHUB_ACTIONS_BOT_LOGIN = 'github-actions[bot]';
|
|
|
14
15
|
const MASKED_PR_AUTHOR_LABEL = 'PR author';
|
|
15
16
|
function getSummaryProviders(apiKeys) {
|
|
16
17
|
return [
|
|
17
|
-
...(apiKeys?.gemini ? [{ provider: 'gemini',
|
|
18
|
-
...(apiKeys?.openai ? [{ provider: 'openai',
|
|
19
|
-
...(apiKeys?.anthropic
|
|
20
|
-
? [{ provider: 'anthropic', apiKey: apiKeys.anthropic }]
|
|
21
|
-
: []),
|
|
18
|
+
...(apiKeys?.gemini ? [{ provider: 'gemini', credential: apiKeys.gemini }] : []),
|
|
19
|
+
...(apiKeys?.openai ? [{ provider: 'openai', credential: apiKeys.openai }] : []),
|
|
22
20
|
];
|
|
23
21
|
}
|
|
24
22
|
function isRelevantBotLogin(login) {
|
|
@@ -253,7 +251,7 @@ If a section has no relevant information, use "- None."
|
|
|
253
251
|
${rawContext}
|
|
254
252
|
</pr_conversation>`;
|
|
255
253
|
}
|
|
256
|
-
async function summarizeConversationContextWithModel({ provider,
|
|
254
|
+
async function summarizeConversationContextWithModel({ provider, credential, rawContext, workspaceDir, }) {
|
|
257
255
|
const startedAt = Date.now();
|
|
258
256
|
logger.info('PR conversation summarization started', {
|
|
259
257
|
task: 'review',
|
|
@@ -261,9 +259,17 @@ async function summarizeConversationContextWithModel({ provider, apiKey, rawCont
|
|
|
261
259
|
raw_chars: rawContext.length,
|
|
262
260
|
});
|
|
263
261
|
try {
|
|
262
|
+
const resolvedCredential = resolveReviewProviderCredential(credential);
|
|
263
|
+
if (!resolvedCredential) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
264
266
|
const result = await invokePiPrompt({
|
|
265
267
|
provider,
|
|
266
|
-
apiKey,
|
|
268
|
+
apiKey: resolvedCredential.apiKey,
|
|
269
|
+
...(resolvedCredential.piProviderId
|
|
270
|
+
? { piProviderId: resolvedCredential.piProviderId }
|
|
271
|
+
: {}),
|
|
272
|
+
...(resolvedCredential.modelName ? { modelName: resolvedCredential.modelName } : {}),
|
|
267
273
|
prompt: buildSummaryPrompt(rawContext),
|
|
268
274
|
cwd: workspaceDir ?? WORKSPACE_DIR,
|
|
269
275
|
timeoutMs: CONVERSATION_SUMMARY_TIMEOUT_MS,
|
|
@@ -1,11 +1,14 @@
|
|
|
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
|
+
openrouter: 'google/gemini-3-flash-preview',
|
|
11
|
+
};
|
|
9
12
|
const DEDUPE_THINKING_LEVEL = 'medium';
|
|
10
13
|
const DEDUPE_MAX_ATTEMPTS = 2;
|
|
11
14
|
const DEDUPE_RETRY_DELAY_MS = 1_000;
|
|
@@ -106,28 +109,31 @@ export function applyDedupeGroups(findings, groups) {
|
|
|
106
109
|
});
|
|
107
110
|
return findings.filter((finding) => !dropped.has(finding.id));
|
|
108
111
|
}
|
|
109
|
-
export async function dedupeMultiFocusFindings({ findings, availableModels, workspaceDir,
|
|
112
|
+
export async function dedupeMultiFocusFindings({ findings, availableModels, workspaceDir, geminiCredential, }) {
|
|
110
113
|
if (findings.length <= 1) {
|
|
111
114
|
return findings;
|
|
112
115
|
}
|
|
113
116
|
if (!availableModels.includes(DEDUPE_PROVIDER)) {
|
|
114
117
|
logger.error('Multi-focus dedupe unavailable because Gemini is not enabled', {
|
|
115
118
|
provider: DEDUPE_PROVIDER,
|
|
116
|
-
model:
|
|
119
|
+
model: DEDUPE_MODEL_BY_PI_PROVIDER.openrouter,
|
|
117
120
|
});
|
|
118
121
|
throw new Error('Multi-focus dedupe requires Gemini to be enabled');
|
|
119
122
|
}
|
|
120
|
-
|
|
121
|
-
|
|
123
|
+
const credential = resolveReviewProviderCredential(geminiCredential);
|
|
124
|
+
if (!credential) {
|
|
125
|
+
logger.error('Multi-focus dedupe unavailable because Gemini review credential is unavailable', {
|
|
122
126
|
provider: DEDUPE_PROVIDER,
|
|
123
|
-
model:
|
|
127
|
+
model: DEDUPE_MODEL_BY_PI_PROVIDER.openrouter,
|
|
124
128
|
});
|
|
125
|
-
throw new Error('Multi-focus dedupe requires a Gemini
|
|
129
|
+
throw new Error('Multi-focus dedupe requires a Gemini review credential');
|
|
126
130
|
}
|
|
131
|
+
const piProviderId = credential.piProviderId ?? 'openrouter';
|
|
127
132
|
const providerConfig = {
|
|
128
133
|
provider: DEDUPE_PROVIDER,
|
|
129
|
-
apiKey:
|
|
130
|
-
|
|
134
|
+
apiKey: credential.apiKey,
|
|
135
|
+
...(credential.piProviderId ? { piProviderId: credential.piProviderId } : {}),
|
|
136
|
+
modelName: DEDUPE_MODEL_BY_PI_PROVIDER[piProviderId] ?? DEDUPE_MODEL_BY_PI_PROVIDER.openrouter,
|
|
131
137
|
};
|
|
132
138
|
for (let attempt = 1; attempt <= DEDUPE_MAX_ATTEMPTS; attempt += 1) {
|
|
133
139
|
let result;
|
|
@@ -135,6 +141,9 @@ export async function dedupeMultiFocusFindings({ findings, availableModels, work
|
|
|
135
141
|
result = await invokePiPrompt({
|
|
136
142
|
provider: providerConfig.provider,
|
|
137
143
|
apiKey: providerConfig.apiKey,
|
|
144
|
+
...(providerConfig.piProviderId
|
|
145
|
+
? { piProviderId: providerConfig.piProviderId }
|
|
146
|
+
: {}),
|
|
138
147
|
prompt: buildDedupePrompt(findings),
|
|
139
148
|
cwd: workspaceDir,
|
|
140
149
|
timeoutMs: DEDUPE_TIMEOUT_MS,
|
|
@@ -1,8 +1,8 @@
|
|
|
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
7
|
import { dedupeMultiFocusFindings } from './dedupe.js';
|
|
8
8
|
import { getApiKeys, getAvailableModels, getProviderFailures, requestModelReview, } from './shared.js';
|
|
@@ -14,7 +14,6 @@ async function runFocusPass({ focus, model, basePrompt, standardsSection, apiKey
|
|
|
14
14
|
prompt,
|
|
15
15
|
apiKeys,
|
|
16
16
|
workspaceDir,
|
|
17
|
-
logEngine: 'multi_focus',
|
|
18
17
|
onProgress,
|
|
19
18
|
});
|
|
20
19
|
if (!result) {
|
|
@@ -70,10 +69,10 @@ function routeLowPriorityFindings(findings, placement = 'summary') {
|
|
|
70
69
|
}
|
|
71
70
|
return { inlineFindings, summaryFindings };
|
|
72
71
|
}
|
|
73
|
-
function buildNoInlineSummaryWithNotes(prTitle
|
|
72
|
+
function buildNoInlineSummaryWithNotes(prTitle) {
|
|
74
73
|
const safeTitle = prTitle.trim();
|
|
75
74
|
const base = safeTitle ? `This PR updates ${safeTitle}.` : 'This PR updates the codebase.';
|
|
76
|
-
return
|
|
75
|
+
return `${base} No inline issues were flagged.`;
|
|
77
76
|
}
|
|
78
77
|
function collectReviewObservations(focusRuns) {
|
|
79
78
|
const observations = [];
|
|
@@ -116,34 +115,25 @@ function appendSummaryOnlyFindings(summary, summaryFindings) {
|
|
|
116
115
|
const title = `Optional follow-up note${notes.length === 1 ? '' : 's'} (${notes.length})`;
|
|
117
116
|
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
117
|
}
|
|
119
|
-
async function buildMultiFocusReviewOutput({ inlineFindings, summaryFindings, lineLookup, prTitle, prBody,
|
|
118
|
+
async function buildMultiFocusReviewOutput({ inlineFindings, summaryFindings, lineLookup, prTitle, prBody, reviewObservations, summaryCredential, workspaceDir, }) {
|
|
120
119
|
const comments = convertFindingsToComments(inlineFindings, lineLookup);
|
|
121
120
|
const findings = Array.from(new Set(comments.map((comment) => comment.body.trim()).filter(Boolean)));
|
|
122
|
-
const
|
|
123
|
-
? undefined
|
|
124
|
-
: (prAuthorGreetingName ?? (await resolvePrAuthorGreetingName?.()));
|
|
125
|
-
const synthesizedSummary = summaryApiKey == null
|
|
121
|
+
const synthesizedSummary = summaryCredential == null
|
|
126
122
|
? null
|
|
127
|
-
: await
|
|
128
|
-
|
|
123
|
+
: await summarizeReviewFindings({
|
|
124
|
+
credential: summaryCredential,
|
|
129
125
|
workspaceDir,
|
|
130
126
|
prTitle,
|
|
131
127
|
prBody,
|
|
132
|
-
prAuthor,
|
|
133
|
-
prAuthorGreetingName: resolvedPrAuthorGreetingName,
|
|
134
|
-
excludePrAuthorFromSummary,
|
|
135
128
|
findings,
|
|
136
129
|
reviewObservations,
|
|
137
130
|
});
|
|
138
131
|
const summaryWithoutNotes = synthesizedSummary ||
|
|
139
132
|
(comments.length === 0 && summaryFindings.length > 0
|
|
140
|
-
? buildNoInlineSummaryWithNotes(prTitle
|
|
133
|
+
? buildNoInlineSummaryWithNotes(prTitle)
|
|
141
134
|
: buildFallbackSummary({
|
|
142
135
|
prTitle,
|
|
143
136
|
findings,
|
|
144
|
-
prAuthor,
|
|
145
|
-
prAuthorGreetingName: resolvedPrAuthorGreetingName,
|
|
146
|
-
excludePrAuthorFromSummary,
|
|
147
137
|
}));
|
|
148
138
|
return {
|
|
149
139
|
summary: appendSummaryOnlyFindings(summaryWithoutNotes, summaryFindings),
|
|
@@ -155,10 +145,10 @@ async function buildMultiFocusReviewOutput({ inlineFindings, summaryFindings, li
|
|
|
155
145
|
}),
|
|
156
146
|
};
|
|
157
147
|
}
|
|
158
|
-
export async function runMultiFocusReviewEngine({ ctx, prompt, lineLookup, prTitle, prBody,
|
|
148
|
+
export async function runMultiFocusReviewEngine({ ctx, prompt, lineLookup, prTitle, prBody, lowPriorityFindingPlacement, standardsSection, onProgress, }) {
|
|
159
149
|
const apiKeys = getApiKeys(ctx);
|
|
160
150
|
const availableModels = getAvailableModels(apiKeys);
|
|
161
|
-
const metricModels = availableModels.length > 0 ? [...availableModels] : [...ctx.
|
|
151
|
+
const metricModels = availableModels.length > 0 ? [...availableModels] : [...ctx.reviewModelsEnabled];
|
|
162
152
|
if (availableModels.length === 0) {
|
|
163
153
|
return {
|
|
164
154
|
review: null,
|
|
@@ -198,12 +188,21 @@ export async function runMultiFocusReviewEngine({ ctx, prompt, lineLookup, prTit
|
|
|
198
188
|
}
|
|
199
189
|
const allFindings = successfulRuns.flatMap((run) => run.findings);
|
|
200
190
|
const reviewObservations = collectReviewObservations(successfulRuns);
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
191
|
+
let dedupedFindings = allFindings;
|
|
192
|
+
try {
|
|
193
|
+
dedupedFindings = await dedupeMultiFocusFindings({
|
|
194
|
+
findings: allFindings,
|
|
195
|
+
availableModels,
|
|
196
|
+
workspaceDir: ctx.workspaceDir,
|
|
197
|
+
geminiCredential: ctx.geminiCredential,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
logger.warn('Multi-focus dedupe failed, continuing with raw findings', {
|
|
202
|
+
findingsCount: allFindings.length,
|
|
203
|
+
error: error instanceof Error ? error.message : String(error),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
207
206
|
const routedFindings = routeLowPriorityFindings(dedupedFindings, lowPriorityFindingPlacement);
|
|
208
207
|
const review = await buildMultiFocusReviewOutput({
|
|
209
208
|
inlineFindings: routedFindings.inlineFindings,
|
|
@@ -211,12 +210,8 @@ export async function runMultiFocusReviewEngine({ ctx, prompt, lineLookup, prTit
|
|
|
211
210
|
lineLookup,
|
|
212
211
|
prTitle,
|
|
213
212
|
prBody,
|
|
214
|
-
prAuthor,
|
|
215
|
-
prAuthorGreetingName,
|
|
216
|
-
resolvePrAuthorGreetingName,
|
|
217
|
-
excludePrAuthorFromSummary,
|
|
218
213
|
reviewObservations,
|
|
219
|
-
|
|
214
|
+
summaryCredential: apiKeys.gemini,
|
|
220
215
|
workspaceDir: ctx.workspaceDir,
|
|
221
216
|
});
|
|
222
217
|
logger.info('Multi-focus review completed', {
|
|
@@ -1,29 +1,22 @@
|
|
|
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
|
-
if (ctx.
|
|
9
|
-
apiKeys.gemini = ctx.
|
|
6
|
+
if (ctx.reviewModelsEnabled.includes('gemini')) {
|
|
7
|
+
apiKeys.gemini = ctx.geminiCredential;
|
|
10
8
|
}
|
|
11
|
-
if (ctx.
|
|
9
|
+
if (ctx.reviewModelsEnabled.includes('openai')) {
|
|
12
10
|
apiKeys.openai = ctx.openaiApiKey;
|
|
13
11
|
}
|
|
14
|
-
if (ctx.councilModelsEnabled.includes('anthropic')) {
|
|
15
|
-
apiKeys.anthropic = ctx.anthropicApiKey;
|
|
16
|
-
}
|
|
17
12
|
return apiKeys;
|
|
18
13
|
}
|
|
19
14
|
export function getAvailableModels(apiKeys) {
|
|
20
15
|
const models = [];
|
|
21
|
-
if (apiKeys.gemini)
|
|
16
|
+
if (hasReviewProviderCredential(apiKeys.gemini))
|
|
22
17
|
models.push('gemini');
|
|
23
|
-
if (apiKeys.openai)
|
|
18
|
+
if (hasReviewProviderCredential(apiKeys.openai))
|
|
24
19
|
models.push('openai');
|
|
25
|
-
if (apiKeys.anthropic)
|
|
26
|
-
models.push('anthropic');
|
|
27
20
|
return models;
|
|
28
21
|
}
|
|
29
22
|
export function getProviderFailures(results) {
|
|
@@ -34,63 +27,22 @@ export function getProviderFailures(results) {
|
|
|
34
27
|
error: result.error,
|
|
35
28
|
}));
|
|
36
29
|
}
|
|
37
|
-
export async function requestModelReview({ model, prompt, apiKeys, workspaceDir,
|
|
38
|
-
const
|
|
39
|
-
if (!
|
|
30
|
+
export async function requestModelReview({ model, prompt, apiKeys, workspaceDir, onProgress, }) {
|
|
31
|
+
const credential = getReviewProviderCredential(apiKeys, model);
|
|
32
|
+
if (!credential) {
|
|
40
33
|
return null;
|
|
41
34
|
}
|
|
42
35
|
const provider = getProvider(model);
|
|
43
36
|
const config = {
|
|
44
|
-
|
|
37
|
+
...credential,
|
|
45
38
|
workspaceDir,
|
|
46
39
|
thinkingLevel: getReviewThinkingLevel(model),
|
|
47
40
|
onProgress,
|
|
48
41
|
};
|
|
49
42
|
try {
|
|
50
|
-
return await provider.review(prompt, config
|
|
43
|
+
return await provider.review(prompt, config);
|
|
51
44
|
}
|
|
52
45
|
catch {
|
|
53
46
|
return null;
|
|
54
47
|
}
|
|
55
48
|
}
|
|
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
|
-
}
|
|
@@ -177,7 +177,9 @@ export function getLocalReviewFiles({ runner, source, }) {
|
|
|
177
177
|
baseBranch: `commit ${source.baseSha.slice(0, 7)}`,
|
|
178
178
|
};
|
|
179
179
|
}
|
|
180
|
-
const baseBranch =
|
|
180
|
+
const baseBranch = typeof source === 'object' && source.kind === 'base-ref'
|
|
181
|
+
? source.ref
|
|
182
|
+
: resolveOriginBaseRef(runner);
|
|
181
183
|
const mergeBase = runner(['merge-base', baseBranch, 'HEAD']).trim();
|
|
182
184
|
const diff = runner(['diff', '--no-ext-diff', '--no-color', mergeBase]);
|
|
183
185
|
return {
|
|
@@ -1,24 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseReviewModelsEnabled } 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,10 @@ 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
|
-
|
|
40
|
+
reviewModelsEnabled: parseReviewModelsEnabled(process.env.REVIEW_MODELS_ENABLED, {
|
|
41
|
+
requiredModels: [],
|
|
42
|
+
}),
|
|
68
43
|
lowPriorityFindingPlacement: 'inline',
|
|
69
44
|
workspaceDir: input.workspaceDir,
|
|
70
45
|
onProgress: input.onProgress,
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
|
-
import { getCustomPrompt } from 'doistbot-repo-config';
|
|
5
4
|
import { logger } from '../../core/logging.js';
|
|
6
|
-
import { fillPromptTemplate, loadPromptTemplateFile } from '../../core/prompt-template.js';
|
|
7
5
|
const MAX_BODY_CHARS = 4000;
|
|
8
6
|
let cachedStandardsSection = null;
|
|
9
7
|
const REVIEW_TASK_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
const PROMPT_FILE = path.resolve(REVIEW_TASK_DIR, '../../../review-prompt.md');
|
|
11
8
|
const STANDARDS_PROMPTS_DIR = path.resolve(REVIEW_TASK_DIR, '../../../src/review/prompts');
|
|
12
9
|
function loadOutlineMap() {
|
|
13
10
|
try {
|
|
@@ -51,29 +48,6 @@ export function loadStandardsSection() {
|
|
|
51
48
|
return '';
|
|
52
49
|
}
|
|
53
50
|
}
|
|
54
|
-
export async function resolvePromptTemplate({ octokit, owner, repo, promptFile, }) {
|
|
55
|
-
if (promptFile) {
|
|
56
|
-
const customPrompt = await getCustomPrompt({
|
|
57
|
-
octokit,
|
|
58
|
-
owner,
|
|
59
|
-
repo,
|
|
60
|
-
path: promptFile,
|
|
61
|
-
logger,
|
|
62
|
-
});
|
|
63
|
-
if (customPrompt) {
|
|
64
|
-
logger.info('Using custom prompt file', { path: promptFile });
|
|
65
|
-
return customPrompt;
|
|
66
|
-
}
|
|
67
|
-
logger.info('Custom prompt not found, using default', { path: promptFile });
|
|
68
|
-
}
|
|
69
|
-
return loadDefaultReviewPromptTemplate();
|
|
70
|
-
}
|
|
71
|
-
export function loadDefaultReviewPromptTemplate() {
|
|
72
|
-
return loadPromptTemplateFile({
|
|
73
|
-
fileName: PROMPT_FILE,
|
|
74
|
-
promptLabel: 'review',
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
51
|
function formatPrConversation(prConversation) {
|
|
78
52
|
const promptText = prConversation?.promptText.trim();
|
|
79
53
|
if (promptText) {
|
|
@@ -89,38 +63,17 @@ export function buildReviewPromptVariables({ repoOwner, repoName, prNumber, prTi
|
|
|
89
63
|
const condensedBody = safeBody
|
|
90
64
|
? safeBody.slice(0, MAX_BODY_CHARS)
|
|
91
65
|
: 'No PR description was provided.';
|
|
92
|
-
const conversation = formatPrConversation(prConversation);
|
|
93
|
-
const repoFull = `${repoOwner}/${repoName}`;
|
|
94
66
|
const fullDiffSection = diffSection + (truncated ? '\n[Diff was truncated to fit token limits]' : '');
|
|
95
67
|
return {
|
|
96
|
-
REPO_FULL:
|
|
68
|
+
REPO_FULL: `${repoOwner}/${repoName}`,
|
|
97
69
|
PR_NUMBER: String(prNumber),
|
|
98
70
|
PR_TITLE: prTitle || 'Untitled',
|
|
99
71
|
PR_AUTHOR: prAuthor,
|
|
100
72
|
BASE_REF: baseRef,
|
|
101
73
|
HEAD_REF: headRef,
|
|
102
74
|
PR_BODY: condensedBody,
|
|
103
|
-
PR_CONVERSATION:
|
|
75
|
+
PR_CONVERSATION: formatPrConversation(prConversation),
|
|
104
76
|
DIFF_SECTION: fullDiffSection,
|
|
105
77
|
STANDARDS_SECTION: standardsSection,
|
|
106
78
|
};
|
|
107
79
|
}
|
|
108
|
-
/**
|
|
109
|
-
* Builds the final prompt sent to the review summary model, combining metadata, description, and annotated diff.
|
|
110
|
-
*/
|
|
111
|
-
export function buildPrompt({ promptTemplate, repoOwner, repoName, prNumber, prTitle, prBody, prConversation, prAuthor, baseRef, headRef, diffSection, truncated, standardsSection, }) {
|
|
112
|
-
return fillPromptTemplate(promptTemplate, buildReviewPromptVariables({
|
|
113
|
-
repoOwner,
|
|
114
|
-
repoName,
|
|
115
|
-
prNumber,
|
|
116
|
-
prTitle,
|
|
117
|
-
prBody,
|
|
118
|
-
prConversation,
|
|
119
|
-
prAuthor,
|
|
120
|
-
baseRef,
|
|
121
|
-
headRef,
|
|
122
|
-
diffSection,
|
|
123
|
-
truncated,
|
|
124
|
-
standardsSection,
|
|
125
|
-
}));
|
|
126
|
-
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { logger } from '../../core/logging.js';
|
|
2
|
+
import { requestReviewSummary } from './summary-model.js';
|
|
3
|
+
export const MAX_SUMMARY_BODY_CHARS = 2000;
|
|
4
|
+
const MAX_REVIEW_OBSERVATION_CHARS = 500;
|
|
5
|
+
export function buildReviewSummaryPrompt({ prTitle, prBody, findings, reviewObservations = [], }) {
|
|
6
|
+
const safeTitle = prTitle.trim() || 'Untitled';
|
|
7
|
+
const safeBody = prBody.trim();
|
|
8
|
+
const condensedBody = safeBody
|
|
9
|
+
? safeBody.slice(0, MAX_SUMMARY_BODY_CHARS)
|
|
10
|
+
: 'No PR description was provided.';
|
|
11
|
+
const findingsBlock = findings.length > 0
|
|
12
|
+
? findings
|
|
13
|
+
.map((finding) => `<review-finding>\n${finding.trim()}\n</review-finding>`)
|
|
14
|
+
.join('\n\n')
|
|
15
|
+
: 'No issues were flagged in the inline comments.';
|
|
16
|
+
const observationsBlock = reviewObservations.length > 0
|
|
17
|
+
? reviewObservations
|
|
18
|
+
.map((observation) => observation.trim())
|
|
19
|
+
.filter(Boolean)
|
|
20
|
+
.slice(0, 8)
|
|
21
|
+
.map((observation) => `<review-observation>\n${observation.slice(0, MAX_REVIEW_OBSERVATION_CHARS)}\n</review-observation>`)
|
|
22
|
+
.join('\n\n') || 'No additional review observations were captured.'
|
|
23
|
+
: 'No additional review observations were captured.';
|
|
24
|
+
return `You are an expert code review summarizer for the Doist engineering team.
|
|
25
|
+
|
|
26
|
+
<pr_title>
|
|
27
|
+
${safeTitle}
|
|
28
|
+
</pr_title>
|
|
29
|
+
|
|
30
|
+
<pr_description>
|
|
31
|
+
${condensedBody}
|
|
32
|
+
</pr_description>
|
|
33
|
+
|
|
34
|
+
<review_findings>
|
|
35
|
+
${findingsBlock}
|
|
36
|
+
</review_findings>
|
|
37
|
+
|
|
38
|
+
<review_observations>
|
|
39
|
+
${observationsBlock}
|
|
40
|
+
</review_observations>
|
|
41
|
+
|
|
42
|
+
Write the complete GitHub review summary body. Synthesize the findings into grouped themes—do not mirror every inline comment one-for-one or reference the reviewers, models, focus passes, or review observations. Use review observations only as context for the overview. Do not mention issues not present in the <review-finding> blocks. If there are no <review-finding> blocks, state that no issues were flagged.
|
|
43
|
+
|
|
44
|
+
Structure:
|
|
45
|
+
- Start with one short sentence giving a concise overview of what changed.
|
|
46
|
+
- If issues exist, add a blank line, then the exact heading "Few things worth tightening:", then 1-4 concise bullets grouping what to improve.
|
|
47
|
+
- If no issues exist, do not add a heading or bullets.
|
|
48
|
+
|
|
49
|
+
Keep it human and compact: the opener should be 1 sentence, or 2 only if the PR is large and the second sentence adds useful context. Use a kind, warm, and supportive tone while staying concrete, constructive, and pragmatic. Avoid generic praise or flattery. Bullets should be short fragments or single sentences, not paragraphs. Do not include any <review-finding> or <review-observation> tags in the output.
|
|
50
|
+
|
|
51
|
+
Respond with only valid JSON in this exact shape:
|
|
52
|
+
{"summary":"<your summary>","comments":[]}`;
|
|
53
|
+
}
|
|
54
|
+
export async function summarizeReviewFindings({ credential, prTitle, prBody, findings, reviewObservations, workspaceDir, requestReview = requestReviewSummary, }) {
|
|
55
|
+
const prompt = buildReviewSummaryPrompt({
|
|
56
|
+
prTitle,
|
|
57
|
+
prBody,
|
|
58
|
+
findings,
|
|
59
|
+
reviewObservations,
|
|
60
|
+
});
|
|
61
|
+
logger.info('Summarizing review findings with the review summary model', {
|
|
62
|
+
findingsCount: findings.length,
|
|
63
|
+
reviewObservationsCount: reviewObservations?.length ?? 0,
|
|
64
|
+
});
|
|
65
|
+
const result = await requestReview({ credential, prompt, workspaceDir });
|
|
66
|
+
const summary = result?.summary?.trim() ?? '';
|
|
67
|
+
if (!summary) {
|
|
68
|
+
logger.warn('Review summary model returned an empty summary', {
|
|
69
|
+
findingsCount: findings.length,
|
|
70
|
+
});
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return summary;
|
|
74
|
+
}
|
|
75
|
+
export function buildFallbackSummary({ prTitle, findings, }) {
|
|
76
|
+
const safeTitle = prTitle.trim();
|
|
77
|
+
const base = safeTitle ? `This PR updates ${safeTitle}.` : 'This PR updates the codebase.';
|
|
78
|
+
if (findings.length === 0) {
|
|
79
|
+
return `${base} No issues were flagged in the reviewed diff.`;
|
|
80
|
+
}
|
|
81
|
+
return `${base}\n\nFew things worth tightening:\n- See the inline comments for the issues found in the review.`;
|
|
82
|
+
}
|