@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,440 @@
|
|
|
1
|
+
import { performance } from 'node:perf_hooks';
|
|
2
|
+
import { SpanStatusCode } from '@opentelemetry/api';
|
|
3
|
+
import { isKnownDoistbotLogin } from '../../core/bot-logins.js';
|
|
4
|
+
import { recordTaskMetrics } from '../../core/datadog-metrics.js';
|
|
5
|
+
import { parseBaseContext, requiredEnv, WORKSPACE_DIR } from '../../core/environment.js';
|
|
6
|
+
import { logger } from '../../core/logging.js';
|
|
7
|
+
import { invokePiPrompt } from '../../core/pi.js';
|
|
8
|
+
import { loadPromptTemplateFile, resolvePromptFile } from '../../core/prompt-template.js';
|
|
9
|
+
import { removeIssueCommentReactionIfPresent } from '../../core/reactions.js';
|
|
10
|
+
import { cloneRepository, createGitHubClients } from '../../core/shared.js';
|
|
11
|
+
import { withPhase } from '../../core/tracing.js';
|
|
12
|
+
import { shutdownTracing } from '../../core/tracing-setup.js';
|
|
13
|
+
import { buildBroPrompt, buildPrompt } from './prompt.js';
|
|
14
|
+
const CHAT_PROMPT_FILE = 'chat-prompt.md';
|
|
15
|
+
const BRO_PROMPT_FILE = 'bro-prompt.md';
|
|
16
|
+
const CHAT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
17
|
+
const CHAT_PROVIDER = 'openai';
|
|
18
|
+
const CHAT_MODEL = 'gpt-6-luna';
|
|
19
|
+
const CHAT_THINKING_LEVEL = 'high';
|
|
20
|
+
const BRO_SYSTEM_PROMPT = 'You are Doistbot re-explaining one of your own review comments. Use only the source comment in the task prompt. Do not inspect files, call tools, or add facts. Output only the rewritten explanation.';
|
|
21
|
+
const EMPTY_BRO_RESPONSE = "There's no earlier Doistbot comment in this review thread for me to simplify, bro.";
|
|
22
|
+
function parseContext() {
|
|
23
|
+
const base = parseBaseContext();
|
|
24
|
+
const reactionIdRaw = process.env.REACTION_ID;
|
|
25
|
+
const reactionId = reactionIdRaw ? Number(reactionIdRaw) : undefined;
|
|
26
|
+
return {
|
|
27
|
+
...base,
|
|
28
|
+
commentId: Number(requiredEnv('COMMENT_ID')),
|
|
29
|
+
reactionId: Number.isFinite(reactionId) ? reactionId : undefined,
|
|
30
|
+
commentType: requiredEnv('COMMENT_TYPE'),
|
|
31
|
+
eventName: requiredEnv('EVENT_NAME'),
|
|
32
|
+
mode: process.env.CHAT_MODE === 'bro' ? 'bro' : 'chat',
|
|
33
|
+
dryRun: process.env.DRY_RUN?.toLowerCase() === 'true',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function findLatestDoistbotComment(entries) {
|
|
37
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
38
|
+
const entry = entries[index];
|
|
39
|
+
if (entry && isKnownDoistbotLogin(entry.author)) {
|
|
40
|
+
return entry.body;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Gathers context for a review comment (inline code comment thread).
|
|
47
|
+
*/
|
|
48
|
+
export async function gatherReviewCommentContext(octokit, owner, repo, commentId, mode = 'chat') {
|
|
49
|
+
const comment = await octokit.rest.pulls.getReviewComment({
|
|
50
|
+
owner,
|
|
51
|
+
repo,
|
|
52
|
+
comment_id: commentId,
|
|
53
|
+
});
|
|
54
|
+
const threadEntries = [];
|
|
55
|
+
let inReplyToId = comment.data.in_reply_to_id;
|
|
56
|
+
while (inReplyToId) {
|
|
57
|
+
try {
|
|
58
|
+
const parentComment = await octokit.rest.pulls.getReviewComment({
|
|
59
|
+
owner,
|
|
60
|
+
repo,
|
|
61
|
+
comment_id: inReplyToId,
|
|
62
|
+
});
|
|
63
|
+
const parentAuthor = parentComment.data.user?.login ?? 'unknown';
|
|
64
|
+
const parentBody = parentComment.data.body ?? '';
|
|
65
|
+
if (mode === 'bro' && isKnownDoistbotLogin(parentAuthor)) {
|
|
66
|
+
return {
|
|
67
|
+
previousDoistbotComment: parentBody,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (mode === 'bro') {
|
|
71
|
+
inReplyToId = parentComment.data.in_reply_to_id;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
threadEntries.unshift({ author: parentAuthor, body: parentBody });
|
|
75
|
+
inReplyToId = parentComment.data.in_reply_to_id;
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
logger.warn('Failed to fetch parent comment', {
|
|
79
|
+
commentId: inReplyToId,
|
|
80
|
+
error: error instanceof Error ? error.message : String(error),
|
|
81
|
+
});
|
|
82
|
+
if (mode === 'bro') {
|
|
83
|
+
throw new Error('Failed to fetch the review thread for bro mode', { cause: error });
|
|
84
|
+
}
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (mode === 'bro') {
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
const codeSnippet = comment.data.diff_hunk ?? '';
|
|
92
|
+
const commentBody = comment.data.body ?? '';
|
|
93
|
+
const currentCommentAuthor = comment.data.user?.login ?? 'unknown';
|
|
94
|
+
const previousDoistbotComment = findLatestDoistbotComment(threadEntries);
|
|
95
|
+
threadEntries.push({ author: currentCommentAuthor, body: commentBody });
|
|
96
|
+
const userQuestion = commentBody.replace(/@doistbot\s*/i, '').trim();
|
|
97
|
+
return {
|
|
98
|
+
threadHistory: threadEntries
|
|
99
|
+
.map(({ author, body }) => `**${author}**: ${body}`)
|
|
100
|
+
.join('\n\n'),
|
|
101
|
+
userQuestion,
|
|
102
|
+
codeSnippet,
|
|
103
|
+
previousDoistbotComment,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Gathers context for a general PR comment (not tied to specific code).
|
|
108
|
+
*/
|
|
109
|
+
async function gatherIssueCommentContext(octokit, owner, repo, prNumber, commentId) {
|
|
110
|
+
const [pr, comment] = await Promise.all([
|
|
111
|
+
octokit.rest.pulls.get({
|
|
112
|
+
owner,
|
|
113
|
+
repo,
|
|
114
|
+
pull_number: prNumber,
|
|
115
|
+
}),
|
|
116
|
+
octokit.rest.issues.getComment({
|
|
117
|
+
owner,
|
|
118
|
+
repo,
|
|
119
|
+
comment_id: commentId,
|
|
120
|
+
}),
|
|
121
|
+
]);
|
|
122
|
+
const prTitle = pr.data.title ?? '';
|
|
123
|
+
const prAuthor = pr.data.user?.login ?? 'unknown';
|
|
124
|
+
const commentBody = comment.data.body ?? '';
|
|
125
|
+
// Get recent doistbot reviews to provide context
|
|
126
|
+
let threadHistory = '';
|
|
127
|
+
try {
|
|
128
|
+
const reviews = await octokit.rest.pulls.listReviews({
|
|
129
|
+
owner,
|
|
130
|
+
repo,
|
|
131
|
+
pull_number: prNumber,
|
|
132
|
+
});
|
|
133
|
+
const doistbotReview = reviews.data
|
|
134
|
+
.filter((review) => {
|
|
135
|
+
const login = review.user?.login?.toLowerCase() ?? '';
|
|
136
|
+
return isKnownDoistbotLogin(login) || login === 'github-actions[bot]';
|
|
137
|
+
})
|
|
138
|
+
.sort((a, b) => {
|
|
139
|
+
const dateA = new Date(a.submitted_at ?? 0);
|
|
140
|
+
const dateB = new Date(b.submitted_at ?? 0);
|
|
141
|
+
return dateB.getTime() - dateA.getTime();
|
|
142
|
+
})[0];
|
|
143
|
+
if (doistbotReview?.body) {
|
|
144
|
+
threadHistory = `**Previous Review Summary:**\n\n${doistbotReview.body}`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
logger.warn('Failed to fetch reviews', {
|
|
149
|
+
error: error instanceof Error ? error.message : String(error),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const userQuestion = commentBody.replace(/@doistbot\s*/i, '').trim();
|
|
153
|
+
return {
|
|
154
|
+
threadHistory,
|
|
155
|
+
userQuestion,
|
|
156
|
+
prTitle,
|
|
157
|
+
prAuthor,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
export async function invokeChatModel(prompt, mode = 'chat') {
|
|
161
|
+
const apiKey = process.env.OPENAI_KEY?.trim();
|
|
162
|
+
if (!apiKey) {
|
|
163
|
+
// Config error: no model call attempted, so no model span. The reason is
|
|
164
|
+
// returned so the caller's root span reports the real cause.
|
|
165
|
+
logger.warn('Chat model API key missing');
|
|
166
|
+
return { ok: false, reason: 'OPENAI_KEY missing' };
|
|
167
|
+
}
|
|
168
|
+
return withPhase('chat.model', {
|
|
169
|
+
'gen_ai.operation.name': 'chat',
|
|
170
|
+
'gen_ai.provider.name': CHAT_PROVIDER,
|
|
171
|
+
'gen_ai.request.model': CHAT_MODEL,
|
|
172
|
+
}, async (span) => {
|
|
173
|
+
const result = await invokePiPrompt({
|
|
174
|
+
provider: CHAT_PROVIDER,
|
|
175
|
+
apiKey,
|
|
176
|
+
prompt,
|
|
177
|
+
cwd: WORKSPACE_DIR,
|
|
178
|
+
timeoutMs: CHAT_TIMEOUT_MS,
|
|
179
|
+
capabilityPreset: mode === 'bro' ? 'none' : 'workspace-inspection',
|
|
180
|
+
thinkingLevel: CHAT_THINKING_LEVEL,
|
|
181
|
+
// Keep the executed model in sync with the recorded span attr.
|
|
182
|
+
modelName: CHAT_MODEL,
|
|
183
|
+
...(mode === 'bro'
|
|
184
|
+
? {
|
|
185
|
+
systemPrompt: BRO_SYSTEM_PROMPT,
|
|
186
|
+
noContextFiles: true,
|
|
187
|
+
}
|
|
188
|
+
: {}),
|
|
189
|
+
});
|
|
190
|
+
// Record why the model stopped, before the ok/failure branch so
|
|
191
|
+
// terminal "error"/"aborted"/"length" endings are reported too.
|
|
192
|
+
// stopReason values:
|
|
193
|
+
// stop Natural end — model finished or hit a stop sequence.
|
|
194
|
+
// length Hit the token cap (max_tokens or context limit).
|
|
195
|
+
// toolUse Paused to call a tool. Expected mid-agent-loop.
|
|
196
|
+
// error Provider errored mid-generation.
|
|
197
|
+
// aborted Generation was cancelled (e.g. signal/timeout).
|
|
198
|
+
if (result.stopReason) {
|
|
199
|
+
span.setAttribute('gen_ai.response.finish_reasons', [result.stopReason]);
|
|
200
|
+
}
|
|
201
|
+
if (!result.ok) {
|
|
202
|
+
// Failure returns a result rather than throwing — mark the span
|
|
203
|
+
// ERROR explicitly so it survives to phase.end (withPhase only
|
|
204
|
+
// auto-errors on a throw).
|
|
205
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: result.error });
|
|
206
|
+
logger.warn('Chat model request failed', {
|
|
207
|
+
error: result.error,
|
|
208
|
+
timedOut: result.timedOut,
|
|
209
|
+
});
|
|
210
|
+
return { ok: false, reason: result.error };
|
|
211
|
+
}
|
|
212
|
+
const output = result.output.trim();
|
|
213
|
+
if (!output) {
|
|
214
|
+
const reason = 'chat model produced no output';
|
|
215
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: reason });
|
|
216
|
+
return { ok: false, reason };
|
|
217
|
+
}
|
|
218
|
+
return { ok: true, output };
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Posts the response to the correct location.
|
|
223
|
+
*/
|
|
224
|
+
async function postResponse(octokit, ctx, response) {
|
|
225
|
+
if (ctx.commentType === 'review') {
|
|
226
|
+
await octokit.rest.pulls.createReplyForReviewComment({
|
|
227
|
+
owner: ctx.owner,
|
|
228
|
+
repo: ctx.repo,
|
|
229
|
+
pull_number: ctx.prNumber,
|
|
230
|
+
comment_id: ctx.commentId,
|
|
231
|
+
body: response,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
await octokit.rest.issues.createComment({
|
|
236
|
+
owner: ctx.owner,
|
|
237
|
+
repo: ctx.repo,
|
|
238
|
+
issue_number: ctx.prNumber,
|
|
239
|
+
body: response,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
logger.info('Posted chat response', {
|
|
243
|
+
commentType: ctx.commentType,
|
|
244
|
+
commentId: ctx.commentId,
|
|
245
|
+
prNumber: ctx.prNumber,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
function formatChatDryRunOutput(ctx, response) {
|
|
249
|
+
return JSON.stringify({
|
|
250
|
+
task: 'chat',
|
|
251
|
+
repository: `${ctx.owner}/${ctx.repo}`,
|
|
252
|
+
prNumber: ctx.prNumber,
|
|
253
|
+
commentId: ctx.commentId,
|
|
254
|
+
commentType: ctx.commentType,
|
|
255
|
+
eventName: ctx.eventName,
|
|
256
|
+
mode: ctx.mode,
|
|
257
|
+
response,
|
|
258
|
+
}, null, 2);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Removes the 👀 reaction from the triggering comment.
|
|
262
|
+
*/
|
|
263
|
+
async function removeReaction(octokit, ctx, reactionId) {
|
|
264
|
+
try {
|
|
265
|
+
if (ctx.commentType === 'review') {
|
|
266
|
+
await octokit.rest.reactions.deleteForPullRequestComment({
|
|
267
|
+
owner: ctx.owner,
|
|
268
|
+
repo: ctx.repo,
|
|
269
|
+
comment_id: ctx.commentId,
|
|
270
|
+
reaction_id: reactionId,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
await removeIssueCommentReactionIfPresent({
|
|
275
|
+
owner: ctx.owner,
|
|
276
|
+
repo: ctx.repo,
|
|
277
|
+
commentId: ctx.commentId,
|
|
278
|
+
reactionId,
|
|
279
|
+
octokit,
|
|
280
|
+
warnMessage: 'Failed to remove reaction',
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
logger.warn('Failed to remove reaction', {
|
|
286
|
+
error: error instanceof Error ? error.message : String(error),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async function main() {
|
|
291
|
+
const startedAt = performance.now();
|
|
292
|
+
const ctx = parseContext();
|
|
293
|
+
const logContext = {
|
|
294
|
+
deliveryId: ctx.deliveryId ?? 'unknown',
|
|
295
|
+
repository: `${ctx.owner}/${ctx.repo}`,
|
|
296
|
+
prNumber: ctx.prNumber,
|
|
297
|
+
commentId: ctx.commentId,
|
|
298
|
+
};
|
|
299
|
+
logger.setBaseContext(logContext);
|
|
300
|
+
const reactionId = ctx.reactionId;
|
|
301
|
+
let exitCode = 0;
|
|
302
|
+
let metricOutcome = 'failure';
|
|
303
|
+
await withPhase('chat', {
|
|
304
|
+
task: 'chat',
|
|
305
|
+
'gen_ai.operation.name': 'invoke_agent',
|
|
306
|
+
'gen_ai.agent.name': 'doistbot.chat',
|
|
307
|
+
'chat.mode': ctx.mode,
|
|
308
|
+
'pr.number': ctx.prNumber,
|
|
309
|
+
'comment.id': ctx.commentId,
|
|
310
|
+
'comment.type': ctx.commentType,
|
|
311
|
+
'event.name': ctx.eventName,
|
|
312
|
+
dry_run: ctx.dryRun,
|
|
313
|
+
}, async (span) => {
|
|
314
|
+
// Inside the span so auth/setup failures are captured by the root span.
|
|
315
|
+
const { app, readOctokit, writeOctokit } = await createGitHubClients(ctx);
|
|
316
|
+
try {
|
|
317
|
+
// /bro only rewrites an existing comment, so it does not need repository contents.
|
|
318
|
+
if (ctx.mode !== 'bro') {
|
|
319
|
+
await cloneRepository({
|
|
320
|
+
app,
|
|
321
|
+
owner: ctx.owner,
|
|
322
|
+
repo: ctx.repo,
|
|
323
|
+
prNumber: ctx.prNumber,
|
|
324
|
+
installationId: ctx.installationId,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
// Gather thread context from GitHub API
|
|
328
|
+
let threadContext;
|
|
329
|
+
if (ctx.commentType === 'review') {
|
|
330
|
+
threadContext = await gatherReviewCommentContext(readOctokit, ctx.owner, ctx.repo, ctx.commentId, ctx.mode);
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
threadContext = await gatherIssueCommentContext(readOctokit, ctx.owner, ctx.repo, ctx.prNumber, ctx.commentId);
|
|
334
|
+
}
|
|
335
|
+
let response;
|
|
336
|
+
if (ctx.mode === 'bro' && !threadContext.previousDoistbotComment) {
|
|
337
|
+
response = EMPTY_BRO_RESPONSE;
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
const promptTemplate = loadPromptTemplateFile({
|
|
341
|
+
fileName: resolvePromptFile(import.meta.url, ctx.mode === 'bro' ? BRO_PROMPT_FILE : CHAT_PROMPT_FILE),
|
|
342
|
+
promptLabel: ctx.mode,
|
|
343
|
+
});
|
|
344
|
+
const prompt = ctx.mode === 'bro'
|
|
345
|
+
? buildBroPrompt({
|
|
346
|
+
promptTemplate,
|
|
347
|
+
previousDoistbotComment: threadContext.previousDoistbotComment ?? '',
|
|
348
|
+
})
|
|
349
|
+
: buildPrompt({
|
|
350
|
+
promptTemplate,
|
|
351
|
+
owner: ctx.owner,
|
|
352
|
+
repo: ctx.repo,
|
|
353
|
+
prNumber: ctx.prNumber,
|
|
354
|
+
threadHistory: threadContext.threadHistory ?? '',
|
|
355
|
+
userQuestion: threadContext.userQuestion ?? '',
|
|
356
|
+
codeSnippet: threadContext.codeSnippet,
|
|
357
|
+
prTitle: threadContext.prTitle,
|
|
358
|
+
prAuthor: threadContext.prAuthor,
|
|
359
|
+
});
|
|
360
|
+
const modelResult = await invokeChatModel(prompt, ctx.mode);
|
|
361
|
+
if (!modelResult.ok) {
|
|
362
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: modelResult.reason });
|
|
363
|
+
logger.warn('Chat model produced no usable response', {
|
|
364
|
+
reason: modelResult.reason,
|
|
365
|
+
});
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
response = modelResult.output;
|
|
369
|
+
}
|
|
370
|
+
if (ctx.dryRun) {
|
|
371
|
+
logger.info('Dry run enabled, printing chat response instead of posting to GitHub');
|
|
372
|
+
process.stdout.write(`${formatChatDryRunOutput(ctx, response)}\n`);
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
await postResponse(writeOctokit, ctx, response);
|
|
376
|
+
}
|
|
377
|
+
metricOutcome = 'success';
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
381
|
+
logger.error('Chat task failed', { error: message });
|
|
382
|
+
// The catch swallows the error to post a user reply, so set the
|
|
383
|
+
// span status explicitly — withPhase only auto-errors on rethrow.
|
|
384
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message });
|
|
385
|
+
if (ctx.dryRun) {
|
|
386
|
+
logger.info('Dry run enabled, skipping chat error response post');
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
try {
|
|
390
|
+
const errorMessage = 'Sorry, I encountered an error while processing your request. Please try again or contact the team maintaining the bot.';
|
|
391
|
+
if (ctx.commentType === 'review') {
|
|
392
|
+
await writeOctokit.rest.pulls.createReplyForReviewComment({
|
|
393
|
+
owner: ctx.owner,
|
|
394
|
+
repo: ctx.repo,
|
|
395
|
+
pull_number: ctx.prNumber,
|
|
396
|
+
comment_id: ctx.commentId,
|
|
397
|
+
body: errorMessage,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
await writeOctokit.rest.issues.createComment({
|
|
402
|
+
owner: ctx.owner,
|
|
403
|
+
repo: ctx.repo,
|
|
404
|
+
issue_number: ctx.prNumber,
|
|
405
|
+
body: errorMessage,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
catch (replyError) {
|
|
410
|
+
logger.error('Failed to post error message', {
|
|
411
|
+
error: replyError instanceof Error
|
|
412
|
+
? replyError.message
|
|
413
|
+
: String(replyError),
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
exitCode = 1;
|
|
418
|
+
}
|
|
419
|
+
finally {
|
|
420
|
+
// Local dry runs must stay side-effect free.
|
|
421
|
+
if (reactionId && !ctx.dryRun) {
|
|
422
|
+
await removeReaction(writeOctokit, ctx, reactionId);
|
|
423
|
+
}
|
|
424
|
+
await recordTaskMetrics({
|
|
425
|
+
type: 'chat',
|
|
426
|
+
repository: `${ctx.owner}/${ctx.repo}`,
|
|
427
|
+
model: 'openai',
|
|
428
|
+
outcome: metricOutcome,
|
|
429
|
+
durationMs: performance.now() - startedAt,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
if (exitCode !== 0) {
|
|
434
|
+
await shutdownTracing();
|
|
435
|
+
process.exit(exitCode);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (process.env.VITEST !== 'true') {
|
|
439
|
+
main();
|
|
440
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { fillPromptTemplate } from '../../core/prompt-template.js';
|
|
2
|
+
/**
|
|
3
|
+
* Fills the chat prompt template with variable substitution.
|
|
4
|
+
*/
|
|
5
|
+
export function buildPrompt({ promptTemplate, owner, repo, prNumber, threadHistory, userQuestion, codeSnippet, prTitle, prAuthor, }) {
|
|
6
|
+
return fillPromptTemplate(promptTemplate, {
|
|
7
|
+
PR_NUMBER: String(prNumber),
|
|
8
|
+
REPO_FULL: `${owner}/${repo}`,
|
|
9
|
+
PR_TITLE: prTitle ?? '',
|
|
10
|
+
PR_AUTHOR: prAuthor ?? 'unknown',
|
|
11
|
+
THREAD_HISTORY: threadHistory || 'No previous conversation.',
|
|
12
|
+
USER_QUESTION: userQuestion ?? '',
|
|
13
|
+
CODE_SNIPPET: codeSnippet ? `\`\`\`\n${codeSnippet}\n\`\`\`` : 'No code context available.',
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export function buildBroPrompt({ promptTemplate, previousDoistbotComment, }) {
|
|
17
|
+
return fillPromptTemplate(promptTemplate, {
|
|
18
|
+
PREVIOUS_DOISTBOT_COMMENT: previousDoistbotComment,
|
|
19
|
+
});
|
|
20
|
+
}
|