@doist/doistbot-cli 1.0.6 → 1.0.8
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 +43 -11
- package/dist/actions/doctor.js +4 -2
- package/dist/actions/review.js +11 -14
- package/dist/auth.js +23 -6
- package/dist/config.js +58 -12
- package/package.json +3 -3
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +1 -1
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +1 -1
- package/sandbox/dist/core/pi.js +31 -1
- package/sandbox/dist/core/prompt-template.js +45 -3
- package/sandbox/dist/core/repository-map.js +24 -0
- package/sandbox/dist/core/tracing.js +9 -0
- package/sandbox/dist/providers/helpers.js +51 -0
- package/sandbox/dist/providers/pi-review.js +7 -1
- package/sandbox/dist/tasks/chat/chat.js +20 -8
- package/sandbox/dist/tasks/issue-summarize/model.js +119 -48
- package/sandbox/dist/tasks/issue-summarize/prompt.js +2 -2
- package/sandbox/dist/tasks/issue-summarize/summarize.js +104 -88
- package/sandbox/dist/tasks/issue-triage/fix-attempt.js +3 -2
- package/sandbox/dist/tasks/issue-triage/hero-group-map.js +2 -0
- package/sandbox/dist/tasks/issue-triage/pr-creator.js +2 -2
- package/sandbox/dist/tasks/issue-triage/prompt.js +2 -2
- package/sandbox/dist/tasks/issue-triage/triage.js +2 -2
- package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +3 -3
- package/sandbox/dist/tasks/review/engines/dedupe.js +107 -42
- package/sandbox/dist/tasks/review/engines/multi-focus.js +229 -119
- package/sandbox/dist/tasks/review/engines/shared.js +36 -1
- package/sandbox/dist/tasks/review/github-comment-format.js +12 -4
- package/sandbox/dist/tasks/review/local.js +6 -1
- package/sandbox/dist/tasks/review/review-summary.js +3 -0
- package/sandbox/dist/tasks/review/review.js +253 -186
- package/sandbox/dist/tasks/review/runner.js +10 -5
- package/sandbox/dist/tasks/review/summary-model.js +40 -18
- package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +1 -1
- package/sandbox/node_modules/doistbot-repo-config/dist/index.js +1 -1
- package/sandbox/src/tasks/review/prompts/review-multi-focus-base-prompt.md +1 -0
|
@@ -6,6 +6,57 @@ import { logger } from '../core/logging.js';
|
|
|
6
6
|
import { isRawReviewResult } from './schemas.js';
|
|
7
7
|
const RAW_MODEL_LOG_CHUNK_CHARS = 8000;
|
|
8
8
|
const REVIEW_LOG_ENGINE = 'multi_focus';
|
|
9
|
+
const PROVIDER_ERROR_DETAILS_MAX_DEPTH = 8;
|
|
10
|
+
function isRecord(value) {
|
|
11
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
function parseJsonValue(value) {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(value.trim());
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function readProviderErrorDetails(error) {
|
|
22
|
+
const rawError = error instanceof Error ? error.message : String(error);
|
|
23
|
+
const details = { rawError };
|
|
24
|
+
function visit(value, depth) {
|
|
25
|
+
if (depth > PROVIDER_ERROR_DETAILS_MAX_DEPTH) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (typeof value === 'string') {
|
|
29
|
+
const trimmed = value.trim();
|
|
30
|
+
const parsed = parseJsonValue(trimmed);
|
|
31
|
+
if (isRecord(parsed)) {
|
|
32
|
+
visit(parsed, depth + 1);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (!details.reason && trimmed) {
|
|
36
|
+
details.reason = trimmed;
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!isRecord(value)) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (value.error !== undefined) {
|
|
44
|
+
visit(value.error, depth + 1);
|
|
45
|
+
}
|
|
46
|
+
if (typeof value.message === 'string') {
|
|
47
|
+
visit(value.message, depth + 1);
|
|
48
|
+
}
|
|
49
|
+
if (details.errorCode === undefined &&
|
|
50
|
+
(typeof value.code === 'number' || typeof value.code === 'string')) {
|
|
51
|
+
details.errorCode = value.code;
|
|
52
|
+
}
|
|
53
|
+
if (details.status === undefined && typeof value.status === 'string') {
|
|
54
|
+
details.status = value.status;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
visit(rawError, 0);
|
|
58
|
+
return details;
|
|
59
|
+
}
|
|
9
60
|
function redactSensitiveStrings(content) {
|
|
10
61
|
return content
|
|
11
62
|
.replace(/(sk-[A-Za-z0-9_-]{10,})/g, '[REDACTED_OPENAI_KEY]')
|
|
@@ -53,7 +53,13 @@ function isOpenRouterBackedReviewRequest(provider, config) {
|
|
|
53
53
|
PI_PROVIDER_BY_DOISTBOT_PROVIDER[provider];
|
|
54
54
|
return resolvedPiProviderId === 'openrouter';
|
|
55
55
|
}
|
|
56
|
-
export async function runPiReviewProvider(
|
|
56
|
+
export async function runPiReviewProvider(params) {
|
|
57
|
+
// Stamp the resolved model spec on every result (success or failure) so
|
|
58
|
+
// telemetry has the real model without re-deriving each provider's default.
|
|
59
|
+
const result = await runPiReviewProviderCore(params);
|
|
60
|
+
return { ...result, requestModel: params.modelName };
|
|
61
|
+
}
|
|
62
|
+
async function runPiReviewProviderCore({ provider, modelName, prompt, config, prompts = [prompt], parseOutput, }) {
|
|
57
63
|
const startTime = Date.now();
|
|
58
64
|
const timeoutMs = config.timeout ?? 0;
|
|
59
65
|
let stdout = '';
|
|
@@ -4,15 +4,16 @@ import { isKnownDoistbotLogin } from '../../core/bot-logins.js';
|
|
|
4
4
|
import { recordTaskMetrics } from '../../core/datadog-metrics.js';
|
|
5
5
|
import { logger } from '../../core/logging.js';
|
|
6
6
|
import { invokePiPrompt } from '../../core/pi.js';
|
|
7
|
-
import { loadPromptTemplateFile } from '../../core/prompt-template.js';
|
|
7
|
+
import { loadPromptTemplateFile, resolvePromptFile } from '../../core/prompt-template.js';
|
|
8
8
|
import { removeIssueCommentReactionIfPresent } from '../../core/reactions.js';
|
|
9
9
|
import { cloneRepository, createGitHubClients, parseBaseContext, requiredEnv, WORKSPACE_DIR, } from '../../core/shared.js';
|
|
10
10
|
import { withPhase } from '../../core/tracing.js';
|
|
11
11
|
import { buildPrompt } from './prompt.js';
|
|
12
12
|
const CHAT_PROMPT_FILE = 'chat-prompt.md';
|
|
13
13
|
const CHAT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
14
|
-
const CHAT_PROVIDER = '
|
|
15
|
-
const CHAT_MODEL = '
|
|
14
|
+
const CHAT_PROVIDER = 'openai';
|
|
15
|
+
const CHAT_MODEL = 'gpt-5.6-luna';
|
|
16
|
+
const CHAT_THINKING_LEVEL = 'high';
|
|
16
17
|
function parseContext() {
|
|
17
18
|
const base = parseBaseContext();
|
|
18
19
|
const reactionIdRaw = process.env.REACTION_ID;
|
|
@@ -125,12 +126,12 @@ async function gatherIssueCommentContext(octokit, owner, repo, prNumber, comment
|
|
|
125
126
|
};
|
|
126
127
|
}
|
|
127
128
|
export async function invokeChatModel(prompt) {
|
|
128
|
-
const apiKey = process.env.
|
|
129
|
+
const apiKey = process.env.OPENAI_KEY?.trim();
|
|
129
130
|
if (!apiKey) {
|
|
130
131
|
// Config error: no model call attempted, so no model span. The reason is
|
|
131
132
|
// returned so the caller's root span reports the real cause.
|
|
132
133
|
logger.warn('Chat model API key missing');
|
|
133
|
-
return { ok: false, reason: '
|
|
134
|
+
return { ok: false, reason: 'OPENAI_KEY missing' };
|
|
134
135
|
}
|
|
135
136
|
return withPhase('chat.model', {
|
|
136
137
|
'gen_ai.operation.name': 'chat',
|
|
@@ -140,14 +141,25 @@ export async function invokeChatModel(prompt) {
|
|
|
140
141
|
const result = await invokePiPrompt({
|
|
141
142
|
provider: CHAT_PROVIDER,
|
|
142
143
|
apiKey,
|
|
143
|
-
piProviderId: 'openrouter',
|
|
144
144
|
prompt,
|
|
145
145
|
cwd: WORKSPACE_DIR,
|
|
146
146
|
timeoutMs: CHAT_TIMEOUT_MS,
|
|
147
147
|
capabilityPreset: 'workspace-inspection',
|
|
148
|
+
thinkingLevel: CHAT_THINKING_LEVEL,
|
|
148
149
|
// Keep the executed model in sync with the recorded span attr.
|
|
149
150
|
modelName: CHAT_MODEL,
|
|
150
151
|
});
|
|
152
|
+
// Record why the model stopped, before the ok/failure branch so
|
|
153
|
+
// terminal "error"/"aborted"/"length" endings are reported too.
|
|
154
|
+
// stopReason values:
|
|
155
|
+
// stop Natural end — model finished or hit a stop sequence.
|
|
156
|
+
// length Hit the token cap (max_tokens or context limit).
|
|
157
|
+
// toolUse Paused to call a tool. Expected mid-agent-loop.
|
|
158
|
+
// error Provider errored mid-generation.
|
|
159
|
+
// aborted Generation was cancelled (e.g. signal/timeout).
|
|
160
|
+
if (result.stopReason) {
|
|
161
|
+
span.setAttribute('gen_ai.response.finish_reasons', [result.stopReason]);
|
|
162
|
+
}
|
|
151
163
|
if (!result.ok) {
|
|
152
164
|
// Failure returns a result rather than throwing — mark the span
|
|
153
165
|
// ERROR explicitly so it survives to phase.end (withPhase only
|
|
@@ -280,7 +292,7 @@ async function main() {
|
|
|
280
292
|
}
|
|
281
293
|
// Build prompt and invoke the chat model
|
|
282
294
|
const promptTemplate = loadPromptTemplateFile({
|
|
283
|
-
fileName: CHAT_PROMPT_FILE,
|
|
295
|
+
fileName: resolvePromptFile(import.meta.url, CHAT_PROMPT_FILE),
|
|
284
296
|
promptLabel: 'chat',
|
|
285
297
|
});
|
|
286
298
|
const prompt = buildPrompt({
|
|
@@ -360,7 +372,7 @@ async function main() {
|
|
|
360
372
|
await recordTaskMetrics({
|
|
361
373
|
type: 'chat',
|
|
362
374
|
repository: `${ctx.owner}/${ctx.repo}`,
|
|
363
|
-
model: '
|
|
375
|
+
model: 'openai',
|
|
364
376
|
outcome: metricOutcome,
|
|
365
377
|
durationMs: performance.now() - startedAt,
|
|
366
378
|
});
|
|
@@ -1,7 +1,19 @@
|
|
|
1
|
+
import { SpanStatusCode } from '@opentelemetry/api';
|
|
1
2
|
import { logger } from '../../core/logging.js';
|
|
2
|
-
import { buildPiImageContent, createPiInvoker } from '../../core/pi.js';
|
|
3
|
+
import { buildPiImageContent, createPiInvoker, DEFAULT_OPENROUTER_MODELS } from '../../core/pi.js';
|
|
4
|
+
import { withPhase } from '../../core/tracing.js';
|
|
3
5
|
const ISSUE_SUMMARIZE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
4
6
|
const ISSUE_SUMMARIZE_MULTIMODAL_MODEL = 'google/gemini-2.5-pro';
|
|
7
|
+
const SUMMARIZE_TEXT_PROVIDER = 'zai';
|
|
8
|
+
// Single source of truth: inherit the repo-wide ZAI default so a default-model
|
|
9
|
+
// bump in core/pi.ts moves summarize too, and the executed model matches the
|
|
10
|
+
// recorded gen_ai.request.model.
|
|
11
|
+
const SUMMARIZE_TEXT_MODEL = DEFAULT_OPENROUTER_MODELS.zai;
|
|
12
|
+
const SUMMARIZE_MULTIMODAL_PROVIDER = 'gcp.gemini';
|
|
13
|
+
// One model span per actual model.invoke. The final summary uses
|
|
14
|
+
// `issue-summarize.model`; each chunk summary uses `issue-summarize.chunk`.
|
|
15
|
+
const SUMMARIZE_MODEL_PHASE = 'issue-summarize.model';
|
|
16
|
+
const SUMMARIZE_CHUNK_PHASE = 'issue-summarize.chunk';
|
|
5
17
|
export function createIssueSummarizeModel(apiKey) {
|
|
6
18
|
return createPiInvoker({
|
|
7
19
|
provider: 'zai',
|
|
@@ -11,28 +23,40 @@ export function createIssueSummarizeModel(apiKey) {
|
|
|
11
23
|
capabilityPreset: 'read-only',
|
|
12
24
|
});
|
|
13
25
|
}
|
|
14
|
-
async function invokeIssueSummarizeTextModel({ model, prompt, }) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
async function invokeIssueSummarizeTextModel({ model, prompt, phaseName, }) {
|
|
27
|
+
return withPhase(phaseName, {
|
|
28
|
+
'gen_ai.operation.name': 'chat',
|
|
29
|
+
'gen_ai.provider.name': SUMMARIZE_TEXT_PROVIDER,
|
|
30
|
+
'gen_ai.request.model': SUMMARIZE_TEXT_MODEL,
|
|
31
|
+
}, async (span) => {
|
|
32
|
+
const result = await model.invoke({
|
|
33
|
+
prompt,
|
|
34
|
+
modelName: SUMMARIZE_TEXT_MODEL,
|
|
35
|
+
timeoutMs: ISSUE_SUMMARIZE_TIMEOUT_MS,
|
|
23
36
|
});
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
if (!result.ok) {
|
|
38
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: result.error });
|
|
39
|
+
logger.warn('Issue summarize model request failed', {
|
|
40
|
+
error: result.error,
|
|
41
|
+
timedOut: result.timedOut,
|
|
42
|
+
});
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const output = result.output.trim() || null;
|
|
46
|
+
if (!output) {
|
|
47
|
+
span.setStatus({
|
|
48
|
+
code: SpanStatusCode.ERROR,
|
|
49
|
+
message: 'summarize model produced no output',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return output;
|
|
35
53
|
});
|
|
54
|
+
}
|
|
55
|
+
// One model span for the multimodal model.invoke (provider/model known at
|
|
56
|
+
// start). On failure it returns null and the caller falls back to a separate
|
|
57
|
+
// text request — which emits its own model span, so a fallback shows two real
|
|
58
|
+
// model calls rather than one.
|
|
59
|
+
async function invokeMultimodalSummarizeModel({ model, prompt, mediaImages, }) {
|
|
36
60
|
const mediaContext = mediaImages
|
|
37
61
|
.map((mediaImage, index) => [
|
|
38
62
|
`Media item ${index + 1} of ${mediaImages.length}`,
|
|
@@ -43,40 +67,72 @@ export async function invokeIssueSummarizeModel({ model, prompt, mediaImages, })
|
|
|
43
67
|
mediaImage.sourceContext,
|
|
44
68
|
].join('\n'))
|
|
45
69
|
.join('\n\n');
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
mediaImages: mediaImages.length,
|
|
70
|
+
return withPhase(SUMMARIZE_MODEL_PHASE, {
|
|
71
|
+
'gen_ai.operation.name': 'chat',
|
|
72
|
+
'gen_ai.provider.name': SUMMARIZE_MULTIMODAL_PROVIDER,
|
|
73
|
+
'gen_ai.request.model': ISSUE_SUMMARIZE_MULTIMODAL_MODEL,
|
|
74
|
+
}, async (span) => {
|
|
75
|
+
try {
|
|
76
|
+
const result = await model.invoke({
|
|
77
|
+
modelName: ISSUE_SUMMARIZE_MULTIMODAL_MODEL,
|
|
78
|
+
prompt: `${prompt}\n\n<media_context>\n${mediaContext}\n</media_context>`,
|
|
79
|
+
timeoutMs: ISSUE_SUMMARIZE_TIMEOUT_MS,
|
|
80
|
+
images: mediaImages.map((mediaImage) => buildPiImageContent({
|
|
81
|
+
dataBase64: mediaImage.dataBase64,
|
|
82
|
+
contentType: mediaImage.contentType,
|
|
83
|
+
})),
|
|
61
84
|
});
|
|
62
|
-
|
|
85
|
+
if (!result.ok) {
|
|
86
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: result.error });
|
|
87
|
+
logger.warn('Issue summarize multimodal request failed', {
|
|
88
|
+
error: result.error,
|
|
89
|
+
timedOut: result.timedOut,
|
|
90
|
+
mediaImages: mediaImages.length,
|
|
91
|
+
});
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const text = result.output.trim();
|
|
95
|
+
if (!text) {
|
|
96
|
+
span.setStatus({
|
|
97
|
+
code: SpanStatusCode.ERROR,
|
|
98
|
+
message: 'multimodal summarize produced no output',
|
|
99
|
+
});
|
|
100
|
+
logger.warn('Issue summarize multimodal request returned empty output', {
|
|
101
|
+
mediaImages: mediaImages.length,
|
|
102
|
+
});
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
return text;
|
|
63
106
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
107
|
+
catch (error) {
|
|
108
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
109
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message });
|
|
110
|
+
logger.warn('Issue summarize multimodal request errored', {
|
|
111
|
+
error: message,
|
|
67
112
|
mediaImages: mediaImages.length,
|
|
68
113
|
});
|
|
69
|
-
return
|
|
114
|
+
return null;
|
|
70
115
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
export async function invokeIssueSummarizeModel({ model, prompt, mediaImages, }) {
|
|
119
|
+
if (mediaImages.length === 0) {
|
|
120
|
+
logger.info('Using text-only summarize model request');
|
|
121
|
+
return await invokeIssueSummarizeTextModel({
|
|
122
|
+
model,
|
|
123
|
+
prompt,
|
|
124
|
+
phaseName: SUMMARIZE_MODEL_PHASE,
|
|
77
125
|
});
|
|
78
|
-
return await invokeIssueSummarizeTextModel({ model, prompt });
|
|
79
126
|
}
|
|
127
|
+
logger.info('Using multimodal summarize model request', {
|
|
128
|
+
mediaImages: mediaImages.length,
|
|
129
|
+
});
|
|
130
|
+
const multimodal = await invokeMultimodalSummarizeModel({ model, prompt, mediaImages });
|
|
131
|
+
if (multimodal) {
|
|
132
|
+
return multimodal;
|
|
133
|
+
}
|
|
134
|
+
// Multimodal failed — fall back to a separate text request (own model span).
|
|
135
|
+
return await invokeIssueSummarizeTextModel({ model, prompt, phaseName: SUMMARIZE_MODEL_PHASE });
|
|
80
136
|
}
|
|
81
137
|
function buildChunkSummaryPrompt({ chunk, chunkIndex, selectedChunkCount, totalChunks, }) {
|
|
82
138
|
return `You are preparing an intermediate summary for a GitHub issue discussion.
|
|
@@ -97,6 +153,14 @@ ${chunk}
|
|
|
97
153
|
</chunk_content>`;
|
|
98
154
|
}
|
|
99
155
|
export async function summarizeChunks({ model, chunks, totalChunks, }) {
|
|
156
|
+
// Workflow phase: it orchestrates one model request PER chunk, so it is not
|
|
157
|
+
// itself a single gen-AI operation and carries no gen_ai.* attrs.
|
|
158
|
+
return withPhase('issue-summarize.chunks', {
|
|
159
|
+
'chunks.selected': chunks.length,
|
|
160
|
+
'chunks.total': totalChunks,
|
|
161
|
+
}, async (span) => summarizeChunksInner({ model, chunks, totalChunks }, span));
|
|
162
|
+
}
|
|
163
|
+
async function summarizeChunksInner({ model, chunks, totalChunks, }, span) {
|
|
100
164
|
const summaries = [];
|
|
101
165
|
let missingChunkSummaries = 0;
|
|
102
166
|
logger.info('Summarizing issue thread in chunks', {
|
|
@@ -113,6 +177,7 @@ export async function summarizeChunks({ model, chunks, totalChunks, }) {
|
|
|
113
177
|
const chunkSummary = await invokeIssueSummarizeTextModel({
|
|
114
178
|
model,
|
|
115
179
|
prompt: chunkPrompt,
|
|
180
|
+
phaseName: SUMMARIZE_CHUNK_PHASE,
|
|
116
181
|
});
|
|
117
182
|
if (chunkSummary?.trim()) {
|
|
118
183
|
summaries.push(`### Chunk ${index + 1}\n${chunkSummary.trim()}`);
|
|
@@ -121,7 +186,13 @@ export async function summarizeChunks({ model, chunks, totalChunks, }) {
|
|
|
121
186
|
missingChunkSummaries += 1;
|
|
122
187
|
}
|
|
123
188
|
}
|
|
189
|
+
span.setAttributes({ 'chunks.summarized': summaries.length });
|
|
124
190
|
if (summaries.length === 0) {
|
|
191
|
+
// No chunk produced a model summary — we fall back to raw chunk content.
|
|
192
|
+
span.setStatus({
|
|
193
|
+
code: SpanStatusCode.ERROR,
|
|
194
|
+
message: 'chunk summarization produced no model output',
|
|
195
|
+
});
|
|
125
196
|
logger.warn('Chunk summarization returned no model output, using raw chunk content', {
|
|
126
197
|
selectedChunks: chunks.length,
|
|
127
198
|
totalChunks,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { fillPromptTemplate, loadPromptTemplateFile } from '../../core/prompt-template.js';
|
|
1
|
+
import { fillPromptTemplate, loadPromptTemplateFile, resolvePromptFile, } from '../../core/prompt-template.js';
|
|
2
2
|
const PROMPT_FILE = 'issue-summarize-prompt.md';
|
|
3
3
|
export function loadPromptTemplate() {
|
|
4
4
|
return loadPromptTemplateFile({
|
|
5
|
-
fileName: PROMPT_FILE,
|
|
5
|
+
fileName: resolvePromptFile(import.meta.url, PROMPT_FILE),
|
|
6
6
|
promptLabel: 'issue summarize',
|
|
7
7
|
});
|
|
8
8
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { SpanStatusCode } from '@opentelemetry/api';
|
|
1
2
|
import { App } from 'octokit';
|
|
2
3
|
import { fetchIssue, fetchIssueComments } from '../../core/github-issues.js';
|
|
3
4
|
import { logger } from '../../core/logging.js';
|
|
4
5
|
import { removeIssueCommentReactionIfPresent } from '../../core/reactions.js';
|
|
5
6
|
import { resolveWriteOctokit } from '../../core/shared.js';
|
|
7
|
+
import { withPhase } from '../../core/tracing.js';
|
|
6
8
|
import { parseSummarizeContext } from './context.js';
|
|
7
9
|
import { buildMediaPromptImages, extractMediaReferences, formatMediaContext, inspectMediaReferences, } from './media.js';
|
|
8
10
|
import { createIssueSummarizeModel, invokeIssueSummarizeModel, summarizeChunks } from './model.js';
|
|
@@ -109,108 +111,122 @@ async function main() {
|
|
|
109
111
|
repository: `${ctx.owner}/${ctx.repo}`,
|
|
110
112
|
issueNumber: ctx.issueNumber,
|
|
111
113
|
});
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
fetchIssue(readOctokit, issueParams),
|
|
126
|
-
fetchIssueComments(readOctokit, issueParams),
|
|
127
|
-
]);
|
|
128
|
-
logger.info('Loaded issue summarize context', {
|
|
129
|
-
issueState: issue.state ?? 'unknown',
|
|
130
|
-
commentsCount: comments.length,
|
|
131
|
-
});
|
|
132
|
-
const entries = buildThreadEntries({
|
|
133
|
-
issue,
|
|
134
|
-
comments,
|
|
135
|
-
triggerCommentId: ctx.commentId,
|
|
136
|
-
});
|
|
137
|
-
logger.info('Prepared issue summarize thread entries', {
|
|
138
|
-
issueNumber: issue.number,
|
|
139
|
-
entryCount: entries.length,
|
|
140
|
-
});
|
|
141
|
-
const mediaReferences = extractMediaReferences(entries);
|
|
142
|
-
const mediaInspections = await inspectMediaReferences(mediaReferences);
|
|
143
|
-
const mediaInspectionStats = summarizeMediaInspectionStats(mediaInspections);
|
|
144
|
-
logger.info('Inspected issue summarize media references', {
|
|
145
|
-
mediaReferences: mediaReferences.length,
|
|
146
|
-
mediaInspections: mediaInspections.length,
|
|
147
|
-
mediaFetched: mediaInspectionStats.fetched,
|
|
148
|
-
mediaFetchFailed: mediaInspectionStats.fetchFailed,
|
|
149
|
-
mediaNotImage: mediaInspectionStats.notMedia,
|
|
150
|
-
});
|
|
151
|
-
const summary = await summarizeIssue({
|
|
152
|
-
ctx,
|
|
153
|
-
issue,
|
|
154
|
-
entries,
|
|
155
|
-
mediaReferences,
|
|
156
|
-
mediaInspections,
|
|
157
|
-
});
|
|
158
|
-
logger.info('Generated issue summary', {
|
|
159
|
-
issueNumber: ctx.issueNumber,
|
|
160
|
-
summaryChars: summary.length,
|
|
161
|
-
});
|
|
162
|
-
const summaryComment = `${SUMMARY_MARKER}\n\n${summary}`;
|
|
163
|
-
if (ctx.dryRun) {
|
|
164
|
-
logger.info('Dry run enabled, printing summarize output instead of posting comment', {
|
|
165
|
-
issueNumber: ctx.issueNumber,
|
|
166
|
-
});
|
|
167
|
-
process.stdout.write(`${summaryComment}\n`);
|
|
168
|
-
}
|
|
169
|
-
else {
|
|
170
|
-
await writeOctokit.rest.issues.createComment({
|
|
114
|
+
let exitCode = 0;
|
|
115
|
+
await withPhase('issue-summarize', {
|
|
116
|
+
task: 'issue-summarize',
|
|
117
|
+
'gen_ai.operation.name': 'invoke_agent',
|
|
118
|
+
'gen_ai.agent.name': 'doistbot.issue-summarize',
|
|
119
|
+
'issue.number': ctx.issueNumber,
|
|
120
|
+
'comment.id': ctx.commentId,
|
|
121
|
+
dry_run: ctx.dryRun,
|
|
122
|
+
}, async (span) => {
|
|
123
|
+
// Inside the span so auth/setup failures are captured by the root span.
|
|
124
|
+
const { readOctokit, writeOctokit } = await createGitHubClients(ctx);
|
|
125
|
+
try {
|
|
126
|
+
const issueParams = {
|
|
171
127
|
owner: ctx.owner,
|
|
172
128
|
repo: ctx.repo,
|
|
173
|
-
issue_number: ctx.issueNumber,
|
|
174
|
-
body: summaryComment,
|
|
175
|
-
});
|
|
176
|
-
logger.info('Posted issue summary comment', {
|
|
177
129
|
issueNumber: ctx.issueNumber,
|
|
130
|
+
mediaType: { format: 'full' },
|
|
131
|
+
};
|
|
132
|
+
const [issue, comments] = await Promise.all([
|
|
133
|
+
fetchIssue(readOctokit, issueParams),
|
|
134
|
+
fetchIssueComments(readOctokit, issueParams),
|
|
135
|
+
]);
|
|
136
|
+
logger.info('Loaded issue summarize context', {
|
|
137
|
+
issueState: issue.state ?? 'unknown',
|
|
138
|
+
commentsCount: comments.length,
|
|
139
|
+
});
|
|
140
|
+
const entries = buildThreadEntries({
|
|
141
|
+
issue,
|
|
142
|
+
comments,
|
|
143
|
+
triggerCommentId: ctx.commentId,
|
|
144
|
+
});
|
|
145
|
+
logger.info('Prepared issue summarize thread entries', {
|
|
146
|
+
issueNumber: issue.number,
|
|
147
|
+
entryCount: entries.length,
|
|
148
|
+
});
|
|
149
|
+
const mediaReferences = extractMediaReferences(entries);
|
|
150
|
+
const mediaInspections = await inspectMediaReferences(mediaReferences);
|
|
151
|
+
const mediaInspectionStats = summarizeMediaInspectionStats(mediaInspections);
|
|
152
|
+
logger.info('Inspected issue summarize media references', {
|
|
178
153
|
mediaReferences: mediaReferences.length,
|
|
154
|
+
mediaInspections: mediaInspections.length,
|
|
155
|
+
mediaFetched: mediaInspectionStats.fetched,
|
|
156
|
+
mediaFetchFailed: mediaInspectionStats.fetchFailed,
|
|
157
|
+
mediaNotImage: mediaInspectionStats.notMedia,
|
|
179
158
|
});
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
159
|
+
const summary = await summarizeIssue({
|
|
160
|
+
ctx,
|
|
161
|
+
issue,
|
|
162
|
+
entries,
|
|
163
|
+
mediaReferences,
|
|
164
|
+
mediaInspections,
|
|
165
|
+
});
|
|
166
|
+
logger.info('Generated issue summary', {
|
|
167
|
+
issueNumber: ctx.issueNumber,
|
|
168
|
+
summaryChars: summary.length,
|
|
169
|
+
});
|
|
170
|
+
const summaryComment = `${SUMMARY_MARKER}\n\n${summary}`;
|
|
171
|
+
if (ctx.dryRun) {
|
|
172
|
+
logger.info('Dry run enabled, printing summarize output instead of posting comment', { issueNumber: ctx.issueNumber });
|
|
173
|
+
process.stdout.write(`${summaryComment}\n`);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
189
176
|
await writeOctokit.rest.issues.createComment({
|
|
190
177
|
owner: ctx.owner,
|
|
191
178
|
repo: ctx.repo,
|
|
192
179
|
issue_number: ctx.issueNumber,
|
|
193
|
-
body:
|
|
180
|
+
body: summaryComment,
|
|
194
181
|
});
|
|
195
|
-
|
|
196
|
-
catch (replyError) {
|
|
197
|
-
logger.warn('Failed to post summarize error response', {
|
|
182
|
+
logger.info('Posted issue summary comment', {
|
|
198
183
|
issueNumber: ctx.issueNumber,
|
|
199
|
-
|
|
184
|
+
mediaReferences: mediaReferences.length,
|
|
200
185
|
});
|
|
201
186
|
}
|
|
202
187
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
188
|
+
catch (error) {
|
|
189
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
190
|
+
logger.error('Issue summarize task failed', {
|
|
191
|
+
issueNumber: ctx.issueNumber,
|
|
192
|
+
error: message,
|
|
193
|
+
});
|
|
194
|
+
// Set the span status before the run exits; deferring process.exit
|
|
195
|
+
// until after withPhase lets phase.end flush.
|
|
196
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message });
|
|
197
|
+
if (!ctx.dryRun) {
|
|
198
|
+
try {
|
|
199
|
+
await writeOctokit.rest.issues.createComment({
|
|
200
|
+
owner: ctx.owner,
|
|
201
|
+
repo: ctx.repo,
|
|
202
|
+
issue_number: ctx.issueNumber,
|
|
203
|
+
body: 'Sorry, I encountered an error while summarizing this issue. Please try again or contact the team maintaining the bot.',
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
catch (replyError) {
|
|
207
|
+
logger.warn('Failed to post summarize error response', {
|
|
208
|
+
issueNumber: ctx.issueNumber,
|
|
209
|
+
error: replyError instanceof Error
|
|
210
|
+
? replyError.message
|
|
211
|
+
: String(replyError),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
exitCode = 1;
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
await removeIssueCommentReactionIfPresent({
|
|
219
|
+
octokit: writeOctokit,
|
|
220
|
+
owner: ctx.owner,
|
|
221
|
+
repo: ctx.repo,
|
|
222
|
+
commentId: ctx.commentId,
|
|
223
|
+
reactionId: ctx.reactionId,
|
|
224
|
+
warnMessage: 'Failed to remove summarize reaction',
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
if (exitCode !== 0) {
|
|
229
|
+
process.exit(exitCode);
|
|
214
230
|
}
|
|
215
231
|
}
|
|
216
232
|
if (process.env.VITEST !== 'true') {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from 'child_process';
|
|
2
2
|
import { logger } from '../../core/logging.js';
|
|
3
|
-
import { fillPromptTemplate, loadPromptTemplateFile } from '../../core/prompt-template.js';
|
|
3
|
+
import { fillPromptTemplate, loadPromptTemplateFile, resolvePromptFile, } from '../../core/prompt-template.js';
|
|
4
4
|
import { validateDiff } from './diff-validator.js';
|
|
5
5
|
import { CODEX_CANNOT_FIX_REASON, FIX_LOOP_FAILED_REASON_PREFIX } from './fix-failure-reasons.js';
|
|
6
6
|
import { CX_HERO_MANUAL_HANDLE, resolveHeroGroup } from './hero-group-map.js';
|
|
@@ -24,7 +24,7 @@ export function buildFixAssessmentSkipReason(assessment) {
|
|
|
24
24
|
}
|
|
25
25
|
function loadFixPromptTemplate() {
|
|
26
26
|
return loadPromptTemplateFile({
|
|
27
|
-
fileName: FIX_PROMPT_FILE,
|
|
27
|
+
fileName: resolvePromptFile(import.meta.url, FIX_PROMPT_FILE),
|
|
28
28
|
promptLabel: 'issue triage fix',
|
|
29
29
|
});
|
|
30
30
|
}
|
|
@@ -383,6 +383,7 @@ export async function runFixCore(input) {
|
|
|
383
383
|
prompt,
|
|
384
384
|
apiKey: ctx.openaiApiKey,
|
|
385
385
|
cwd: targetRepo.localPath,
|
|
386
|
+
thinkingLevel: 'xhigh',
|
|
386
387
|
timeoutMs,
|
|
387
388
|
extraEnv: {
|
|
388
389
|
GH_TOKEN: input.ghToken,
|
|
@@ -42,8 +42,10 @@ const TEAM_LABEL_TO_HERO = {
|
|
|
42
42
|
const SURFACE_TO_HERO = {
|
|
43
43
|
android: 'android-hero',
|
|
44
44
|
backend: 'backend-hero',
|
|
45
|
+
cli: 'frontend-hero',
|
|
45
46
|
desktop: 'desktop-hero',
|
|
46
47
|
ios: 'apple-hero',
|
|
48
|
+
mcp: 'frontend-hero',
|
|
47
49
|
web: 'frontend-hero',
|
|
48
50
|
};
|
|
49
51
|
export const CX_HERO_MANUAL_HANDLE = HERO_GROUPS['cx-hero'].mention;
|
|
@@ -3,7 +3,7 @@ import fs from 'fs';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { DOISTBOT_AUTO_FIX_LABEL, DOISTBOT_SPECULATIVE_FIX_LABEL } from 'doistbot-shared-contracts';
|
|
5
5
|
import { logger } from '../../core/logging.js';
|
|
6
|
-
import { fillPromptTemplate, loadPromptTemplateFile } from '../../core/prompt-template.js';
|
|
6
|
+
import { fillPromptTemplate, loadPromptTemplateFile, resolvePromptFile, } from '../../core/prompt-template.js';
|
|
7
7
|
const PR_TEMPLATE_PATHS = [
|
|
8
8
|
'.github/pull_request_template.md',
|
|
9
9
|
'.github/PULL_REQUEST_TEMPLATE.md',
|
|
@@ -196,7 +196,7 @@ function getConfidenceQualifier(classification, confidence) {
|
|
|
196
196
|
const PR_BODY_TEMPLATE_FILE = 'auto-fix-pr-body-template.md';
|
|
197
197
|
function buildPRBody({ issueNumber, issueUrl, triageAnalysis, fixDescription, fixAssessment, }) {
|
|
198
198
|
const template = loadPromptTemplateFile({
|
|
199
|
-
fileName: PR_BODY_TEMPLATE_FILE,
|
|
199
|
+
fileName: resolvePromptFile(import.meta.url, PR_BODY_TEMPLATE_FILE),
|
|
200
200
|
promptLabel: 'auto-fix PR body',
|
|
201
201
|
});
|
|
202
202
|
let confidenceLine = '';
|