@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.
Files changed (36) hide show
  1. package/dist/actions/auth.js +43 -11
  2. package/dist/actions/doctor.js +4 -2
  3. package/dist/actions/review.js +11 -14
  4. package/dist/auth.js +23 -6
  5. package/dist/config.js +58 -12
  6. package/package.json +3 -3
  7. package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +1 -1
  8. package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +1 -1
  9. package/sandbox/dist/core/pi.js +31 -1
  10. package/sandbox/dist/core/prompt-template.js +45 -3
  11. package/sandbox/dist/core/repository-map.js +24 -0
  12. package/sandbox/dist/core/tracing.js +9 -0
  13. package/sandbox/dist/providers/helpers.js +51 -0
  14. package/sandbox/dist/providers/pi-review.js +7 -1
  15. package/sandbox/dist/tasks/chat/chat.js +20 -8
  16. package/sandbox/dist/tasks/issue-summarize/model.js +119 -48
  17. package/sandbox/dist/tasks/issue-summarize/prompt.js +2 -2
  18. package/sandbox/dist/tasks/issue-summarize/summarize.js +104 -88
  19. package/sandbox/dist/tasks/issue-triage/fix-attempt.js +3 -2
  20. package/sandbox/dist/tasks/issue-triage/hero-group-map.js +2 -0
  21. package/sandbox/dist/tasks/issue-triage/pr-creator.js +2 -2
  22. package/sandbox/dist/tasks/issue-triage/prompt.js +2 -2
  23. package/sandbox/dist/tasks/issue-triage/triage.js +2 -2
  24. package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +3 -3
  25. package/sandbox/dist/tasks/review/engines/dedupe.js +107 -42
  26. package/sandbox/dist/tasks/review/engines/multi-focus.js +229 -119
  27. package/sandbox/dist/tasks/review/engines/shared.js +36 -1
  28. package/sandbox/dist/tasks/review/github-comment-format.js +12 -4
  29. package/sandbox/dist/tasks/review/local.js +6 -1
  30. package/sandbox/dist/tasks/review/review-summary.js +3 -0
  31. package/sandbox/dist/tasks/review/review.js +253 -186
  32. package/sandbox/dist/tasks/review/runner.js +10 -5
  33. package/sandbox/dist/tasks/review/summary-model.js +40 -18
  34. package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +1 -1
  35. package/sandbox/node_modules/doistbot-repo-config/dist/index.js +1 -1
  36. 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({ provider, modelName, prompt, config, prompts = [prompt], parseOutput, }) {
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 = 'zai';
15
- const CHAT_MODEL = 'z-ai/glm-5.2';
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.OPENROUTER_API_KEY?.trim();
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: 'OPENROUTER_API_KEY missing' };
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: 'zai',
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
- const result = await model.invoke({
16
- prompt,
17
- timeoutMs: ISSUE_SUMMARIZE_TIMEOUT_MS,
18
- });
19
- if (!result.ok) {
20
- logger.warn('Issue summarize model request failed', {
21
- error: result.error,
22
- timedOut: result.timedOut,
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
- return null;
25
- }
26
- return result.output.trim() || null;
27
- }
28
- export async function invokeIssueSummarizeModel({ model, prompt, mediaImages, }) {
29
- if (mediaImages.length === 0) {
30
- logger.info('Using text-only summarize model request');
31
- return await invokeIssueSummarizeTextModel({ model, prompt });
32
- }
33
- logger.info('Using multimodal summarize model request', {
34
- mediaImages: mediaImages.length,
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
- try {
47
- const result = await model.invoke({
48
- modelName: ISSUE_SUMMARIZE_MULTIMODAL_MODEL,
49
- prompt: `${prompt}\n\n<media_context>\n${mediaContext}\n</media_context>`,
50
- timeoutMs: ISSUE_SUMMARIZE_TIMEOUT_MS,
51
- images: mediaImages.map((mediaImage) => buildPiImageContent({
52
- dataBase64: mediaImage.dataBase64,
53
- contentType: mediaImage.contentType,
54
- })),
55
- });
56
- if (!result.ok) {
57
- logger.warn('Issue summarize multimodal request failed', {
58
- error: result.error,
59
- timedOut: result.timedOut,
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
- return await invokeIssueSummarizeTextModel({ model, prompt });
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
- const text = result.output.trim();
65
- if (!text) {
66
- logger.warn('Issue summarize multimodal request returned empty output', {
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 await invokeIssueSummarizeTextModel({ model, prompt });
114
+ return null;
70
115
  }
71
- return text;
72
- }
73
- catch (error) {
74
- logger.warn('Issue summarize multimodal request errored', {
75
- error: error instanceof Error ? error.message : String(error),
76
- mediaImages: mediaImages.length,
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
- logger.info('Issue summarize task started', {
113
- dryRun: ctx.dryRun,
114
- issueNumber: ctx.issueNumber,
115
- });
116
- const { readOctokit, writeOctokit } = await createGitHubClients(ctx);
117
- try {
118
- const issueParams = {
119
- owner: ctx.owner,
120
- repo: ctx.repo,
121
- issueNumber: ctx.issueNumber,
122
- mediaType: { format: 'full' },
123
- };
124
- const [issue, comments] = await Promise.all([
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
- catch (error) {
183
- logger.error('Issue summarize task failed', {
184
- issueNumber: ctx.issueNumber,
185
- error: error instanceof Error ? error.message : String(error),
186
- });
187
- if (!ctx.dryRun) {
188
- try {
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: 'Sorry, I encountered an error while summarizing this issue. Please try again or contact the team maintaining the bot.',
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
- error: replyError instanceof Error ? replyError.message : String(replyError),
184
+ mediaReferences: mediaReferences.length,
200
185
  });
201
186
  }
202
187
  }
203
- process.exit(1);
204
- }
205
- finally {
206
- await removeIssueCommentReactionIfPresent({
207
- octokit: writeOctokit,
208
- owner: ctx.owner,
209
- repo: ctx.repo,
210
- commentId: ctx.commentId,
211
- reactionId: ctx.reactionId,
212
- warnMessage: 'Failed to remove summarize reaction',
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 = '';