@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
|
@@ -1,27 +1,22 @@
|
|
|
1
1
|
import { performance } from 'node:perf_hooks';
|
|
2
|
-
import {
|
|
2
|
+
import { parseReviewModelsEnabled as parseConfiguredReviewModelsEnabled } from 'doistbot-repo-config';
|
|
3
3
|
import { recordTaskMetrics } from '../../core/datadog-metrics.js';
|
|
4
4
|
import { logger } from '../../core/logging.js';
|
|
5
|
+
import { createOpenRouterCredential } from '../../providers/credentials.js';
|
|
5
6
|
import { cloneRepository, createGitHubClients, parseBaseContext, requiredEnv, WORKSPACE_DIR, } from '../../core/shared.js';
|
|
6
|
-
import { resolvePrAuthorGreetingName, shouldExcludePrAuthorFromReviewSummary, } from './author-profile.js';
|
|
7
7
|
import { getReviewCheckRunTitle, isAnotherReviewInProgress, postReviewFailureCheckRun, postReviewInProgressCheckRun, postReviewSuccessCheckRun, postStaleReviewPointerIfHeadMoved, } from './check-run.js';
|
|
8
8
|
import { fetchReviewConversationContext } from './conversation-context.js';
|
|
9
9
|
import {} from './diff.js';
|
|
10
10
|
import { formatReviewForTerminal } from './format.js';
|
|
11
11
|
import { formatGithubReviewComments } from './github-comment-format.js';
|
|
12
|
-
import { resolvePromptTemplate } from './prompt.js';
|
|
13
|
-
import { parseReviewEngine } from './review-engine.js';
|
|
14
12
|
import { appendReviewFeedbackLink } from './review-feedback.js';
|
|
15
13
|
import { runSharedReview } from './runner.js';
|
|
16
14
|
const REVIEW_EVENT = 'COMMENT';
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return process.env.COUNCIL_ENABLED?.toLowerCase() !== 'false';
|
|
20
|
-
}
|
|
21
|
-
function parseCouncilModelsEnabled() {
|
|
15
|
+
const REQUIRED_PRODUCTION_REVIEW_MODELS = ['gemini', 'openai'];
|
|
16
|
+
function parseReviewModelsEnabled() {
|
|
22
17
|
// Parsed/defaulted in webhook-handler before being passed as task env.
|
|
23
|
-
const raw = process.env.
|
|
24
|
-
return
|
|
18
|
+
const raw = process.env.REVIEW_MODELS_ENABLED;
|
|
19
|
+
return parseConfiguredReviewModelsEnabled(raw, { defaultModels: [] });
|
|
25
20
|
}
|
|
26
21
|
function optionalEnv(name) {
|
|
27
22
|
const value = process.env[name]?.trim();
|
|
@@ -29,33 +24,25 @@ function optionalEnv(name) {
|
|
|
29
24
|
}
|
|
30
25
|
function parseContext() {
|
|
31
26
|
const base = parseBaseContext();
|
|
32
|
-
const requestedReviewEngine = parseReviewEngine(process.env.REVIEW_ENGINE);
|
|
33
27
|
return {
|
|
34
28
|
...base,
|
|
35
29
|
branch: requiredEnv('BRANCH'),
|
|
36
30
|
baseBranch: requiredEnv('BASE_BRANCH'),
|
|
37
31
|
headSha: optionalEnv('HEAD_SHA'),
|
|
38
|
-
|
|
32
|
+
openRouterApiKey: optionalEnv('OPENROUTER_API_KEY'),
|
|
39
33
|
openaiApiKey: optionalEnv('OPENAI_KEY'),
|
|
40
|
-
|
|
41
|
-
requestedReviewEngine,
|
|
42
|
-
reviewEngine: requestedReviewEngine,
|
|
43
|
-
councilEnabled: parseCouncilEnabled(),
|
|
44
|
-
councilModelsEnabled: parseCouncilModelsEnabled(),
|
|
34
|
+
reviewModelsEnabled: parseReviewModelsEnabled(),
|
|
45
35
|
dryRun: process.env.DRY_RUN?.toLowerCase() === 'true',
|
|
46
36
|
};
|
|
47
37
|
}
|
|
48
38
|
function getConversationSummaryApiKeys(ctx) {
|
|
49
39
|
const apiKeys = {};
|
|
50
|
-
if (ctx.
|
|
51
|
-
apiKeys.gemini = ctx.
|
|
40
|
+
if (ctx.reviewModelsEnabled.includes('gemini') && ctx.openRouterApiKey) {
|
|
41
|
+
apiKeys.gemini = createOpenRouterCredential(ctx.openRouterApiKey);
|
|
52
42
|
}
|
|
53
|
-
if (ctx.
|
|
43
|
+
if (ctx.reviewModelsEnabled.includes('openai') && ctx.openaiApiKey) {
|
|
54
44
|
apiKeys.openai = ctx.openaiApiKey;
|
|
55
45
|
}
|
|
56
|
-
if (ctx.councilModelsEnabled.includes('anthropic') && ctx.anthropicApiKey) {
|
|
57
|
-
apiKeys.anthropic = ctx.anthropicApiKey;
|
|
58
|
-
}
|
|
59
46
|
return apiKeys;
|
|
60
47
|
}
|
|
61
48
|
async function main() {
|
|
@@ -69,17 +56,8 @@ async function main() {
|
|
|
69
56
|
logger.setBaseContext(logContext);
|
|
70
57
|
logger.info('Review task started', {
|
|
71
58
|
dryRun: ctx.dryRun,
|
|
72
|
-
|
|
73
|
-
reviewEngine: ctx.reviewEngine,
|
|
74
|
-
councilEnabled: ctx.councilEnabled,
|
|
75
|
-
councilModelsEnabled: ctx.councilModelsEnabled,
|
|
59
|
+
reviewModelsEnabled: ctx.reviewModelsEnabled,
|
|
76
60
|
});
|
|
77
|
-
if (ctx.requestedReviewEngine !== ctx.reviewEngine) {
|
|
78
|
-
logger.warn('Requested review engine is not implemented yet, falling back', {
|
|
79
|
-
requestedReviewEngine: ctx.requestedReviewEngine,
|
|
80
|
-
effectiveReviewEngine: ctx.reviewEngine,
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
61
|
const { app, readOctokit, writeOctokit } = await createGitHubClients(ctx);
|
|
84
62
|
// GitHub's Check Runs API requires App-installation auth; calling it with
|
|
85
63
|
// writeOctokit (the doistbot PAT) returns 403. Centralize the choice here
|
|
@@ -193,7 +171,6 @@ async function main() {
|
|
|
193
171
|
]);
|
|
194
172
|
const pr = prResponse.data;
|
|
195
173
|
const prAuthor = pr.user?.login ?? 'unknown';
|
|
196
|
-
const excludePrAuthorFromSummary = shouldExcludePrAuthorFromReviewSummary(prAuthor);
|
|
197
174
|
function loadPrConversation() {
|
|
198
175
|
return fetchReviewConversationContext({
|
|
199
176
|
octokit: readOctokit,
|
|
@@ -205,14 +182,14 @@ async function main() {
|
|
|
205
182
|
workspaceDir: WORKSPACE_DIR,
|
|
206
183
|
});
|
|
207
184
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
185
|
+
reviewMetricModels = [...ctx.reviewModelsEnabled];
|
|
186
|
+
const reviewApiKeys = {};
|
|
187
|
+
if (ctx.openRouterApiKey) {
|
|
188
|
+
reviewApiKeys.gemini = createOpenRouterCredential(ctx.openRouterApiKey);
|
|
189
|
+
}
|
|
190
|
+
if (ctx.openaiApiKey) {
|
|
191
|
+
reviewApiKeys.openai = ctx.openaiApiKey;
|
|
192
|
+
}
|
|
216
193
|
const reviewResult = await runSharedReview({
|
|
217
194
|
owner: ctx.owner,
|
|
218
195
|
repo: ctx.repo,
|
|
@@ -223,21 +200,10 @@ async function main() {
|
|
|
223
200
|
prBody: pr.body ?? '',
|
|
224
201
|
loadPrConversation,
|
|
225
202
|
prAuthor,
|
|
226
|
-
resolvePrAuthorGreetingName: () => resolvePrAuthorGreetingName({
|
|
227
|
-
octokit: readOctokit,
|
|
228
|
-
login: prAuthor,
|
|
229
|
-
}),
|
|
230
|
-
excludePrAuthorFromSummary,
|
|
231
203
|
files: files,
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
gemini: ctx.geminiApiKey,
|
|
236
|
-
openai: ctx.openaiApiKey,
|
|
237
|
-
anthropic: ctx.anthropicApiKey,
|
|
238
|
-
},
|
|
239
|
-
councilEnabled: ctx.councilEnabled,
|
|
240
|
-
councilModelsEnabled: ctx.councilModelsEnabled,
|
|
204
|
+
apiKeys: reviewApiKeys,
|
|
205
|
+
reviewModelsEnabled: ctx.reviewModelsEnabled,
|
|
206
|
+
requiredReviewModels: REQUIRED_PRODUCTION_REVIEW_MODELS,
|
|
241
207
|
workspaceDir: WORKSPACE_DIR,
|
|
242
208
|
});
|
|
243
209
|
reviewMetricModels = reviewResult.metricModels;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import { logger } from '../../core/logging.js';
|
|
3
|
-
import {
|
|
3
|
+
import { hasReviewProviderCredential } from '../../providers/credentials.js';
|
|
4
|
+
import { runMultiFocusReviewEngine } from './engines/multi-focus.js';
|
|
4
5
|
import {} from './engines/types.js';
|
|
5
6
|
import { buildAnnotatedDiff, buildDiffSection } from './diff.js';
|
|
6
7
|
import { buildMultiFocusSharedPrompt } from './multi-focus-prompt.js';
|
|
7
|
-
import {
|
|
8
|
+
import { loadStandardsSection } from './prompt.js';
|
|
8
9
|
const REVIEW_GUIDANCE_FILES = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md'];
|
|
9
10
|
const MASKED_PR_AUTHOR_LABEL = 'PR author';
|
|
10
11
|
function getGuidanceFilePresence(workspaceDir) {
|
|
@@ -42,25 +43,15 @@ function buildEnginePrompt({ input, diffSection, truncated, standardsSection, pr
|
|
|
42
43
|
truncated,
|
|
43
44
|
standardsSection,
|
|
44
45
|
};
|
|
45
|
-
if (input.reviewEngine === 'council') {
|
|
46
|
-
const promptTemplate = input.promptTemplate;
|
|
47
|
-
if (!promptTemplate) {
|
|
48
|
-
throw new Error('Council review requires a prompt template');
|
|
49
|
-
}
|
|
50
|
-
return {
|
|
51
|
-
prompt: buildPrompt({
|
|
52
|
-
promptTemplate,
|
|
53
|
-
...sharedPromptArgs,
|
|
54
|
-
}),
|
|
55
|
-
promptTemplateLength: promptTemplate.length,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
46
|
return {
|
|
59
47
|
prompt: buildMultiFocusSharedPrompt(sharedPromptArgs),
|
|
60
48
|
};
|
|
61
49
|
}
|
|
62
|
-
function getEnabledReviewModels({ apiKeys,
|
|
63
|
-
return
|
|
50
|
+
function getEnabledReviewModels({ apiKeys, reviewModelsEnabled, }) {
|
|
51
|
+
return reviewModelsEnabled.filter((model) => hasReviewProviderCredential(apiKeys[model]));
|
|
52
|
+
}
|
|
53
|
+
function getMissingRequiredReviewModels(availableModels, requiredReviewModels = []) {
|
|
54
|
+
return requiredReviewModels.filter((model) => !availableModels.includes(model));
|
|
64
55
|
}
|
|
65
56
|
export async function runSharedReview(input) {
|
|
66
57
|
const { annotatedFiles, lineLookup } = buildAnnotatedDiff(input.files);
|
|
@@ -85,35 +76,40 @@ export async function runSharedReview(input) {
|
|
|
85
76
|
providerFailures: [],
|
|
86
77
|
};
|
|
87
78
|
}
|
|
88
|
-
|
|
79
|
+
const configuredAvailableModels = getEnabledReviewModels(input);
|
|
80
|
+
if (configuredAvailableModels.length === 0) {
|
|
89
81
|
logger.error('No enabled review models with API keys are available, skipping review', {
|
|
90
|
-
|
|
91
|
-
council_models_enabled: input.councilModelsEnabled,
|
|
82
|
+
review_models_enabled: input.reviewModelsEnabled,
|
|
92
83
|
});
|
|
93
84
|
return {
|
|
94
85
|
status: 'skipped',
|
|
95
86
|
reason: 'no_models',
|
|
96
87
|
availableModels: [],
|
|
97
|
-
metricModels: [...input.
|
|
98
|
-
mode:
|
|
88
|
+
metricModels: [...input.reviewModelsEnabled],
|
|
89
|
+
mode: 'multi_focus',
|
|
99
90
|
providerFailures: [],
|
|
100
91
|
};
|
|
101
92
|
}
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
:
|
|
107
|
-
|
|
108
|
-
logger.warn('Failed to resolve PR author greeting name for review summary', {
|
|
109
|
-
error: error instanceof Error ? error.message : String(error),
|
|
93
|
+
const missingRequiredModels = getMissingRequiredReviewModels(configuredAvailableModels, input.requiredReviewModels);
|
|
94
|
+
if (missingRequiredModels.length > 0) {
|
|
95
|
+
logger.error('Required review models with API keys are unavailable, skipping review', {
|
|
96
|
+
review_models_enabled: input.reviewModelsEnabled,
|
|
97
|
+
available_models: configuredAvailableModels,
|
|
98
|
+
missing_required_models: missingRequiredModels,
|
|
110
99
|
});
|
|
111
|
-
return
|
|
112
|
-
|
|
100
|
+
return {
|
|
101
|
+
status: 'skipped',
|
|
102
|
+
reason: 'no_models',
|
|
103
|
+
availableModels: configuredAvailableModels,
|
|
104
|
+
metricModels: [...input.reviewModelsEnabled],
|
|
105
|
+
mode: 'multi_focus',
|
|
106
|
+
providerFailures: [],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
113
109
|
const prConversationPromise = input.loadPrConversation?.() ?? Promise.resolve(undefined);
|
|
114
110
|
const prConversation = maskPrAuthorInConversation(await prConversationPromise, input.prAuthor);
|
|
115
111
|
const standardsSection = loadStandardsSection();
|
|
116
|
-
const { prompt
|
|
112
|
+
const { prompt } = buildEnginePrompt({
|
|
117
113
|
input,
|
|
118
114
|
diffSection,
|
|
119
115
|
truncated,
|
|
@@ -121,9 +117,8 @@ export async function runSharedReview(input) {
|
|
|
121
117
|
prConversation,
|
|
122
118
|
});
|
|
123
119
|
logger.info('Review prompt prepared', {
|
|
124
|
-
engine:
|
|
120
|
+
engine: 'multi_focus',
|
|
125
121
|
prompt_length: prompt.length,
|
|
126
|
-
prompt_template_length: promptTemplateLength,
|
|
127
122
|
pr_body_length: input.prBody.trim().slice(0, 4000).length,
|
|
128
123
|
pr_conversation_length: prConversation?.promptText.trim().length ?? 0,
|
|
129
124
|
diff_length: diffSection.length,
|
|
@@ -134,39 +129,28 @@ export async function runSharedReview(input) {
|
|
|
134
129
|
guidance_files: getGuidanceFilePresence(input.workspaceDir),
|
|
135
130
|
});
|
|
136
131
|
input.onProgress?.({ type: 'prompt_prepared', fileCount: annotatedFiles.length });
|
|
137
|
-
const runReviewEngine = getReviewEngineRunner(input.reviewEngine);
|
|
138
132
|
const engineContext = {
|
|
139
133
|
owner: input.owner,
|
|
140
134
|
repo: input.repo,
|
|
141
135
|
prNumber: input.prNumber,
|
|
142
136
|
workspaceDir: input.workspaceDir,
|
|
143
|
-
|
|
137
|
+
geminiCredential: input.apiKeys.gemini,
|
|
144
138
|
openaiApiKey: input.apiKeys.openai,
|
|
145
|
-
|
|
146
|
-
reviewEngine: input.reviewEngine,
|
|
147
|
-
councilEnabled: input.councilEnabled,
|
|
148
|
-
councilModelsEnabled: input.councilModelsEnabled,
|
|
139
|
+
reviewModelsEnabled: input.reviewModelsEnabled,
|
|
149
140
|
};
|
|
150
|
-
const { review, availableModels, metricModels, mode, providerFailures } = await
|
|
141
|
+
const { review, availableModels, metricModels, mode, providerFailures } = await runMultiFocusReviewEngine({
|
|
151
142
|
ctx: engineContext,
|
|
152
143
|
prompt,
|
|
153
144
|
standardsSection,
|
|
154
145
|
lineLookup,
|
|
155
146
|
prTitle: input.prTitle,
|
|
156
147
|
prBody: input.prBody,
|
|
157
|
-
prAuthor: input.prAuthor,
|
|
158
|
-
prAuthorGreetingName: input.prAuthorGreetingName,
|
|
159
|
-
resolvePrAuthorGreetingName: prAuthorGreetingNamePromise
|
|
160
|
-
? () => prAuthorGreetingNamePromise
|
|
161
|
-
: undefined,
|
|
162
|
-
excludePrAuthorFromSummary: input.excludePrAuthorFromSummary,
|
|
163
148
|
lowPriorityFindingPlacement: input.lowPriorityFindingPlacement,
|
|
164
149
|
onProgress: input.onProgress,
|
|
165
150
|
});
|
|
166
151
|
if (availableModels.length === 0) {
|
|
167
152
|
logger.error('No enabled review models with API keys are available, skipping review', {
|
|
168
|
-
|
|
169
|
-
council_models_enabled: input.councilModelsEnabled,
|
|
153
|
+
review_models_enabled: input.reviewModelsEnabled,
|
|
170
154
|
});
|
|
171
155
|
return {
|
|
172
156
|
status: 'skipped',
|
|
@@ -200,7 +184,7 @@ export async function runSharedReview(input) {
|
|
|
200
184
|
};
|
|
201
185
|
}
|
|
202
186
|
logger.info('Review completed', {
|
|
203
|
-
engine:
|
|
187
|
+
engine: 'multi_focus',
|
|
204
188
|
mode,
|
|
205
189
|
comments_count: review.comments.length,
|
|
206
190
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { logger } from '../../core/logging.js';
|
|
2
2
|
import { invokePiPrompt } from '../../core/pi.js';
|
|
3
3
|
import { WORKSPACE_DIR } from '../../core/shared.js';
|
|
4
|
+
import { resolveReviewProviderCredential } from '../../providers/credentials.js';
|
|
4
5
|
const REVIEW_SUMMARY_TIMEOUT_MS = 5 * 60 * 1000;
|
|
5
6
|
const DIFF_SIDE = {
|
|
6
7
|
LEFT: 'LEFT',
|
|
@@ -9,11 +10,19 @@ const DIFF_SIDE = {
|
|
|
9
10
|
function getWorkspaceDir() {
|
|
10
11
|
return WORKSPACE_DIR;
|
|
11
12
|
}
|
|
12
|
-
export async function requestReviewSummary({
|
|
13
|
+
export async function requestReviewSummary({ credential, prompt, workspaceDir, }) {
|
|
13
14
|
try {
|
|
15
|
+
const resolvedCredential = resolveReviewProviderCredential(credential);
|
|
16
|
+
if (!resolvedCredential) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
14
19
|
const result = await invokePiPrompt({
|
|
15
20
|
provider: 'gemini',
|
|
16
|
-
apiKey,
|
|
21
|
+
apiKey: resolvedCredential.apiKey,
|
|
22
|
+
...(resolvedCredential.piProviderId
|
|
23
|
+
? { piProviderId: resolvedCredential.piProviderId }
|
|
24
|
+
: {}),
|
|
25
|
+
...(resolvedCredential.modelName ? { modelName: resolvedCredential.modelName } : {}),
|
|
17
26
|
prompt,
|
|
18
27
|
cwd: workspaceDir ?? getWorkspaceDir(),
|
|
19
28
|
timeoutMs: REVIEW_SUMMARY_TIMEOUT_MS,
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { Logger } from 'doistbot-logging';
|
|
2
2
|
import type { Octokit } from 'octokit';
|
|
3
3
|
export declare const CONFIG_FILE_PATH = ".github/doistbot.yml";
|
|
4
|
-
export declare const
|
|
5
|
-
|
|
4
|
+
export declare const DEFAULT_REVIEW_MODELS: readonly ["gemini", "openai"];
|
|
5
|
+
export declare const REQUIRED_REVIEW_MODELS: readonly ["gemini", "openai"];
|
|
6
|
+
type ParseReviewModelsEnabledOptions = {
|
|
6
7
|
defaultModels?: readonly string[];
|
|
8
|
+
requiredModels?: readonly string[];
|
|
7
9
|
};
|
|
8
10
|
export type RepoConfig = {
|
|
9
11
|
review?: {
|
|
@@ -11,11 +13,6 @@ export type RepoConfig = {
|
|
|
11
13
|
skip_drafts?: boolean;
|
|
12
14
|
skip_title_pattern?: string;
|
|
13
15
|
skip_branch_pattern?: string;
|
|
14
|
-
prompt_file?: string;
|
|
15
|
-
engine?: 'council' | 'multi_focus';
|
|
16
|
-
council?: {
|
|
17
|
-
enabled: boolean;
|
|
18
|
-
};
|
|
19
16
|
/**
|
|
20
17
|
* @deprecated Ignored as of #274. Doistbot now posts a GitHub Check
|
|
21
18
|
* Run for every `/review` (always-on; no opt-in) when `HEAD_SHA` is
|
|
@@ -53,17 +50,6 @@ export declare function getLocalRepoConfig({ workspaceDir, logger, }: {
|
|
|
53
50
|
workspaceDir: string;
|
|
54
51
|
logger: Logger;
|
|
55
52
|
}): RepoConfig | null;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
*/
|
|
60
|
-
export declare function getCustomPrompt({ octokit, owner, repo, path, ref, logger, }: FetchFileParams): Promise<string | null>;
|
|
61
|
-
export declare function getLocalCustomPrompt({ workspaceDir, path: promptPath, logger, }: {
|
|
62
|
-
workspaceDir: string;
|
|
63
|
-
path: string;
|
|
64
|
-
logger: Logger;
|
|
65
|
-
}): string | null;
|
|
66
|
-
export declare function parseCouncilModelsEnabled(raw: string | undefined, { defaultModels }?: ParseCouncilModelsEnabledOptions): string[];
|
|
67
|
-
export declare function getDefaultCouncilModels(): string[];
|
|
68
|
-
export type { ReviewEngine } from './review-engine.js';
|
|
69
|
-
export { DEFAULT_REVIEW_ENGINE, parseReviewEngine, resolveReviewEngine } from './review-engine.js';
|
|
53
|
+
export declare function parseReviewModelsEnabled(raw: string | undefined, { defaultModels, requiredModels, }?: ParseReviewModelsEnabledOptions): string[];
|
|
54
|
+
export declare function getDefaultReviewModels(): string[];
|
|
55
|
+
export {};
|
|
@@ -2,19 +2,11 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { load as yamlLoad } from 'js-yaml';
|
|
4
4
|
export const CONFIG_FILE_PATH = '.github/doistbot.yml';
|
|
5
|
-
export const
|
|
5
|
+
export const DEFAULT_REVIEW_MODELS = ['gemini', 'openai'];
|
|
6
|
+
export const REQUIRED_REVIEW_MODELS = DEFAULT_REVIEW_MODELS;
|
|
6
7
|
function parseRepoConfigContent(content) {
|
|
7
8
|
return yamlLoad(content);
|
|
8
9
|
}
|
|
9
|
-
function resolveWorkspacePath(workspaceDir, repoPath) {
|
|
10
|
-
const root = path.resolve(workspaceDir);
|
|
11
|
-
const resolved = path.resolve(root, repoPath);
|
|
12
|
-
const relative = path.relative(root, resolved);
|
|
13
|
-
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
14
|
-
return null;
|
|
15
|
-
}
|
|
16
|
-
return resolved;
|
|
17
|
-
}
|
|
18
10
|
async function fetchFileContent({ octokit, owner, repo, path, ref, logger, }) {
|
|
19
11
|
const params = {
|
|
20
12
|
owner,
|
|
@@ -99,67 +91,11 @@ export function getLocalRepoConfig({ workspaceDir, logger, }) {
|
|
|
99
91
|
return null;
|
|
100
92
|
}
|
|
101
93
|
}
|
|
102
|
-
|
|
103
|
-
* Fetches a custom prompt file from the repository.
|
|
104
|
-
* Returns null if the file doesn't exist.
|
|
105
|
-
*/
|
|
106
|
-
export async function getCustomPrompt({ octokit, owner, repo, path, ref, logger, }) {
|
|
107
|
-
let content;
|
|
108
|
-
try {
|
|
109
|
-
content = await fetchFileContent({ octokit, owner, repo, path, ref, logger });
|
|
110
|
-
}
|
|
111
|
-
catch (error) {
|
|
112
|
-
logger.warn('Error fetching custom prompt', {
|
|
113
|
-
component: 'repo-config',
|
|
114
|
-
repository: `${owner}/${repo}`,
|
|
115
|
-
path,
|
|
116
|
-
error: error instanceof Error ? error.message : String(error),
|
|
117
|
-
});
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
if (content !== null) {
|
|
121
|
-
logger.info('Loaded custom prompt', {
|
|
122
|
-
component: 'repo-config',
|
|
123
|
-
repository: `${owner}/${repo}`,
|
|
124
|
-
path,
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
return content;
|
|
128
|
-
}
|
|
129
|
-
export function getLocalCustomPrompt({ workspaceDir, path: promptPath, logger, }) {
|
|
130
|
-
const resolvedPath = resolveWorkspacePath(workspaceDir, promptPath);
|
|
131
|
-
if (resolvedPath === null) {
|
|
132
|
-
logger.warn('Custom prompt path escapes workspace', {
|
|
133
|
-
component: 'repo-config',
|
|
134
|
-
path: promptPath,
|
|
135
|
-
});
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
if (!fs.existsSync(resolvedPath)) {
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
141
|
-
try {
|
|
142
|
-
const content = fs.readFileSync(resolvedPath, 'utf8');
|
|
143
|
-
logger.info('Loaded local custom prompt', {
|
|
144
|
-
component: 'repo-config',
|
|
145
|
-
path: promptPath,
|
|
146
|
-
});
|
|
147
|
-
return content;
|
|
148
|
-
}
|
|
149
|
-
catch (error) {
|
|
150
|
-
logger.warn('Failed to load local custom prompt', {
|
|
151
|
-
component: 'repo-config',
|
|
152
|
-
path: promptPath,
|
|
153
|
-
error: error instanceof Error ? error.message : String(error),
|
|
154
|
-
});
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
export function parseCouncilModelsEnabled(raw, { defaultModels = DEFAULT_COUNCIL_MODELS } = {}) {
|
|
94
|
+
export function parseReviewModelsEnabled(raw, { defaultModels = DEFAULT_REVIEW_MODELS, requiredModels = REQUIRED_REVIEW_MODELS, } = {}) {
|
|
159
95
|
if (!raw?.trim()) {
|
|
160
96
|
return [...defaultModels];
|
|
161
97
|
}
|
|
162
|
-
const allowedModels = new Set(
|
|
98
|
+
const allowedModels = new Set(DEFAULT_REVIEW_MODELS);
|
|
163
99
|
const models = [];
|
|
164
100
|
for (const token of raw.split(',')) {
|
|
165
101
|
const model = token.trim().toLowerCase();
|
|
@@ -170,9 +106,9 @@ export function parseCouncilModelsEnabled(raw, { defaultModels = DEFAULT_COUNCIL
|
|
|
170
106
|
models.push(model);
|
|
171
107
|
}
|
|
172
108
|
}
|
|
173
|
-
|
|
109
|
+
const hasRequiredModels = requiredModels.every((model) => models.includes(model));
|
|
110
|
+
return models.length > 0 && hasRequiredModels ? models : [...defaultModels];
|
|
174
111
|
}
|
|
175
|
-
export function
|
|
176
|
-
return [...
|
|
112
|
+
export function getDefaultReviewModels() {
|
|
113
|
+
return [...DEFAULT_REVIEW_MODELS];
|
|
177
114
|
}
|
|
178
|
-
export { DEFAULT_REVIEW_ENGINE, parseReviewEngine, resolveReviewEngine } from './review-engine.js';
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export declare const DEFAULT_REVIEW_ENGINE: "multi_focus";
|
|
2
|
-
export type ReviewEngine = 'council' | 'multi_focus';
|
|
3
|
-
export declare function parseReviewEngine(raw: unknown): ReviewEngine | null;
|
|
4
|
-
export declare function resolveReviewEngine({ configuredEngine, envEngine, logger, repository, }: {
|
|
5
|
-
configuredEngine?: unknown;
|
|
6
|
-
envEngine?: unknown;
|
|
7
|
-
logger?: {
|
|
8
|
-
warn: (message: string, context?: Record<string, unknown>) => void;
|
|
9
|
-
};
|
|
10
|
-
repository?: string;
|
|
11
|
-
}): ReviewEngine;
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
export const DEFAULT_REVIEW_ENGINE = 'multi_focus';
|
|
2
|
-
function isReviewEngine(value) {
|
|
3
|
-
return value === 'council' || value === 'multi_focus';
|
|
4
|
-
}
|
|
5
|
-
export function parseReviewEngine(raw) {
|
|
6
|
-
if (typeof raw !== 'string') {
|
|
7
|
-
return null;
|
|
8
|
-
}
|
|
9
|
-
const normalized = raw.trim().toLowerCase();
|
|
10
|
-
if (!normalized) {
|
|
11
|
-
return null;
|
|
12
|
-
}
|
|
13
|
-
return isReviewEngine(normalized) ? normalized : null;
|
|
14
|
-
}
|
|
15
|
-
export function resolveReviewEngine({ configuredEngine, envEngine, logger, repository, }) {
|
|
16
|
-
const configured = parseReviewEngine(configuredEngine);
|
|
17
|
-
if (configured) {
|
|
18
|
-
return configured;
|
|
19
|
-
}
|
|
20
|
-
if (configuredEngine != null) {
|
|
21
|
-
logger?.warn('Invalid configured review engine, checking environment override next', {
|
|
22
|
-
component: 'review-engine',
|
|
23
|
-
repository,
|
|
24
|
-
configured_engine: configuredEngine,
|
|
25
|
-
next_source: 'env',
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
const fromEnv = parseReviewEngine(envEngine);
|
|
29
|
-
if (fromEnv) {
|
|
30
|
-
return fromEnv;
|
|
31
|
-
}
|
|
32
|
-
if (envEngine != null) {
|
|
33
|
-
logger?.warn('Invalid env review engine, falling back', {
|
|
34
|
-
component: 'review-engine',
|
|
35
|
-
repository,
|
|
36
|
-
env_engine: envEngine,
|
|
37
|
-
fallback: DEFAULT_REVIEW_ENGINE,
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
return DEFAULT_REVIEW_ENGINE;
|
|
41
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Anthropic provider for the multi-model code review system.
|
|
3
|
-
* Uses the Pi harness instead of the Claude CLI.
|
|
4
|
-
*/
|
|
5
|
-
import { DEFAULT_PI_MODELS } from '../core/pi.js';
|
|
6
|
-
import { resolveProviderTimeoutMs } from './config.js';
|
|
7
|
-
import { extractJsonFromResponse } from './helpers.js';
|
|
8
|
-
import { runPiReviewProvider } from './pi-review.js';
|
|
9
|
-
export class AnthropicProvider {
|
|
10
|
-
name = 'anthropic';
|
|
11
|
-
async review(prompt, config, logOptions) {
|
|
12
|
-
return await runPiReviewProvider({
|
|
13
|
-
provider: this.name,
|
|
14
|
-
modelName: config.modelName ?? DEFAULT_PI_MODELS.anthropic,
|
|
15
|
-
prompt,
|
|
16
|
-
config: {
|
|
17
|
-
...config,
|
|
18
|
-
timeout: resolveProviderTimeoutMs(config.timeout),
|
|
19
|
-
},
|
|
20
|
-
prompts: config.jsonSchema
|
|
21
|
-
? [
|
|
22
|
-
prompt,
|
|
23
|
-
`${prompt}\n\nReturn only JSON that matches the provided schema. Do not include analysis text.`,
|
|
24
|
-
]
|
|
25
|
-
: [prompt],
|
|
26
|
-
parseOutput: (output) => JSON.parse(extractJsonFromResponse(output)),
|
|
27
|
-
logOptions,
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export const REVIEW_AUTHOR_EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
2
|
-
export function normalizePrAuthor(prAuthor) {
|
|
3
|
-
return (prAuthor ?? '').trim().replace(/^@+/, '');
|
|
4
|
-
}
|
|
5
|
-
export function formatPrAuthorForGreeting(prAuthor) {
|
|
6
|
-
const normalizedAuthor = normalizePrAuthor(prAuthor);
|
|
7
|
-
if (!normalizedAuthor || normalizedAuthor.toLowerCase() === 'unknown') {
|
|
8
|
-
return null;
|
|
9
|
-
}
|
|
10
|
-
if (REVIEW_AUTHOR_EMAIL_PATTERN.test(normalizedAuthor)) {
|
|
11
|
-
return null;
|
|
12
|
-
}
|
|
13
|
-
return normalizedAuthor;
|
|
14
|
-
}
|