@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.
Files changed (65) hide show
  1. package/dist/actions/auth.js +23 -23
  2. package/dist/actions/doctor.js +3 -5
  3. package/dist/actions/review-usage.js +6 -1
  4. package/dist/actions/review.js +7 -11
  5. package/dist/auth.js +17 -17
  6. package/dist/config.js +14 -9
  7. package/dist/env.js +1 -2
  8. package/dist/file-store.js +1 -1
  9. package/dist/index.js +19 -1
  10. package/dist/terminal.js +0 -7
  11. package/package.json +5 -5
  12. package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +7 -21
  13. package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +8 -72
  14. package/sandbox/dist/core/bot-logins.js +0 -13
  15. package/sandbox/dist/core/datadog-metrics.js +0 -2
  16. package/sandbox/dist/core/pi.js +16 -13
  17. package/sandbox/dist/providers/config.js +1 -1
  18. package/sandbox/dist/providers/credentials.js +30 -0
  19. package/sandbox/dist/providers/gemini.js +4 -10
  20. package/sandbox/dist/providers/helpers.js +6 -21
  21. package/sandbox/dist/providers/index.js +0 -3
  22. package/sandbox/dist/providers/openai.js +1 -8
  23. package/sandbox/dist/providers/pi-review.js +16 -54
  24. package/sandbox/dist/providers/schemas.js +0 -39
  25. package/sandbox/dist/tasks/chat/chat.js +2 -1
  26. package/sandbox/dist/tasks/issue-summarize/context.js +1 -1
  27. package/sandbox/dist/tasks/issue-summarize/model.js +3 -1
  28. package/sandbox/dist/tasks/issue-summarize/summarize.js +1 -1
  29. package/sandbox/dist/tasks/issue-triage/context.js +1 -2
  30. package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +3 -3
  31. package/sandbox/dist/tasks/persist-logs/dd-proxy.js +2 -2
  32. package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +1 -1
  33. package/sandbox/dist/tasks/persist-logs/persist-logs.js +1 -1
  34. package/sandbox/dist/tasks/review/conversation-context.js +13 -7
  35. package/sandbox/dist/tasks/review/engines/dedupe.js +18 -9
  36. package/sandbox/dist/tasks/review/engines/multi-focus.js +26 -31
  37. package/sandbox/dist/tasks/review/engines/shared.js +11 -59
  38. package/sandbox/dist/tasks/review/local-git.js +3 -1
  39. package/sandbox/dist/tasks/review/local.js +4 -29
  40. package/sandbox/dist/tasks/review/prompt.js +2 -49
  41. package/sandbox/dist/tasks/review/review-summary.js +82 -0
  42. package/sandbox/dist/tasks/review/review.js +23 -57
  43. package/sandbox/dist/tasks/review/runner.js +35 -51
  44. package/sandbox/dist/tasks/review/summary-model.js +11 -2
  45. package/sandbox/dist/tasks/review/thinking-level.js +0 -2
  46. package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +7 -21
  47. package/sandbox/node_modules/doistbot-repo-config/dist/index.js +8 -72
  48. package/sandbox/_node_modules/doistbot-repo-config/dist/review-engine.d.ts +0 -11
  49. package/sandbox/_node_modules/doistbot-repo-config/dist/review-engine.js +0 -41
  50. package/sandbox/dist/providers/anthropic.js +0 -30
  51. package/sandbox/dist/tasks/review/author-format.js +0 -14
  52. package/sandbox/dist/tasks/review/author-profile.js +0 -67
  53. package/sandbox/dist/tasks/review/council/normalizer.js +0 -104
  54. package/sandbox/dist/tasks/review/council/orchestrator.js +0 -194
  55. package/sandbox/dist/tasks/review/council/provider-health.js +0 -31
  56. package/sandbox/dist/tasks/review/council/summary.js +0 -192
  57. package/sandbox/dist/tasks/review/council/types.js +0 -4
  58. package/sandbox/dist/tasks/review/council/voter.js +0 -384
  59. package/sandbox/dist/tasks/review/engines/council.js +0 -126
  60. package/sandbox/dist/tasks/review/engines/index.js +0 -12
  61. package/sandbox/dist/tasks/review/review-engine.js +0 -5
  62. package/sandbox/node_modules/doistbot-repo-config/dist/review-engine.d.ts +0 -11
  63. package/sandbox/node_modules/doistbot-repo-config/dist/review-engine.js +0 -41
  64. package/sandbox/review-prompt.md +0 -209
  65. 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', apiKey: apiKeys.gemini }] : []),
