@doist/doistbot-cli 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/auth.js +62 -28
- package/dist/actions/doctor.js +16 -11
- package/dist/actions/review.js +77 -11
- package/dist/auth.js +53 -8
- package/dist/config.js +40 -6
- package/dist/env.js +1 -1
- package/dist/file-store.js +1 -1
- package/dist/runtime.js +1 -1
- package/dist/terminal.js +2 -7
- package/package.json +7 -7
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +5 -23
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +9 -85
- package/sandbox/dist/core/bot-logins.js +0 -13
- package/sandbox/dist/core/check-run.js +16 -0
- package/sandbox/dist/core/datadog-metrics.js +6 -54
- package/sandbox/dist/core/logger-span-processor.js +78 -0
- package/sandbox/dist/core/pi.js +270 -186
- package/sandbox/dist/core/tracing-setup.js +15 -0
- package/sandbox/dist/core/tracing.js +71 -0
- package/sandbox/dist/main.js +4 -1
- package/sandbox/dist/providers/config.js +1 -1
- package/sandbox/dist/providers/credentials.js +40 -0
- package/sandbox/dist/providers/gemini.js +8 -10
- package/sandbox/dist/providers/helpers.js +18 -38
- package/sandbox/dist/providers/index.js +4 -3
- package/sandbox/dist/providers/openai.js +1 -8
- package/sandbox/dist/providers/openrouter-pi.js +25 -0
- package/sandbox/dist/providers/pi-review.js +28 -55
- package/sandbox/dist/providers/schemas.js +0 -39
- package/sandbox/dist/tasks/chat/chat.js +162 -101
- package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +210 -185
- package/sandbox/dist/tasks/issue-summarize/context.js +1 -1
- package/sandbox/dist/tasks/issue-summarize/model.js +4 -2
- package/sandbox/dist/tasks/issue-summarize/summarize.js +1 -1
- package/sandbox/dist/tasks/issue-triage/clone.js +40 -0
- package/sandbox/dist/tasks/issue-triage/context.js +25 -2
- package/sandbox/dist/tasks/issue-triage/fix-attempt.js +31 -8
- package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +154 -101
- package/sandbox/dist/tasks/issue-triage/fix-loop/runners.js +15 -6
- package/sandbox/dist/tasks/issue-triage/model.js +120 -36
- package/sandbox/dist/tasks/issue-triage/output.js +27 -0
- package/sandbox/dist/tasks/issue-triage/pr-creator.js +3 -2
- package/sandbox/dist/tasks/issue-triage/resolution.js +26 -0
- package/sandbox/dist/tasks/issue-triage/routing.js +9 -9
- package/sandbox/dist/tasks/issue-triage/side-effects.js +9 -0
- package/sandbox/dist/tasks/issue-triage/triage.js +288 -418
- package/sandbox/dist/tasks/persist-logs/dd-proxy.js +2 -2
- package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +1 -1
- package/sandbox/dist/tasks/persist-logs/persist-logs.js +1 -1
- package/sandbox/dist/tasks/review/check-run.js +2 -1
- package/sandbox/dist/tasks/review/conversation-context.js +15 -8
- package/sandbox/dist/tasks/review/engines/dedupe.js +19 -9
- package/sandbox/dist/tasks/review/engines/multi-focus.js +62 -36
- package/sandbox/dist/tasks/review/engines/shared.js +15 -61
- package/sandbox/dist/tasks/review/github-comment-format.js +64 -10
- package/sandbox/dist/tasks/review/local.js +2 -29
- package/sandbox/dist/tasks/review/prompt.js +2 -49
- package/sandbox/dist/tasks/review/review-summary.js +120 -0
- package/sandbox/dist/tasks/review/review.js +49 -69
- package/sandbox/dist/tasks/review/runner.js +57 -52
- package/sandbox/dist/tasks/review/summary-model.js +172 -16
- package/sandbox/dist/tasks/review/thinking-level.js +4 -2
- package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +5 -23
- package/sandbox/node_modules/doistbot-repo-config/dist/index.js +9 -85
- package/sandbox/_node_modules/doistbot-repo-config/dist/review-engine.d.ts +0 -11
- package/sandbox/_node_modules/doistbot-repo-config/dist/review-engine.js +0 -41
- package/sandbox/dist/providers/anthropic.js +0 -30
- package/sandbox/dist/tasks/review/author-format.js +0 -14
- package/sandbox/dist/tasks/review/author-profile.js +0 -67
- package/sandbox/dist/tasks/review/council/normalizer.js +0 -104
- package/sandbox/dist/tasks/review/council/orchestrator.js +0 -194
- package/sandbox/dist/tasks/review/council/provider-health.js +0 -31
- package/sandbox/dist/tasks/review/council/summary.js +0 -192
- package/sandbox/dist/tasks/review/council/types.js +0 -4
- package/sandbox/dist/tasks/review/council/voter.js +0 -384
- package/sandbox/dist/tasks/review/engines/council.js +0 -126
- package/sandbox/dist/tasks/review/engines/index.js +0 -12
- package/sandbox/dist/tasks/review/review-engine.js +0 -5
- package/sandbox/node_modules/doistbot-repo-config/dist/review-engine.d.ts +0 -11
- package/sandbox/node_modules/doistbot-repo-config/dist/review-engine.js +0 -41
- package/sandbox/review-prompt.md +0 -209
- package/sandbox/vote-prompt.md +0 -69
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Orchestrator module for the multi-model code review council.
|
|
3
|
-
* Coordinates parallel model reviews, normalization, and consensus voting.
|
|
4
|
-
*/
|
|
5
|
-
import { logger } from '../../../core/logging.js';
|
|
6
|
-
import { getProvider } from '../../../providers/index.js';
|
|
7
|
-
import { REVIEW_SCHEMA } from '../../../providers/schemas.js';
|
|
8
|
-
import { getReviewThinkingLevel } from '../thinking-level.js';
|
|
9
|
-
import { normalizeFindings } from './normalizer.js';
|
|
10
|
-
import { DeliveryProviderHealthTracker } from './provider-health.js';
|
|
11
|
-
import { getApprovedFindings, runVoting } from './voter.js';
|
|
12
|
-
/**
|
|
13
|
-
* Runs a review with a single model.
|
|
14
|
-
*/
|
|
15
|
-
async function runModelReview(model, prompt, apiKey, workspaceDir, onProgress) {
|
|
16
|
-
const provider = getProvider(model);
|
|
17
|
-
const config = {
|
|
18
|
-
apiKey,
|
|
19
|
-
workspaceDir,
|
|
20
|
-
jsonSchema: REVIEW_SCHEMA,
|
|
21
|
-
thinkingLevel: getReviewThinkingLevel(model),
|
|
22
|
-
onProgress,
|
|
23
|
-
};
|
|
24
|
-
return provider.review(prompt, config);
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Display names for models in review summaries.
|
|
28
|
-
*/
|
|
29
|
-
const MODEL_DISPLAY_NAMES = {
|
|
30
|
-
gemini: 'GEMINI',
|
|
31
|
-
openai: 'CODEX',
|
|
32
|
-
anthropic: 'CLAUDE',
|
|
33
|
-
};
|
|
34
|
-
/**
|
|
35
|
-
* Combines summaries from multiple models into a single summary.
|
|
36
|
-
*/
|
|
37
|
-
function combineSummaries(results) {
|
|
38
|
-
const validSummaries = results
|
|
39
|
-
.filter((r) => !r.error && r.summary.trim())
|
|
40
|
-
.map((r) => `**${MODEL_DISPLAY_NAMES[r.model] || r.model.toUpperCase()}:** ${r.summary.trim()}`);
|
|
41
|
-
if (validSummaries.length === 0) {
|
|
42
|
-
return 'No valid summaries from any model.';
|
|
43
|
-
}
|
|
44
|
-
if (validSummaries.length === 1) {
|
|
45
|
-
// If only one model returned a valid summary, use it directly
|
|
46
|
-
return results.find((r) => !r.error && r.summary.trim())?.summary ?? '';
|
|
47
|
-
}
|
|
48
|
-
return validSummaries.join('\n\n---\n\n');
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Collects all findings from all model results.
|
|
52
|
-
*/
|
|
53
|
-
function collectAllFindings(results) {
|
|
54
|
-
const allFindings = [];
|
|
55
|
-
for (const result of results) {
|
|
56
|
-
if (!result.error) {
|
|
57
|
-
allFindings.push(...result.findings);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return allFindings;
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Runs a full council review with multiple models, normalization, and voting.
|
|
64
|
-
*/
|
|
65
|
-
export async function runCouncilReview({ prompt, apiKeys, repository, prNumber, workspaceDir, onProgress, }) {
|
|
66
|
-
const startTime = Date.now();
|
|
67
|
-
const providerHealth = new DeliveryProviderHealthTracker();
|
|
68
|
-
// Use all models that have API keys available
|
|
69
|
-
const modelsToUse = [];
|
|
70
|
-
if (apiKeys.gemini)
|
|
71
|
-
modelsToUse.push('gemini');
|
|
72
|
-
if (apiKeys.openai)
|
|
73
|
-
modelsToUse.push('openai');
|
|
74
|
-
if (apiKeys.anthropic)
|
|
75
|
-
modelsToUse.push('anthropic');
|
|
76
|
-
if (modelsToUse.length === 0) {
|
|
77
|
-
logger.error('No models available for council review', {
|
|
78
|
-
hasGeminiKey: Boolean(apiKeys.gemini),
|
|
79
|
-
hasOpenAIKey: Boolean(apiKeys.openai),
|
|
80
|
-
hasAnthropicKey: Boolean(apiKeys.anthropic),
|
|
81
|
-
});
|
|
82
|
-
return {
|
|
83
|
-
modelResults: [],
|
|
84
|
-
findingGroups: [],
|
|
85
|
-
consensusResults: [],
|
|
86
|
-
approvedFindings: [],
|
|
87
|
-
combinedSummary: 'No models available for review.',
|
|
88
|
-
totalDurationMs: Date.now() - startTime,
|
|
89
|
-
modelsUsed: [],
|
|
90
|
-
errors: [{ model: 'gemini', error: 'No models configured or API keys missing' }],
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
logger.info('Council review started', {
|
|
94
|
-
models: modelsToUse,
|
|
95
|
-
prNumber,
|
|
96
|
-
repository,
|
|
97
|
-
});
|
|
98
|
-
logger.info('Council models selected for review', { models: modelsToUse });
|
|
99
|
-
// Run all model reviews in parallel
|
|
100
|
-
const reviewPromises = modelsToUse.map((model) => {
|
|
101
|
-
const apiKey = apiKeys[model];
|
|
102
|
-
if (!apiKey) {
|
|
103
|
-
return Promise.resolve({
|
|
104
|
-
model,
|
|
105
|
-
summary: '',
|
|
106
|
-
findings: [],
|
|
107
|
-
error: `No API key for ${model}`,
|
|
108
|
-
durationMs: 0,
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
return runModelReview(model, prompt, apiKey, workspaceDir, onProgress);
|
|
112
|
-
});
|
|
113
|
-
const reviewResults = await Promise.allSettled(reviewPromises);
|
|
114
|
-
// Process results
|
|
115
|
-
const modelResults = [];
|
|
116
|
-
const errors = [];
|
|
117
|
-
reviewResults.forEach((result, index) => {
|
|
118
|
-
const model = modelsToUse[index];
|
|
119
|
-
if (result.status === 'fulfilled') {
|
|
120
|
-
modelResults.push(result.value);
|
|
121
|
-
if (result.value.error) {
|
|
122
|
-
errors.push({ model, error: result.value.error });
|
|
123
|
-
providerHealth.recordFailure({
|
|
124
|
-
provider: model,
|
|
125
|
-
stage: 'review',
|
|
126
|
-
error: result.value.error,
|
|
127
|
-
timedOut: result.value.timedOut,
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
const error = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
133
|
-
errors.push({ model, error });
|
|
134
|
-
modelResults.push({
|
|
135
|
-
model,
|
|
136
|
-
summary: '',
|
|
137
|
-
findings: [],
|
|
138
|
-
error,
|
|
139
|
-
durationMs: 0,
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
// Check if any models succeeded
|
|
144
|
-
const successfulModels = modelResults.filter((r) => !r.error);
|
|
145
|
-
if (successfulModels.length === 0) {
|
|
146
|
-
logger.error('All models failed in council review', { errors });
|
|
147
|
-
return {
|
|
148
|
-
modelResults,
|
|
149
|
-
findingGroups: [],
|
|
150
|
-
consensusResults: [],
|
|
151
|
-
approvedFindings: [],
|
|
152
|
-
combinedSummary: 'All models failed to review.',
|
|
153
|
-
totalDurationMs: Date.now() - startTime,
|
|
154
|
-
modelsUsed: [],
|
|
155
|
-
errors,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
// Collect and normalize findings
|
|
159
|
-
const allFindings = collectAllFindings(modelResults);
|
|
160
|
-
onProgress?.({ type: 'council_findings_collected', findingCount: allFindings.length });
|
|
161
|
-
const findingGroups = normalizeFindings(allFindings);
|
|
162
|
-
onProgress?.({ type: 'council_consensus_check', findingGroupCount: findingGroups.length });
|
|
163
|
-
const successfulModelNames = successfulModels.map((result) => result.model);
|
|
164
|
-
// Run voting for consensus
|
|
165
|
-
const consensusResults = await runVoting({
|
|
166
|
-
findingGroups,
|
|
167
|
-
apiKeys,
|
|
168
|
-
workspaceDir,
|
|
169
|
-
availableModels: providerHealth.getHealthyProviders(successfulModelNames, {
|
|
170
|
-
stage: 'vote',
|
|
171
|
-
}),
|
|
172
|
-
providerHealth,
|
|
173
|
-
});
|
|
174
|
-
const approvedFindings = getApprovedFindings(consensusResults);
|
|
175
|
-
// Combine summaries
|
|
176
|
-
const combinedSummary = combineSummaries(modelResults);
|
|
177
|
-
const totalDurationMs = Date.now() - startTime;
|
|
178
|
-
logger.info('Council review completed', {
|
|
179
|
-
totalFindings: allFindings.length,
|
|
180
|
-
approvedFindings: approvedFindings.length,
|
|
181
|
-
modelsUsed: successfulModelNames,
|
|
182
|
-
totalDurationMs,
|
|
183
|
-
});
|
|
184
|
-
return {
|
|
185
|
-
modelResults,
|
|
186
|
-
findingGroups,
|
|
187
|
-
consensusResults,
|
|
188
|
-
approvedFindings,
|
|
189
|
-
combinedSummary,
|
|
190
|
-
totalDurationMs,
|
|
191
|
-
modelsUsed: successfulModelNames,
|
|
192
|
-
errors,
|
|
193
|
-
};
|
|
194
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { logger } from '../../../core/logging.js';
|
|
2
|
-
export class DeliveryProviderHealthTracker {
|
|
3
|
-
openCircuits = new Map();
|
|
4
|
-
getHealthyProviders(providers, { stage, findingId }) {
|
|
5
|
-
return providers.filter((provider) => {
|
|
6
|
-
const reason = this.openCircuits.get(provider);
|
|
7
|
-
if (!reason) {
|
|
8
|
-
return true;
|
|
9
|
-
}
|
|
10
|
-
logger.info('Council provider skipped due to open circuit', {
|
|
11
|
-
provider,
|
|
12
|
-
stage,
|
|
13
|
-
findingId,
|
|
14
|
-
reason,
|
|
15
|
-
});
|
|
16
|
-
return false;
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
recordFailure({ provider, stage, error, timedOut, findingId }) {
|
|
20
|
-
if (!timedOut || this.openCircuits.has(provider)) {
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
this.openCircuits.set(provider, error);
|
|
24
|
-
logger.warn('Council provider circuit opened', {
|
|
25
|
-
provider,
|
|
26
|
-
stage,
|
|
27
|
-
findingId,
|
|
28
|
-
error,
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
}
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
import { logger } from '../../../core/logging.js';
|
|
2
|
-
import { formatPrAuthorForGreeting, normalizePrAuthor } from '../author-format.js';
|
|
3
|
-
import { requestReviewSummary } from '../summary-model.js';
|
|
4
|
-
export const MAX_SUMMARY_BODY_CHARS = 2000;
|
|
5
|
-
const MAX_REVIEW_OBSERVATION_CHARS = 500;
|
|
6
|
-
const REVIEW_SUMMARY_EMOJIS = ['😊', '😇', '🤗', '😄 🙌', '😎 👊'];
|
|
7
|
-
const REVIEW_AUTHOR_PLACEHOLDER = 'PR_AUTHOR';
|
|
8
|
-
const GENERIC_REVIEW_SUMMARY_GREETING = 'Thanks for your contribution.';
|
|
9
|
-
function pickSummaryEmoji(prAuthor, seedContext) {
|
|
10
|
-
const normalizedAuthor = normalizePrAuthor(prAuthor);
|
|
11
|
-
const normalizedContext = seedContext?.trim();
|
|
12
|
-
const seed = [normalizedAuthor || 'doistbot-review', normalizedContext]
|
|
13
|
-
.filter(Boolean)
|
|
14
|
-
.join(':');
|
|
15
|
-
const hash = Array.from(seed).reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
|
16
|
-
return REVIEW_SUMMARY_EMOJIS[hash % REVIEW_SUMMARY_EMOJIS.length];
|
|
17
|
-
}
|
|
18
|
-
function escapeRegExp(value) {
|
|
19
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
20
|
-
}
|
|
21
|
-
function resolveAuthorGreetingName(prAuthor, prAuthorGreetingName) {
|
|
22
|
-
return formatPrAuthorForGreeting(prAuthorGreetingName) ?? formatPrAuthorForGreeting(prAuthor);
|
|
23
|
-
}
|
|
24
|
-
export function buildReviewSummaryGreeting(prAuthor, seedContext, prAuthorGreetingName, excludePrAuthorFromSummary = false) {
|
|
25
|
-
if (excludePrAuthorFromSummary) {
|
|
26
|
-
return '';
|
|
27
|
-
}
|
|
28
|
-
const authorForGreeting = resolveAuthorGreetingName(prAuthor, prAuthorGreetingName);
|
|
29
|
-
const emoji = pickSummaryEmoji(prAuthor, seedContext);
|
|
30
|
-
if (!authorForGreeting) {
|
|
31
|
-
return `Thanks for your contribution ${emoji}.`;
|
|
32
|
-
}
|
|
33
|
-
return `Thanks ${authorForGreeting} for your contribution ${emoji}.`;
|
|
34
|
-
}
|
|
35
|
-
export function withReviewSummaryGreeting(summary, prAuthor, prAuthorGreetingName, excludePrAuthorFromSummary = false) {
|
|
36
|
-
const trimmedSummary = summary.trim();
|
|
37
|
-
if (excludePrAuthorFromSummary) {
|
|
38
|
-
return trimmedSummary;
|
|
39
|
-
}
|
|
40
|
-
const greeting = buildReviewSummaryGreeting(prAuthor, trimmedSummary, prAuthorGreetingName, excludePrAuthorFromSummary);
|
|
41
|
-
return trimmedSummary ? `${greeting} ${trimmedSummary}` : greeting;
|
|
42
|
-
}
|
|
43
|
-
function splitOpeningLine(summary) {
|
|
44
|
-
const firstLineBreak = summary.indexOf('\n');
|
|
45
|
-
if (firstLineBreak === -1) {
|
|
46
|
-
return { openingLine: summary, remainingText: '' };
|
|
47
|
-
}
|
|
48
|
-
return {
|
|
49
|
-
openingLine: summary.slice(0, firstLineBreak),
|
|
50
|
-
remainingText: summary.slice(firstLineBreak),
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
function normalizeSummarySpacing(summary) {
|
|
54
|
-
return summary
|
|
55
|
-
.replace(/[ \t]{2,}/g, ' ')
|
|
56
|
-
.replace(/\s+([.!?,;:])/g, '$1')
|
|
57
|
-
.trimEnd();
|
|
58
|
-
}
|
|
59
|
-
function replaceUnsafeAuthorPlaceholder(summary) {
|
|
60
|
-
const placeholderPattern = escapeRegExp(REVIEW_AUTHOR_PLACEHOLDER);
|
|
61
|
-
const genericGreeting = GENERIC_REVIEW_SUMMARY_GREETING.replace(/[.!?]+$/, '');
|
|
62
|
-
return normalizeSummarySpacing(summary
|
|
63
|
-
.replace(new RegExp(`\\bThanks\\s+${placeholderPattern}(?=\\s+for\\b)`, 'g'), 'Thanks')
|
|
64
|
-
.replace(new RegExp(`\\bThanks\\s+${placeholderPattern}(?=[.!?])`, 'g'), genericGreeting)
|
|
65
|
-
.replace(new RegExp(`\\bThank you\\s+${placeholderPattern}(?=\\s+for\\b)`, 'g'), 'Thank you')
|
|
66
|
-
.replace(new RegExp(`\\bThank you\\s+${placeholderPattern}(?=[.!?])`, 'g'), 'Thank you for your contribution')
|
|
67
|
-
.split(REVIEW_AUTHOR_PLACEHOLDER)
|
|
68
|
-
.join('the contributor'));
|
|
69
|
-
}
|
|
70
|
-
function replaceAuthorPlaceholders(summary, greetingName) {
|
|
71
|
-
if (!summary.includes(REVIEW_AUTHOR_PLACEHOLDER)) {
|
|
72
|
-
return summary;
|
|
73
|
-
}
|
|
74
|
-
if (!greetingName) {
|
|
75
|
-
return replaceUnsafeAuthorPlaceholder(summary);
|
|
76
|
-
}
|
|
77
|
-
return summary.split(REVIEW_AUTHOR_PLACEHOLDER).join(greetingName);
|
|
78
|
-
}
|
|
79
|
-
function addEmojiToOpeningSentence(openingLine, emoji) {
|
|
80
|
-
const sentenceEndMatch = /[.!?](?=\s|$)/.exec(openingLine);
|
|
81
|
-
if (!sentenceEndMatch) {
|
|
82
|
-
return `${openingLine} ${emoji}`;
|
|
83
|
-
}
|
|
84
|
-
const sentenceEndIndex = sentenceEndMatch.index;
|
|
85
|
-
return `${openingLine.slice(0, sentenceEndIndex).trimEnd()} ${emoji}${openingLine.slice(sentenceEndIndex)}`;
|
|
86
|
-
}
|
|
87
|
-
export function ensureReviewSummaryApprovedEmoji(summary, prAuthor, prAuthorGreetingName, excludePrAuthorFromSummary = false) {
|
|
88
|
-
const trimmedSummary = summary.trim();
|
|
89
|
-
if (!trimmedSummary || excludePrAuthorFromSummary) {
|
|
90
|
-
return trimmedSummary;
|
|
91
|
-
}
|
|
92
|
-
const emoji = pickSummaryEmoji(prAuthor, trimmedSummary);
|
|
93
|
-
const greetingName = resolveAuthorGreetingName(prAuthor, prAuthorGreetingName);
|
|
94
|
-
const summaryWithAuthor = replaceAuthorPlaceholders(trimmedSummary, greetingName);
|
|
95
|
-
const { openingLine: firstLineWithAuthor, remainingText: finalRemainingText } = splitOpeningLine(summaryWithAuthor);
|
|
96
|
-
const firstLineWithEmoji = addEmojiToOpeningSentence(firstLineWithAuthor, emoji);
|
|
97
|
-
return firstLineWithEmoji + finalRemainingText;
|
|
98
|
-
}
|
|
99
|
-
export function buildCouncilSummaryPrompt({ prTitle, prBody, findings, excludePrAuthorFromSummary = false, reviewObservations = [], }) {
|
|
100
|
-
const safeTitle = prTitle.trim() || 'Untitled';
|
|
101
|
-
const safeAuthor = excludePrAuthorFromSummary ? 'unknown' : REVIEW_AUTHOR_PLACEHOLDER;
|
|
102
|
-
const safeBody = prBody.trim();
|
|
103
|
-
const condensedBody = safeBody
|
|
104
|
-
? safeBody.slice(0, MAX_SUMMARY_BODY_CHARS)
|
|
105
|
-
: 'No PR description was provided.';
|
|
106
|
-
const findingsBlock = findings.length > 0
|
|
107
|
-
? findings
|
|
108
|
-
.map((finding) => `<review-finding>\n${finding.trim()}\n</review-finding>`)
|
|
109
|
-
.join('\n\n')
|
|
110
|
-
: 'No issues were flagged in the inline comments.';
|
|
111
|
-
const observationsBlock = reviewObservations.length > 0
|
|
112
|
-
? reviewObservations
|
|
113
|
-
.map((observation) => observation.trim())
|
|
114
|
-
.filter(Boolean)
|
|
115
|
-
.slice(0, 8)
|
|
116
|
-
.map((observation) => `<review-observation>\n${observation.slice(0, MAX_REVIEW_OBSERVATION_CHARS)}\n</review-observation>`)
|
|
117
|
-
.join('\n\n') || 'No additional review observations were captured.'
|
|
118
|
-
: 'No additional review observations were captured.';
|
|
119
|
-
return `You are an expert code review summarizer for the Doist engineering team.
|
|
120
|
-
|
|
121
|
-
<pr_author>
|
|
122
|
-
${safeAuthor}
|
|
123
|
-
</pr_author>
|
|
124
|
-
|
|
125
|
-
<pr_author_excluded_from_summary>
|
|
126
|
-
${excludePrAuthorFromSummary ? 'true' : 'false'}
|
|
127
|
-
</pr_author_excluded_from_summary>
|
|
128
|
-
|
|
129
|
-
<pr_title>
|
|
130
|
-
${safeTitle}
|
|
131
|
-
</pr_title>
|
|
132
|
-
|
|
133
|
-
<pr_description>
|
|
134
|
-
${condensedBody}
|
|
135
|
-
</pr_description>
|
|
136
|
-
|
|
137
|
-
<review_findings>
|
|
138
|
-
${findingsBlock}
|
|
139
|
-
</review_findings>
|
|
140
|
-
|
|
141
|
-
<review_observations>
|
|
142
|
-
${observationsBlock}
|
|
143
|
-
</review_observations>
|
|
144
|
-
|
|
145
|
-
Write the complete GitHub review summary body for the PR author. Synthesize the findings into grouped themes—do not mirror every inline comment one-for-one or reference the reviewers, models, council, focus passes, or review observations. Do not mention issues not present in the <review-finding> blocks. If there are no <review-finding> blocks, state that no issues were flagged.
|
|
146
|
-
|
|
147
|
-
If <pr_author_excluded_from_summary> is "true", do not thank, address, or name the PR author. Otherwise, start with a natural, concise thanks to the author using the literal placeholder ${REVIEW_AUTHOR_PLACEHOLDER}. Do not use a real user name, first name, login, handle, or @ mention. Vary the wording instead of always using the fixed template "Thanks ${REVIEW_AUTHOR_PLACEHOLDER} for your contribution".
|
|
148
|
-
|
|
149
|
-
Do not include emojis in the summary. The caller will replace ${REVIEW_AUTHOR_PLACEHOLDER} locally and add one approved emoji after the author name for non-excluded PR authors.
|
|
150
|
-
|
|
151
|
-
Structure:
|
|
152
|
-
- Start with one short sentence. When the PR author is not excluded, include the thanks and ${REVIEW_AUTHOR_PLACEHOLDER} in that sentence. When the author is excluded, start with a concise overview of what changed.
|
|
153
|
-
- If issues exist, add a blank line, then the exact heading "Few things worth tightening:", then 1-4 concise bullets grouping what to improve.
|
|
154
|
-
- If no issues exist, do not add a heading or bullets.
|
|
155
|
-
|
|
156
|
-
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, constructive, appreciative tone without generic praise. Bullets should be short fragments or single sentences, not paragraphs. Do not include any <review-finding> or <review-observation> tags in the output.
|
|
157
|
-
|
|
158
|
-
Respond with only valid JSON in this exact shape:
|
|
159
|
-
{"summary":"<your summary>","comments":[]}`;
|
|
160
|
-
}
|
|
161
|
-
export async function summarizeCouncilReviews({ apiKey, prTitle, prBody, findings, prAuthor, prAuthorGreetingName, excludePrAuthorFromSummary, reviewObservations, workspaceDir, requestReview = requestReviewSummary, }) {
|
|
162
|
-
const prompt = buildCouncilSummaryPrompt({
|
|
163
|
-
prTitle,
|
|
164
|
-
prBody,
|
|
165
|
-
findings,
|
|
166
|
-
prAuthor,
|
|
167
|
-
prAuthorGreetingName,
|
|
168
|
-
excludePrAuthorFromSummary,
|
|
169
|
-
reviewObservations,
|
|
170
|
-
});
|
|
171
|
-
logger.info('Summarizing council reviews with the review summary model', {
|
|
172
|
-
findingsCount: findings.length,
|
|
173
|
-
reviewObservationsCount: reviewObservations?.length ?? 0,
|
|
174
|
-
});
|
|
175
|
-
const result = await requestReview({ apiKey, prompt, workspaceDir });
|
|
176
|
-
const summary = result?.summary?.trim() ?? '';
|
|
177
|
-
if (!summary) {
|
|
178
|
-
logger.warn('Council summary model returned an empty summary', {
|
|
179
|
-
findingsCount: findings.length,
|
|
180
|
-
});
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
return ensureReviewSummaryApprovedEmoji(summary, prAuthor, prAuthorGreetingName, excludePrAuthorFromSummary);
|
|
184
|
-
}
|
|
185
|
-
export function buildFallbackSummary({ prTitle, findings, prAuthor, prAuthorGreetingName, excludePrAuthorFromSummary, }) {
|
|
186
|
-
const safeTitle = prTitle.trim();
|
|
187
|
-
const base = safeTitle ? `This PR updates ${safeTitle}.` : 'This PR updates the codebase.';
|
|
188
|
-
if (findings.length === 0) {
|
|
189
|
-
return withReviewSummaryGreeting(`${base} No issues were flagged in the reviewed diff.`, prAuthor, prAuthorGreetingName, excludePrAuthorFromSummary);
|
|
190
|
-
}
|
|
191
|
-
return withReviewSummaryGreeting(`${base}\n\nFew things worth tightening:\n- See the inline comments for the issues found in the review.`, prAuthor, prAuthorGreetingName, excludePrAuthorFromSummary);
|
|
192
|
-
}
|