@doist/doistbot-cli 1.0.4 → 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 (62) hide show
  1. package/dist/actions/auth.js +23 -23
  2. package/dist/actions/doctor.js +3 -5
  3. package/dist/actions/review.js +5 -4
  4. package/dist/auth.js +17 -17
  5. package/dist/config.js +14 -9
  6. package/dist/env.js +1 -2
  7. package/dist/file-store.js +1 -1
  8. package/dist/terminal.js +0 -7
  9. package/package.json +5 -5
  10. package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +7 -21
  11. package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +8 -72
  12. package/sandbox/dist/core/bot-logins.js +0 -13
  13. package/sandbox/dist/core/datadog-metrics.js +0 -2
  14. package/sandbox/dist/core/pi.js +16 -13
  15. package/sandbox/dist/providers/config.js +1 -1
  16. package/sandbox/dist/providers/credentials.js +30 -0
  17. package/sandbox/dist/providers/gemini.js +4 -10
  18. package/sandbox/dist/providers/helpers.js +6 -21
  19. package/sandbox/dist/providers/index.js +0 -3
  20. package/sandbox/dist/providers/openai.js +1 -8
  21. package/sandbox/dist/providers/pi-review.js +16 -54
  22. package/sandbox/dist/providers/schemas.js +0 -39
  23. package/sandbox/dist/tasks/chat/chat.js +2 -1
  24. package/sandbox/dist/tasks/issue-summarize/context.js +1 -1
  25. package/sandbox/dist/tasks/issue-summarize/model.js +3 -1
  26. package/sandbox/dist/tasks/issue-summarize/summarize.js +1 -1
  27. package/sandbox/dist/tasks/issue-triage/context.js +1 -2
  28. package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +3 -3
  29. package/sandbox/dist/tasks/persist-logs/dd-proxy.js +2 -2
  30. package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +1 -1
  31. package/sandbox/dist/tasks/persist-logs/persist-logs.js +1 -1
  32. package/sandbox/dist/tasks/review/conversation-context.js +13 -7
  33. package/sandbox/dist/tasks/review/engines/dedupe.js +18 -9
  34. package/sandbox/dist/tasks/review/engines/multi-focus.js +26 -31
  35. package/sandbox/dist/tasks/review/engines/shared.js +11 -59
  36. package/sandbox/dist/tasks/review/local.js +4 -29
  37. package/sandbox/dist/tasks/review/prompt.js +2 -49
  38. package/sandbox/dist/tasks/review/review-summary.js +82 -0
  39. package/sandbox/dist/tasks/review/review.js +23 -57
  40. package/sandbox/dist/tasks/review/runner.js +35 -51
  41. package/sandbox/dist/tasks/review/summary-model.js +11 -2
  42. package/sandbox/dist/tasks/review/thinking-level.js +0 -2
  43. package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +7 -21
  44. package/sandbox/node_modules/doistbot-repo-config/dist/index.js +8 -72
  45. package/sandbox/_node_modules/doistbot-repo-config/dist/review-engine.d.ts +0 -11
  46. package/sandbox/_node_modules/doistbot-repo-config/dist/review-engine.js +0 -41
  47. package/sandbox/dist/providers/anthropic.js +0 -30
  48. package/sandbox/dist/tasks/review/author-format.js +0 -14
  49. package/sandbox/dist/tasks/review/author-profile.js +0 -67
  50. package/sandbox/dist/tasks/review/council/normalizer.js +0 -104
  51. package/sandbox/dist/tasks/review/council/orchestrator.js +0 -194
  52. package/sandbox/dist/tasks/review/council/provider-health.js +0 -31
  53. package/sandbox/dist/tasks/review/council/summary.js +0 -192
  54. package/sandbox/dist/tasks/review/council/types.js +0 -4
  55. package/sandbox/dist/tasks/review/council/voter.js +0 -384
  56. package/sandbox/dist/tasks/review/engines/council.js +0 -126
  57. package/sandbox/dist/tasks/review/engines/index.js +0 -12
  58. package/sandbox/dist/tasks/review/review-engine.js +0 -5
  59. package/sandbox/node_modules/doistbot-repo-config/dist/review-engine.d.ts +0 -11
  60. package/sandbox/node_modules/doistbot-repo-config/dist/review-engine.js +0 -41
  61. package/sandbox/review-prompt.md +0 -209
  62. package/sandbox/vote-prompt.md +0 -69
@@ -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
- }
@@ -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
+ }
@@ -1,27 +1,22 @@
1
1
  import { performance } from 'node:perf_hooks';
2
- import { parseCouncilModelsEnabled as parseConfiguredCouncilModelsEnabled } from 'doistbot-repo-config';
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
- function parseCouncilEnabled() {
18
- // Council is enabled by default unless explicitly disabled
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.COUNCIL_MODELS_ENABLED;
24
- return parseConfiguredCouncilModelsEnabled(raw, { defaultModels: [] });
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
- geminiApiKey: optionalEnv('GEMINI_API_KEY'),
32
+ openRouterApiKey: optionalEnv('OPENROUTER_API_KEY'),
39
33
  openaiApiKey: optionalEnv('OPENAI_KEY'),
40
- anthropicApiKey: optionalEnv('ANTHROPIC_KEY'),
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.councilModelsEnabled.includes('gemini') && ctx.geminiApiKey) {
51
- apiKeys.gemini = ctx.geminiApiKey;
40
+ if (ctx.reviewModelsEnabled.includes('gemini') && ctx.openRouterApiKey) {
41
+ apiKeys.gemini = createOpenRouterCredential(ctx.openRouterApiKey);
52
42
  }
53
- if (ctx.councilModelsEnabled.includes('openai') && ctx.openaiApiKey) {
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
- requestedReviewEngine: ctx.requestedReviewEngine,
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
- const promptTemplate = ctx.reviewEngine === 'council'
209
- ? await resolvePromptTemplate({
210
- octokit: readOctokit,
211
- owner: ctx.owner,
212
- repo: ctx.repo,
213
- })
214
- : undefined;
215
- reviewMetricModels = [...ctx.councilModelsEnabled];
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
- reviewEngine: ctx.reviewEngine,
233
- promptTemplate,
234
- apiKeys: {
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;