18
- ...(apiKeys?.openai ? [{ provider: 'openai', apiKey: apiKeys.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, apiKey, rawContext, workspaceDir, }) {
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 DEDUPE_MODEL_NAME = 'gemini-3-flash-preview';
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, geminiApiKey, }) {
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: DEDUPE_MODEL_NAME,
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
- if (!geminiApiKey) {
121
- logger.error('Multi-focus dedupe unavailable because Gemini API key is unavailable', {
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: DEDUPE_MODEL_NAME,
127
+ model: DEDUPE_MODEL_BY_PI_PROVIDER.openrouter,
124
128
  });
125
- throw new Error('Multi-focus dedupe requires a Gemini API key');
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: geminiApiKey,
130
- modelName: DEDUPE_MODEL_NAME,
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, prAuthor, prAuthorGreetingName, excludePrAuthorFromSummary) {
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 withReviewSummaryGreeting(`${base} No inline issues were flagged.`, prAuthor, prAuthorGreetingName, excludePrAuthorFromSummary);
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, prAuthor, prAuthorGreetingName, resolvePrAuthorGreetingName, excludePrAuthorFromSummary, reviewObservations, summaryApiKey, workspaceDir, }) {
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 resolvedPrAuthorGreetingName = excludePrAuthorFromSummary === true
123
- ? undefined
124
- : (prAuthorGreetingName ?? (await resolvePrAuthorGreetingName?.()));
125
- const synthesizedSummary = summaryApiKey == null
121
+ const synthesizedSummary = summaryCredential == null
126
122
  ? null
127
- : await summarizeCouncilReviews({
128
- apiKey: summaryApiKey,
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, prAuthor, resolvedPrAuthorGreetingName, excludePrAuthorFromSummary)
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, prAuthor, prAuthorGreetingName, resolvePrAuthorGreetingName, excludePrAuthorFromSummary, lowPriorityFindingPlacement, standardsSection, onProgress, }) {
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.councilModelsEnabled];
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
- const dedupedFindings = await dedupeMultiFocusFindings({
202
- findings: allFindings,
203
- availableModels,
204
- workspaceDir: ctx.workspaceDir,
205
- geminiApiKey: ctx.geminiApiKey,
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
- summaryApiKey: apiKeys.gemini,
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.councilModelsEnabled.includes('gemini')) {
9
- apiKeys.gemini = ctx.geminiApiKey;
6
+ if (ctx.reviewModelsEnabled.includes('gemini')) {
7
+ apiKeys.gemini = ctx.geminiCredential;
10
8
  }
11
- if (ctx.councilModelsEnabled.includes('openai')) {
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, logEngine, onProgress, }) {
38
- const apiKey = apiKeys[model];
39
- if (!apiKey) {
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
- apiKey,
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, logEngine ? { engine: logEngine } : undefined);
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 = resolveOriginBaseRef(runner);
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 { getLocalCustomPrompt, getLocalRepoConfig, parseCouncilModelsEnabled, resolveReviewEngine, } from 'doistbot-repo-config';
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
- councilEnabled: repoConfig?.review?.council?.enabled !== false,
67
- councilModelsEnabled: parseCouncilModelsEnabled(process.env.COUNCIL_MODELS_ENABLED),
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: repoFull,
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: 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
+ }