@todoist/doistbot-cli 0.0.0-stage → 1.0.16
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 +225 -0
- package/dist/actions/doctor.js +39 -0
- package/dist/actions/review-usage.js +25 -0
- package/dist/actions/review.js +311 -0
- package/dist/actions/setup.js +14 -0
- package/dist/auth.js +202 -0
- package/dist/config.js +145 -0
- package/dist/env.js +10 -0
- package/dist/file-store.js +112 -0
- package/dist/index.js +164 -0
- package/dist/input.js +40 -0
- package/dist/runtime.js +105 -0
- package/dist/secure-store.js +148 -0
- package/dist/terminal.js +185 -0
- package/dist/test-utils/secure-store-mocks.js +29 -0
- package/dist/update-cache-entrypoint.js +16 -0
- package/dist/update-check.js +235 -0
- package/package.json +65 -5
- package/sandbox/_node_modules/doistbot-logging/dist/index.d.ts +23 -0
- package/sandbox/_node_modules/doistbot-logging/dist/index.js +51 -0
- package/sandbox/_node_modules/doistbot-logging/package.json +12 -0
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +93 -0
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +197 -0
- package/sandbox/_node_modules/doistbot-repo-config/package.json +12 -0
- package/sandbox/dist/core/autofix-resources.js +58 -0
- package/sandbox/dist/core/autofix-service-query.js +69 -0
- package/sandbox/dist/core/autofix-test-policy.js +11 -0
- package/sandbox/dist/core/autofix-verification.js +355 -0
- package/sandbox/dist/core/bot-logins.js +14 -0
- package/sandbox/dist/core/check-run.js +158 -0
- package/sandbox/dist/core/datadog-metrics.js +113 -0
- package/sandbox/dist/core/environment.js +49 -0
- package/sandbox/dist/core/git-auth.js +17 -0
- package/sandbox/dist/core/github-history.js +4 -0
- package/sandbox/dist/core/github-issues.js +18 -0
- package/sandbox/dist/core/label-parsing.js +1 -0
- package/sandbox/dist/core/langfuse-observation.js +33 -0
- package/sandbox/dist/core/langfuse-session.js +39 -0
- package/sandbox/dist/core/logger-span-processor.js +90 -0
- package/sandbox/dist/core/logging.js +18 -0
- package/sandbox/dist/core/pi-autofix-extension.js +188 -0
- package/sandbox/dist/core/pi-execution-limits-extension.js +79 -0
- package/sandbox/dist/core/pi-progress-logging.js +51 -0
- package/sandbox/dist/core/pi-tool-output-filter-extension.js +65 -0
- package/sandbox/dist/core/pi.js +870 -0
- package/sandbox/dist/core/pii-scrubber.js +31 -0
- package/sandbox/dist/core/prompt-template.js +63 -0
- package/sandbox/dist/core/reactions.js +36 -0
- package/sandbox/dist/core/regexp.js +3 -0
- package/sandbox/dist/core/repository-map.js +1 -0
- package/sandbox/dist/core/repository-resolver.js +1 -0
- package/sandbox/dist/core/retry.js +29 -0
- package/sandbox/dist/core/s3-utils.js +42 -0
- package/sandbox/dist/core/shared.js +124 -0
- package/sandbox/dist/core/snapshot-helpers.js +17 -0
- package/sandbox/dist/core/span-test-helpers.js +13 -0
- package/sandbox/dist/core/tool-environment.js +83 -0
- package/sandbox/dist/core/tool-output.js +18 -0
- package/sandbox/dist/core/tracing-setup.js +92 -0
- package/sandbox/dist/core/tracing.js +81 -0
- package/sandbox/dist/main.js +51 -0
- package/sandbox/dist/providers/config.js +23 -0
- package/sandbox/dist/providers/credentials.js +40 -0
- package/sandbox/dist/providers/gemini.js +28 -0
- package/sandbox/dist/providers/helpers.js +253 -0
- package/sandbox/dist/providers/index.js +27 -0
- package/sandbox/dist/providers/openai.js +23 -0
- package/sandbox/dist/providers/openrouter-pi.js +25 -0
- package/sandbox/dist/providers/pi-review.js +301 -0
- package/sandbox/dist/providers/process.js +34 -0
- package/sandbox/dist/providers/schemas.js +61 -0
- package/sandbox/dist/providers/types.js +4 -0
- package/sandbox/dist/tasks/chat/chat.js +440 -0
- package/sandbox/dist/tasks/chat/prompt.js +20 -0
- package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +304 -0
- package/sandbox/dist/tasks/issue-summarize/context.js +21 -0
- package/sandbox/dist/tasks/issue-summarize/media.js +335 -0
- package/sandbox/dist/tasks/issue-summarize/model.js +230 -0
- package/sandbox/dist/tasks/issue-summarize/output.js +45 -0
- package/sandbox/dist/tasks/issue-summarize/prompt.js +11 -0
- package/sandbox/dist/tasks/issue-summarize/summarize.js +238 -0
- package/sandbox/dist/tasks/issue-summarize/thread.js +123 -0
- package/sandbox/dist/tasks/issue-summarize/types.js +1 -0
- package/sandbox/dist/tasks/issue-triage/assignee-review.js +114 -0
- package/sandbox/dist/tasks/issue-triage/autofix-capabilities.js +22 -0
- package/sandbox/dist/tasks/issue-triage/autofix-checkpoint.js +122 -0
- package/sandbox/dist/tasks/issue-triage/autofix-policy.js +36 -0
- package/sandbox/dist/tasks/issue-triage/clone.js +40 -0
- package/sandbox/dist/tasks/issue-triage/context.js +57 -0
- package/sandbox/dist/tasks/issue-triage/diff-validator.js +102 -0
- package/sandbox/dist/tasks/issue-triage/fix-attempt.js +617 -0
- package/sandbox/dist/tasks/issue-triage/fix-context.js +5 -0
- package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +263 -0
- package/sandbox/dist/tasks/issue-triage/fix-failure-reasons.js +2 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/effects.js +526 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/effects.types.js +1 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/fake-effects.js +65 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/promotion-task.js +108 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/recovery.js +512 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/runners.js +233 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/state-machine.js +171 -0
- package/sandbox/dist/tasks/issue-triage/github-request.js +2 -0
- package/sandbox/dist/tasks/issue-triage/hero-group-map.js +206 -0
- package/sandbox/dist/tasks/issue-triage/markers.js +6 -0
- package/sandbox/dist/tasks/issue-triage/model.js +124 -0
- package/sandbox/dist/tasks/issue-triage/output.js +164 -0
- package/sandbox/dist/tasks/issue-triage/pr-creator.js +585 -0
- package/sandbox/dist/tasks/issue-triage/pr-readiness.js +25 -0
- package/sandbox/dist/tasks/issue-triage/prompt.js +63 -0
- package/sandbox/dist/tasks/issue-triage/repo-config.js +47 -0
- package/sandbox/dist/tasks/issue-triage/repo-prompts.js +77 -0
- package/sandbox/dist/tasks/issue-triage/report-fixture.js +53 -0
- package/sandbox/dist/tasks/issue-triage/report-history.js +113 -0
- package/sandbox/dist/tasks/issue-triage/report-schema.js +63 -0
- package/sandbox/dist/tasks/issue-triage/report.js +111 -0
- package/sandbox/dist/tasks/issue-triage/resolution.js +30 -0
- package/sandbox/dist/tasks/issue-triage/routing.js +169 -0
- package/sandbox/dist/tasks/issue-triage/sandbox-environment.js +59 -0
- package/sandbox/dist/tasks/issue-triage/shared.js +86 -0
- package/sandbox/dist/tasks/issue-triage/side-effects.js +102 -0
- package/sandbox/dist/tasks/issue-triage/triage.js +442 -0
- package/sandbox/dist/tasks/issue-triage/types.js +1 -0
- package/sandbox/dist/tasks/persist-logs/context.js +19 -0
- package/sandbox/dist/tasks/persist-logs/dd-proxy.js +116 -0
- package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +156 -0
- package/sandbox/dist/tasks/persist-logs/persist-logs.js +491 -0
- package/sandbox/dist/tasks/refresh-s3-urls/refresh-s3-urls.js +214 -0
- package/sandbox/dist/tasks/review/check-run.js +326 -0
- package/sandbox/dist/tasks/review/comment-mapper.js +46 -0
- package/sandbox/dist/tasks/review/conversation-context.js +477 -0
- package/sandbox/dist/tasks/review/diff.js +95 -0
- package/sandbox/dist/tasks/review/engines/dedupe.js +291 -0
- package/sandbox/dist/tasks/review/engines/multi-focus.js +419 -0
- package/sandbox/dist/tasks/review/engines/shared.js +87 -0
- package/sandbox/dist/tasks/review/engines/types.js +1 -0
- package/sandbox/dist/tasks/review/finding-output.js +18 -0
- package/sandbox/dist/tasks/review/format.js +17 -0
- package/sandbox/dist/tasks/review/github-comment-format.js +94 -0
- package/sandbox/dist/tasks/review/local-git.js +374 -0
- package/sandbox/dist/tasks/review/local.js +118 -0
- package/sandbox/dist/tasks/review/multi-focus-prompt.js +59 -0
- package/sandbox/dist/tasks/review/prompt.js +81 -0
- package/sandbox/dist/tasks/review/review-feedback.js +48 -0
- package/sandbox/dist/tasks/review/review-mode-github.js +58 -0
- package/sandbox/dist/tasks/review/review-mode-policy.js +232 -0
- package/sandbox/dist/tasks/review/review-mode-types.js +1 -0
- package/sandbox/dist/tasks/review/review-mode.js +296 -0
- package/sandbox/dist/tasks/review/review-summary.js +123 -0
- package/sandbox/dist/tasks/review/review.js +391 -0
- package/sandbox/dist/tasks/review/runner.js +255 -0
- package/sandbox/dist/tasks/review/severity.js +5 -0
- package/sandbox/dist/tasks/review/summary-model.js +271 -0
- package/sandbox/dist/tasks/review/thinking-level.js +14 -0
- package/sandbox/dist/tasks/review/translation-review-policy.js +37 -0
- package/sandbox/dist/tasks/review/types.js +1 -0
- package/sandbox/dist/tasks/review-slice-plan/context.js +66 -0
- package/sandbox/dist/tasks/review-slice-plan/plan.js +210 -0
- package/sandbox/dist/tasks/review-slice-plan/prompt.js +59 -0
- package/sandbox/dist/tasks/review-slice-plan/review-slice-plan.js +367 -0
- package/sandbox/dist/tasks/sentry-triage/store.js +133 -0
- package/sandbox/dist/tasks/sentry-triage/triage.js +217 -0
- package/sandbox/node_modules/doistbot-logging/dist/index.d.ts +23 -0
- package/sandbox/node_modules/doistbot-logging/dist/index.js +51 -0
- package/sandbox/node_modules/doistbot-logging/package.json +12 -0
- package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +93 -0
- package/sandbox/node_modules/doistbot-repo-config/dist/index.js +197 -0
- package/sandbox/node_modules/doistbot-repo-config/package.json +12 -0
- package/sandbox/src/review/prompts/ai-internal-tools.md +254 -0
- package/sandbox/src/review/prompts/backend-repository-standards.md +102 -0
- package/sandbox/src/review/prompts/observability-standard.md +169 -0
- package/sandbox/src/review/prompts/outline-map.json +26 -0
- package/sandbox/src/review/prompts/secrets-management.md +286 -0
- package/sandbox/src/review/prompts/service-production-readiness.md +122 -0
- package/sandbox/src/review/prompts/workload-placement.md +345 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/efficiency.md +16 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/general.md +32 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/quality.md +20 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/reuse.md +13 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/security.md +18 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/standards.md +21 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/tests.md +19 -0
- package/sandbox/src/tasks/review/prompts/review-mode.md +40 -0
- package/sandbox/src/tasks/review/prompts/review-multi-focus-base-prompt.md +142 -0
- package/scripts/restore-runtime-assets.mjs +21 -0
- package/README.md +0 -3
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import { isKnownDoistbotLogin } from '../../core/bot-logins.js';
|
|
2
|
+
import { WORKSPACE_DIR } from '../../core/environment.js';
|
|
3
|
+
import { fetchIssueComments } from '../../core/github-issues.js';
|
|
4
|
+
import { logger } from '../../core/logging.js';
|
|
5
|
+
import { invokePiPrompt } from '../../core/pi.js';
|
|
6
|
+
import { resolveReviewProviderCredential } from '../../providers/credentials.js';
|
|
7
|
+
import { normalizeGithubPriorityBadges } from './github-comment-format.js';
|
|
8
|
+
import { stripReviewFeedbackFooter } from './review-feedback.js';
|
|
9
|
+
// Roughly 6k tokens: large enough for substantive PR discussions while still
|
|
10
|
+
// keeping conversation context secondary to the diff.
|
|
11
|
+
export const RAW_CONVERSATION_CONTEXT_THRESHOLD_CHARS = 25_000;
|
|
12
|
+
const CONVERSATION_SUMMARY_TIMEOUT_MS = 5 * 60 * 1000;
|
|
13
|
+
const CONVERSATION_SUMMARY_TARGET_CHARS = 4_000;
|
|
14
|
+
const REVIEW_COMMAND_REGEX = /^@doistbot \/review$/i;
|
|
15
|
+
const GITHUB_ACTIONS_BOT_LOGIN = 'github-actions[bot]';
|
|
16
|
+
const MASKED_PR_AUTHOR_LABEL = 'PR author';
|
|
17
|
+
function getSummaryProviders(apiKeys) {
|
|
18
|
+
return [
|
|
19
|
+
...(apiKeys?.gemini ? [{ provider: 'gemini', credential: apiKeys.gemini }] : []),
|
|
20
|
+
...(apiKeys?.openai ? [{ provider: 'openai', credential: apiKeys.openai }] : []),
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
function isRelevantBotLogin(login) {
|
|
24
|
+
const normalized = login.toLowerCase();
|
|
25
|
+
return isKnownDoistbotLogin(normalized) || normalized === GITHUB_ACTIONS_BOT_LOGIN;
|
|
26
|
+
}
|
|
27
|
+
function shouldIncludeEntry(login, body) {
|
|
28
|
+
if (!body || REVIEW_COMMAND_REGEX.test(body)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
const normalized = login.toLowerCase();
|
|
32
|
+
return !normalized.endsWith('[bot]') || isRelevantBotLogin(normalized);
|
|
33
|
+
}
|
|
34
|
+
function shouldMaskPrAuthor(login, prAuthor) {
|
|
35
|
+
const normalizedLogin = login.trim().toLowerCase();
|
|
36
|
+
const normalizedPrAuthor = prAuthor?.trim().toLowerCase();
|
|
37
|
+
return Boolean(normalizedLogin && normalizedPrAuthor && normalizedLogin === normalizedPrAuthor);
|
|
38
|
+
}
|
|
39
|
+
function cleanBody(body) {
|
|
40
|
+
return normalizeGithubPriorityBadges(stripReviewFeedbackFooter(body));
|
|
41
|
+
}
|
|
42
|
+
function toIssueCommentEntry(comment) {
|
|
43
|
+
const author = comment.user?.login ?? 'unknown';
|
|
44
|
+
const body = cleanBody(comment.body ?? '');
|
|
45
|
+
if (!shouldIncludeEntry(author, body)) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
source: 'issue_comment',
|
|
50
|
+
author,
|
|
51
|
+
createdAt: comment.created_at ?? 'unknown',
|
|
52
|
+
url: comment.html_url ?? undefined,
|
|
53
|
+
body,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function toReviewSummaryEntry(review) {
|
|
57
|
+
const author = review.user?.login ?? 'unknown';
|
|
58
|
+
const body = cleanBody(review.body ?? '');
|
|
59
|
+
if (!shouldIncludeEntry(author, body)) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
source: 'review_summary',
|
|
64
|
+
author,
|
|
65
|
+
createdAt: review.submitted_at ?? 'unknown',
|
|
66
|
+
url: review.html_url ?? undefined,
|
|
67
|
+
state: review.state ?? undefined,
|
|
68
|
+
body,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function toInlineCommentEntry(comment) {
|
|
72
|
+
const author = comment.user?.login ?? 'unknown';
|
|
73
|
+
const body = cleanBody(comment.body ?? '');
|
|
74
|
+
if (!shouldIncludeEntry(author, body)) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
source: 'inline_comment',
|
|
79
|
+
author,
|
|
80
|
+
createdAt: comment.created_at ?? 'unknown',
|
|
81
|
+
url: comment.html_url ?? undefined,
|
|
82
|
+
path: comment.path ?? undefined,
|
|
83
|
+
line: comment.line ?? comment.original_line ?? undefined,
|
|
84
|
+
side: comment.side ?? undefined,
|
|
85
|
+
id: comment.id ?? undefined,
|
|
86
|
+
replyToId: comment.in_reply_to_id ?? undefined,
|
|
87
|
+
body,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function compareCreatedAt(a, b) {
|
|
91
|
+
if (a === b) {
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
if (a === 'unknown') {
|
|
95
|
+
return 1;
|
|
96
|
+
}
|
|
97
|
+
if (b === 'unknown') {
|
|
98
|
+
return -1;
|
|
99
|
+
}
|
|
100
|
+
return a.localeCompare(b);
|
|
101
|
+
}
|
|
102
|
+
// Groups inline comments into threads (a root finding plus its replies) using
|
|
103
|
+
// in_reply_to_id. Replies whose root is missing fall back to standalone entries.
|
|
104
|
+
function buildInlineThreads(entries, prAuthorThumbsDownRootIds) {
|
|
105
|
+
const roots = entries.filter((entry) => entry.replyToId == null);
|
|
106
|
+
const rootById = new Map();
|
|
107
|
+
for (const root of roots) {
|
|
108
|
+
if (root.id != null) {
|
|
109
|
+
rootById.set(root.id, {
|
|
110
|
+
root,
|
|
111
|
+
replies: [],
|
|
112
|
+
prAuthorReactedThumbsDown: prAuthorThumbsDownRootIds.has(root.id),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const orphans = [];
|
|
117
|
+
for (const entry of entries) {
|
|
118
|
+
if (entry.replyToId == null) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const thread = rootById.get(entry.replyToId);
|
|
122
|
+
if (thread) {
|
|
123
|
+
thread.replies.push(entry);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
orphans.push(entry);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const threads = roots.flatMap((root) => {
|
|
130
|
+
const thread = root.id == null ? undefined : rootById.get(root.id);
|
|
131
|
+
if (!thread) {
|
|
132
|
+
// Root without a usable id can't anchor replies; render it standalone.
|
|
133
|
+
orphans.push(root);
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
thread.replies.sort((a, b) => compareCreatedAt(a.createdAt, b.createdAt));
|
|
137
|
+
return [thread];
|
|
138
|
+
});
|
|
139
|
+
return { threads, orphans };
|
|
140
|
+
}
|
|
141
|
+
function describeAuthor(author, prAuthor) {
|
|
142
|
+
const normalized = author.toLowerCase();
|
|
143
|
+
if (isKnownDoistbotLogin(normalized)) {
|
|
144
|
+
return `${author} (doistbot)`;
|
|
145
|
+
}
|
|
146
|
+
if (shouldMaskPrAuthor(author, prAuthor)) {
|
|
147
|
+
return MASKED_PR_AUTHOR_LABEL;
|
|
148
|
+
}
|
|
149
|
+
return author;
|
|
150
|
+
}
|
|
151
|
+
function formatLocation(entry) {
|
|
152
|
+
const line = entry.line == null ? 'unknown' : String(entry.line);
|
|
153
|
+
const side = entry.side ? ` ${entry.side}` : '';
|
|
154
|
+
return `${entry.path}:${line}${side}`;
|
|
155
|
+
}
|
|
156
|
+
function formatEntry(entry, index, prAuthor) {
|
|
157
|
+
const lines = [`### Entry ${index + 1}: ${entry.source.replace('_', ' ')}`];
|
|
158
|
+
lines.push(`- Author: ${describeAuthor(entry.author, prAuthor)}`);
|
|
159
|
+
lines.push(`- Created: ${entry.createdAt}`);
|
|
160
|
+
if (entry.state) {
|
|
161
|
+
lines.push(`- Review state: ${entry.state}`);
|
|
162
|
+
}
|
|
163
|
+
if (entry.path) {
|
|
164
|
+
lines.push(`- Location: ${formatLocation(entry)}`);
|
|
165
|
+
}
|
|
166
|
+
if (entry.replyToId != null) {
|
|
167
|
+
lines.push(`- Reply to inline comment ID: ${entry.replyToId}`);
|
|
168
|
+
}
|
|
169
|
+
if (entry.url) {
|
|
170
|
+
lines.push(`- URL: ${entry.url}`);
|
|
171
|
+
}
|
|
172
|
+
lines.push('Body:');
|
|
173
|
+
lines.push(entry.body);
|
|
174
|
+
return lines.join('\n');
|
|
175
|
+
}
|
|
176
|
+
function formatThreadMessage(entry, prAuthor) {
|
|
177
|
+
return [`${describeAuthor(entry.author, prAuthor)} — ${entry.createdAt}:`, entry.body].join('\n');
|
|
178
|
+
}
|
|
179
|
+
function formatThread(thread, index, prAuthor) {
|
|
180
|
+
const { root, replies } = thread;
|
|
181
|
+
const location = root.path ? formatLocation(root) : 'unknown location';
|
|
182
|
+
const lines = [`### Entry ${index + 1}: prior inline thread on ${location}`];
|
|
183
|
+
const rootIsDoistbot = isKnownDoistbotLogin(root.author.toLowerCase());
|
|
184
|
+
const prAuthorReplied = prAuthor != null && replies.some((reply) => shouldMaskPrAuthor(reply.author, prAuthor));
|
|
185
|
+
if (rootIsDoistbot && thread.prAuthorReactedThumbsDown) {
|
|
186
|
+
lines.push('- Status: the PR author reacted 👎 to this prior doistbot finding. Treat this as author pushback / declined unless later discussion says otherwise.');
|
|
187
|
+
}
|
|
188
|
+
if (rootIsDoistbot && prAuthorReplied) {
|
|
189
|
+
lines.push('- Status: the PR author replied to this prior doistbot finding. Read their response to decide whether they accepted it or pushed back / declined it.');
|
|
190
|
+
}
|
|
191
|
+
if (root.url) {
|
|
192
|
+
lines.push(`- URL: ${root.url}`);
|
|
193
|
+
}
|
|
194
|
+
for (const message of [root, ...replies]) {
|
|
195
|
+
lines.push('');
|
|
196
|
+
lines.push(formatThreadMessage(message, prAuthor));
|
|
197
|
+
}
|
|
198
|
+
return lines.join('\n');
|
|
199
|
+
}
|
|
200
|
+
function buildStats(baseStats, { summarized = false, summarizationFailed = false, } = {}) {
|
|
201
|
+
return {
|
|
202
|
+
...baseStats,
|
|
203
|
+
summarized,
|
|
204
|
+
summarizationFailed,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function emptyContext(baseStats, summarizationFailed = false) {
|
|
208
|
+
return {
|
|
209
|
+
promptText: '',
|
|
210
|
+
stats: buildStats(baseStats, { summarizationFailed }),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function buildSummaryPrompt(rawContext) {
|
|
214
|
+
return `You are summarizing GitHub pull request conversation context for a code review model.
|
|
215
|
+
|
|
216
|
+
Summarize the conversation so the reviewer can account for author clarifications, line-specific discussions, and previous review rounds without rereading every comment.
|
|
217
|
+
|
|
218
|
+
Rules:
|
|
219
|
+
- Use only facts present in the conversation.
|
|
220
|
+
- Preserve line-specific context with file paths and line numbers when available.
|
|
221
|
+
- Distinguish resolved prior review feedback from unresolved or still-relevant feedback when the conversation makes that clear.
|
|
222
|
+
- Capture author intent, decisions, open questions, and previous findings that may affect a re-review.
|
|
223
|
+
- Explicitly preserve any prior finding the PR author pushed back on or declined (replied disagreeing, reacted 👎, called it intentional/by-design, deferred it as out-of-scope, or said won't-fix). Record the file path, the gist of the finding, and that the author declined it, so a re-review knows not to raise it again.
|
|
224
|
+
- Do not invent status or resolution when it is not stated.
|
|
225
|
+
- Keep the summary under ${CONVERSATION_SUMMARY_TARGET_CHARS} characters.
|
|
226
|
+
- Return markdown only; do not wrap the answer in code fences.
|
|
227
|
+
|
|
228
|
+
Use this structure:
|
|
229
|
+
|
|
230
|
+
## PR conversation context
|
|
231
|
+
|
|
232
|
+
Coverage: <one sentence with the kinds of comments summarized>
|
|
233
|
+
|
|
234
|
+
### Author intent / clarifications
|
|
235
|
+
- ...
|
|
236
|
+
|
|
237
|
+
### Line-specific context
|
|
238
|
+
- ...
|
|
239
|
+
|
|
240
|
+
### Previous review rounds
|
|
241
|
+
- ...
|
|
242
|
+
|
|
243
|
+
### Findings the author declined / pushed back on (do not re-raise)
|
|
244
|
+
- ...
|
|
245
|
+
|
|
246
|
+
### Open questions / unresolved discussion
|
|
247
|
+
- ...
|
|
248
|
+
|
|
249
|
+
If a section has no relevant information, use "- None."
|
|
250
|
+
|
|
251
|
+
<pr_conversation>
|
|
252
|
+
${rawContext}
|
|
253
|
+
</pr_conversation>`;
|
|
254
|
+
}
|
|
255
|
+
async function summarizeConversationContextWithModel({ provider, credential, rawContext, workspaceDir, }) {
|
|
256
|
+
const startedAt = Date.now();
|
|
257
|
+
logger.info('PR conversation summarization started', {
|
|
258
|
+
task: 'review',
|
|
259
|
+
provider,
|
|
260
|
+
raw_chars: rawContext.length,
|
|
261
|
+
});
|
|
262
|
+
try {
|
|
263
|
+
const resolvedCredential = resolveReviewProviderCredential(credential);
|
|
264
|
+
if (!resolvedCredential) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
const result = await invokePiPrompt({
|
|
268
|
+
provider,
|
|
269
|
+
apiKey: resolvedCredential.apiKey,
|
|
270
|
+
...(resolvedCredential.piProviderId
|
|
271
|
+
? { piProviderId: resolvedCredential.piProviderId }
|
|
272
|
+
: {}),
|
|
273
|
+
...(resolvedCredential.modelName ? { modelName: resolvedCredential.modelName } : {}),
|
|
274
|
+
prompt: buildSummaryPrompt(rawContext),
|
|
275
|
+
cwd: workspaceDir ?? WORKSPACE_DIR,
|
|
276
|
+
timeoutMs: CONVERSATION_SUMMARY_TIMEOUT_MS,
|
|
277
|
+
capabilityPreset: 'read-only',
|
|
278
|
+
thinkingLevel: 'medium',
|
|
279
|
+
});
|
|
280
|
+
const durationMs = Date.now() - startedAt;
|
|
281
|
+
if (!result.ok) {
|
|
282
|
+
logger.warn('PR conversation context summarization failed', {
|
|
283
|
+
task: 'review',
|
|
284
|
+
provider,
|
|
285
|
+
raw_chars: rawContext.length,
|
|
286
|
+
duration_ms: durationMs,
|
|
287
|
+
timed_out: result.timedOut,
|
|
288
|
+
error: result.error,
|
|
289
|
+
});
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
const output = result.output.trim();
|
|
293
|
+
logger.info('PR conversation summarization finished', {
|
|
294
|
+
task: 'review',
|
|
295
|
+
provider,
|
|
296
|
+
raw_chars: rawContext.length,
|
|
297
|
+
output_chars: output.length,
|
|
298
|
+
duration_ms: durationMs,
|
|
299
|
+
outcome: output ? 'success' : 'empty',
|
|
300
|
+
});
|
|
301
|
+
return output || null;
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
logger.warn('PR conversation context summarization errored', {
|
|
305
|
+
task: 'review',
|
|
306
|
+
provider,
|
|
307
|
+
raw_chars: rawContext.length,
|
|
308
|
+
duration_ms: Date.now() - startedAt,
|
|
309
|
+
error: error instanceof Error ? error.message : String(error),
|
|
310
|
+
});
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function buildConversationItems(standalone, inlineEntries, prAuthorThumbsDownRootIds) {
|
|
315
|
+
const { threads, orphans } = buildInlineThreads(inlineEntries, prAuthorThumbsDownRootIds);
|
|
316
|
+
const items = [
|
|
317
|
+
...[...standalone, ...orphans].map((entry) => ({
|
|
318
|
+
kind: 'entry',
|
|
319
|
+
entry,
|
|
320
|
+
sortKey: entry.createdAt,
|
|
321
|
+
})),
|
|
322
|
+
...threads.map((thread) => ({
|
|
323
|
+
kind: 'thread',
|
|
324
|
+
thread,
|
|
325
|
+
sortKey: thread.root.createdAt,
|
|
326
|
+
})),
|
|
327
|
+
];
|
|
328
|
+
items.sort((a, b) => compareCreatedAt(a.sortKey, b.sortKey));
|
|
329
|
+
return items;
|
|
330
|
+
}
|
|
331
|
+
function formatConversationItems(items, prAuthor) {
|
|
332
|
+
return items
|
|
333
|
+
.map((item, index) => item.kind === 'thread'
|
|
334
|
+
? formatThread(item.thread, index, prAuthor)
|
|
335
|
+
: formatEntry(item.entry, index, prAuthor))
|
|
336
|
+
.join('\n\n---\n\n');
|
|
337
|
+
}
|
|
338
|
+
async function fetchPrAuthorThumbsDownRootIds({ octokit, owner, repo, prAuthor, inlineEntries, }) {
|
|
339
|
+
if (!prAuthor) {
|
|
340
|
+
return new Set();
|
|
341
|
+
}
|
|
342
|
+
const doistbotRootIds = inlineEntries.flatMap((entry) => {
|
|
343
|
+
if (entry.id == null ||
|
|
344
|
+
entry.replyToId != null ||
|
|
345
|
+
!isKnownDoistbotLogin(entry.author.toLowerCase())) {
|
|
346
|
+
return [];
|
|
347
|
+
}
|
|
348
|
+
return [entry.id];
|
|
349
|
+
});
|
|
350
|
+
if (doistbotRootIds.length === 0) {
|
|
351
|
+
return new Set();
|
|
352
|
+
}
|
|
353
|
+
const normalizedPrAuthor = prAuthor.trim().toLowerCase();
|
|
354
|
+
const results = await Promise.allSettled(doistbotRootIds.map(async (commentId) => {
|
|
355
|
+
const reactions = (await octokit.paginate(octokit.rest.reactions.listForPullRequestReviewComment, {
|
|
356
|
+
owner,
|
|
357
|
+
repo,
|
|
358
|
+
comment_id: commentId,
|
|
359
|
+
per_page: 100,
|
|
360
|
+
}));
|
|
361
|
+
const hasPrAuthorThumbsDown = reactions.some((reaction) => {
|
|
362
|
+
const reactionAuthor = reaction.user?.login?.trim().toLowerCase();
|
|
363
|
+
return reaction.content === '-1' && reactionAuthor === normalizedPrAuthor;
|
|
364
|
+
});
|
|
365
|
+
return hasPrAuthorThumbsDown ? commentId : null;
|
|
366
|
+
}));
|
|
367
|
+
const thumbsDownRootIds = new Set();
|
|
368
|
+
for (const result of results) {
|
|
369
|
+
if (result.status === 'fulfilled') {
|
|
370
|
+
if (result.value != null) {
|
|
371
|
+
thumbsDownRootIds.add(result.value);
|
|
372
|
+
}
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
logger.warn('Failed to fetch review comment reactions for PR conversation context', {
|
|
376
|
+
error: result.reason instanceof Error ? result.reason.message : String(result.reason),
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
return thumbsDownRootIds;
|
|
380
|
+
}
|
|
381
|
+
export async function fetchReviewConversationContext({ octokit, owner, repo, pullNumber, prAuthor, summaryApiKeys, workspaceDir, rawContextThresholdChars = RAW_CONVERSATION_CONTEXT_THRESHOLD_CHARS, summarizeConversationContext: summarize = summarizeConversationContextWithModel, }) {
|
|
382
|
+
try {
|
|
383
|
+
const [issueComments, reviews, reviewComments] = await Promise.all([
|
|
384
|
+
fetchIssueComments(octokit, {
|
|
385
|
+
owner,
|
|
386
|
+
repo,
|
|
387
|
+
issueNumber: pullNumber,
|
|
388
|
+
}),
|
|
389
|
+
octokit.paginate(octokit.rest.pulls.listReviews, {
|
|
390
|
+
owner,
|
|
391
|
+
repo,
|
|
392
|
+
pull_number: pullNumber,
|
|
393
|
+
per_page: 100,
|
|
394
|
+
}),
|
|
395
|
+
octokit.paginate(octokit.rest.pulls.listReviewComments, {
|
|
396
|
+
owner,
|
|
397
|
+
repo,
|
|
398
|
+
pull_number: pullNumber,
|
|
399
|
+
per_page: 100,
|
|
400
|
+
}),
|
|
401
|
+
]);
|
|
402
|
+
const issueCommentEntries = issueComments.flatMap((comment) => {
|
|
403
|
+
const entry = toIssueCommentEntry(comment);
|
|
404
|
+
return entry ? [entry] : [];
|
|
405
|
+
});
|
|
406
|
+
const reviewSummaryEntries = reviews.flatMap((review) => {
|
|
407
|
+
const entry = toReviewSummaryEntry(review);
|
|
408
|
+
return entry ? [entry] : [];
|
|
409
|
+
});
|
|
410
|
+
const inlineCommentEntries = reviewComments.flatMap((comment) => {
|
|
411
|
+
const entry = toInlineCommentEntry(comment);
|
|
412
|
+
return entry ? [entry] : [];
|
|
413
|
+
});
|
|
414
|
+
const prAuthorThumbsDownRootIds = await fetchPrAuthorThumbsDownRootIds({
|
|
415
|
+
octokit,
|
|
416
|
+
owner,
|
|
417
|
+
repo,
|
|
418
|
+
prAuthor,
|
|
419
|
+
inlineEntries: inlineCommentEntries,
|
|
420
|
+
});
|
|
421
|
+
const items = buildConversationItems([...issueCommentEntries, ...reviewSummaryEntries], inlineCommentEntries, prAuthorThumbsDownRootIds);
|
|
422
|
+
const rawContext = formatConversationItems(items, prAuthor);
|
|
423
|
+
const rawChars = rawContext.length;
|
|
424
|
+
const baseStats = {
|
|
425
|
+
issueComments: issueCommentEntries.length,
|
|
426
|
+
reviewSummaries: reviewSummaryEntries.length,
|
|
427
|
+
inlineComments: inlineCommentEntries.length,
|
|
428
|
+
rawChars,
|
|
429
|
+
};
|
|
430
|
+
if (items.length === 0) {
|
|
431
|
+
return emptyContext(baseStats);
|
|
432
|
+
}
|
|
433
|
+
if (rawChars <= rawContextThresholdChars) {
|
|
434
|
+
return {
|
|
435
|
+
promptText: rawContext,
|
|
436
|
+
stats: buildStats(baseStats),
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
const summaryProviders = getSummaryProviders(summaryApiKeys);
|
|
440
|
+
if (summaryProviders.length === 0) {
|
|
441
|
+
logger.warn('PR conversation context omitted because summarization requires a review provider', {
|
|
442
|
+
rawChars,
|
|
443
|
+
rawContextThresholdChars,
|
|
444
|
+
});
|
|
445
|
+
return emptyContext(baseStats, true);
|
|
446
|
+
}
|
|
447
|
+
for (const summaryProvider of summaryProviders) {
|
|
448
|
+
const summarizedContext = await summarize({
|
|
449
|
+
...summaryProvider,
|
|
450
|
+
rawContext,
|
|
451
|
+
workspaceDir,
|
|
452
|
+
});
|
|
453
|
+
if (summarizedContext) {
|
|
454
|
+
return {
|
|
455
|
+
promptText: summarizedContext,
|
|
456
|
+
stats: buildStats(baseStats, { summarized: true }),
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
logger.warn('PR conversation context omitted because summarization returned no output', {
|
|
461
|
+
rawChars,
|
|
462
|
+
rawContextThresholdChars,
|
|
463
|
+
});
|
|
464
|
+
return emptyContext(baseStats, true);
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
logger.warn('Failed to fetch PR conversation context', {
|
|
468
|
+
error: error instanceof Error ? error.message : String(error),
|
|
469
|
+
});
|
|
470
|
+
return emptyContext({
|
|
471
|
+
issueComments: 0,
|
|
472
|
+
reviewSummaries: 0,
|
|
473
|
+
inlineComments: 0,
|
|
474
|
+
rawChars: 0,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export const MAX_DIFF_CHARS = 128000;
|
|
2
|
+
/**
|
|
3
|
+
* Converts GitHub's file list (with unified patches) into annotated diffs plus fast line lookups.
|
|
4
|
+
*/
|
|
5
|
+
export function buildAnnotatedDiff(files) {
|
|
6
|
+
const annotatedFiles = [];
|
|
7
|
+
const lineLookup = new Map();
|
|
8
|
+
for (const file of files) {
|
|
9
|
+
if (!file.patch) {
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
const { annotatedDiff, oldLineNumbers, newLineNumbers } = annotatePatch(file.patch);
|
|
13
|
+
if (!annotatedDiff.trim()) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
annotatedFiles.push({ path: file.filename, annotatedDiff });
|
|
17
|
+
lineLookup.set(file.filename, { old: oldLineNumbers, new: newLineNumbers });
|
|
18
|
+
}
|
|
19
|
+
return { annotatedFiles, lineLookup };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Adds `[NEW:L#]`, `[OLD:L#]`, `[CTX:L#]` markers to every line in a unified diff.
|
|
23
|
+
*/
|
|
24
|
+
function annotatePatch(patch) {
|
|
25
|
+
const lines = patch.split('\n');
|
|
26
|
+
let oldLine = 0;
|
|
27
|
+
let newLine = 0;
|
|
28
|
+
const annotated = [];
|
|
29
|
+
const oldLineNumbers = new Set();
|
|
30
|
+
const newLineNumbers = new Set();
|
|
31
|
+
for (const line of lines) {
|
|
32
|
+
if (line.startsWith('@@')) {
|
|
33
|
+
annotated.push(line);
|
|
34
|
+
const match = /@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line);
|
|
35
|
+
if (match) {
|
|
36
|
+
oldLine = Number(match[1]);
|
|
37
|
+
newLine = Number(match[2]);
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (line.startsWith('+')) {
|
|
42
|
+
annotated.push(`[NEW:L${newLine}]+ ${line.slice(1)}`);
|
|
43
|
+
newLineNumbers.add(newLine);
|
|
44
|
+
newLine += 1;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (line.startsWith('-')) {
|
|
48
|
+
annotated.push(`[OLD:L${oldLine}]- ${line.slice(1)}`);
|
|
49
|
+
oldLineNumbers.add(oldLine);
|
|
50
|
+
oldLine += 1;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (line.startsWith(' ')) {
|
|
54
|
+
annotated.push(`[CTX:L${newLine}] ${line.slice(1)}`);
|
|
55
|
+
oldLine += 1;
|
|
56
|
+
newLine += 1;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
// Covers lines such as ""
|
|
60
|
+
annotated.push(line);
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
annotatedDiff: annotated.join('\n'),
|
|
64
|
+
oldLineNumbers,
|
|
65
|
+
newLineNumbers,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Joins annotated diffs into a single prompt-friendly string while respecting length limits.
|
|
70
|
+
*/
|
|
71
|
+
export function buildDiffSection(annotatedFiles) {
|
|
72
|
+
let consumed = 0;
|
|
73
|
+
let truncated = false;
|
|
74
|
+
const chunks = [];
|
|
75
|
+
for (const file of annotatedFiles) {
|
|
76
|
+
const header = `### ${file.path}\n${file.annotatedDiff}`;
|
|
77
|
+
const remaining = MAX_DIFF_CHARS - consumed;
|
|
78
|
+
if (remaining <= 0) {
|
|
79
|
+
truncated = true;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
if (header.length > remaining) {
|
|
83
|
+
chunks.push(header.slice(0, remaining));
|
|
84
|
+
truncated = true;
|
|
85
|
+
consumed = MAX_DIFF_CHARS;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
chunks.push(header);
|
|
89
|
+
consumed += header.length;
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
diffSection: chunks.join('\n\n'),
|
|
93
|
+
truncated,
|
|
94
|
+
};
|
|
95
|
+
}
|