@doist/doistbot-cli 1.0.5 → 1.0.7

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 (61) hide show
  1. package/dist/actions/auth.js +78 -24
  2. package/dist/actions/doctor.js +14 -7
  3. package/dist/actions/review.js +67 -10
  4. package/dist/auth.js +51 -6
  5. package/dist/config.js +77 -5
  6. package/dist/env.js +1 -0
  7. package/dist/runtime.js +1 -1
  8. package/dist/terminal.js +2 -0
  9. package/package.json +4 -4
  10. package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +4 -8
  11. package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +8 -20
  12. package/sandbox/dist/core/check-run.js +16 -0
  13. package/sandbox/dist/core/datadog-metrics.js +6 -52
  14. package/sandbox/dist/core/logger-span-processor.js +78 -0
  15. package/sandbox/dist/core/pi.js +285 -174
  16. package/sandbox/dist/core/prompt-template.js +45 -3
  17. package/sandbox/dist/core/repository-map.js +24 -0
  18. package/sandbox/dist/core/tracing-setup.js +15 -0
  19. package/sandbox/dist/core/tracing.js +80 -0
  20. package/sandbox/dist/main.js +4 -1
  21. package/sandbox/dist/providers/credentials.js +12 -2
  22. package/sandbox/dist/providers/gemini.js +5 -1
  23. package/sandbox/dist/providers/helpers.js +63 -17
  24. package/sandbox/dist/providers/index.js +4 -0
  25. package/sandbox/dist/providers/openrouter-pi.js +25 -0
  26. package/sandbox/dist/providers/pi-review.js +19 -2
  27. package/sandbox/dist/tasks/chat/chat.js +173 -102
  28. package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +210 -185
  29. package/sandbox/dist/tasks/issue-summarize/model.js +120 -49
  30. package/sandbox/dist/tasks/issue-summarize/prompt.js +2 -2
  31. package/sandbox/dist/tasks/issue-summarize/summarize.js +104 -88
  32. package/sandbox/dist/tasks/issue-triage/clone.js +40 -0
  33. package/sandbox/dist/tasks/issue-triage/context.js +25 -1
  34. package/sandbox/dist/tasks/issue-triage/fix-attempt.js +33 -10
  35. package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +154 -101
  36. package/sandbox/dist/tasks/issue-triage/fix-loop/runners.js +15 -6
  37. package/sandbox/dist/tasks/issue-triage/hero-group-map.js +2 -0
  38. package/sandbox/dist/tasks/issue-triage/model.js +120 -36
  39. package/sandbox/dist/tasks/issue-triage/output.js +27 -0
  40. package/sandbox/dist/tasks/issue-triage/pr-creator.js +5 -4
  41. package/sandbox/dist/tasks/issue-triage/prompt.js +2 -2
  42. package/sandbox/dist/tasks/issue-triage/resolution.js +26 -0
  43. package/sandbox/dist/tasks/issue-triage/routing.js +9 -9
  44. package/sandbox/dist/tasks/issue-triage/side-effects.js +9 -0
  45. package/sandbox/dist/tasks/issue-triage/triage.js +290 -420
  46. package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +3 -3
  47. package/sandbox/dist/tasks/review/check-run.js +2 -1
  48. package/sandbox/dist/tasks/review/conversation-context.js +2 -1
  49. package/sandbox/dist/tasks/review/engines/dedupe.js +110 -44
  50. package/sandbox/dist/tasks/review/engines/multi-focus.js +254 -113
  51. package/sandbox/dist/tasks/review/engines/shared.js +43 -6
  52. package/sandbox/dist/tasks/review/github-comment-format.js +72 -10
  53. package/sandbox/dist/tasks/review/local.js +7 -4
  54. package/sandbox/dist/tasks/review/review-summary.js +48 -7
  55. package/sandbox/dist/tasks/review/review.js +267 -186
  56. package/sandbox/dist/tasks/review/runner.js +33 -7
  57. package/sandbox/dist/tasks/review/summary-model.js +189 -20
  58. package/sandbox/dist/tasks/review/thinking-level.js +4 -0
  59. package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +4 -8
  60. package/sandbox/node_modules/doistbot-repo-config/dist/index.js +8 -20
  61. package/sandbox/src/tasks/review/prompts/review-multi-focus-base-prompt.md +1 -0
@@ -1,14 +1,18 @@
1
1
  import { performance } from 'node:perf_hooks';
2
+ import { SpanStatusCode } from '@opentelemetry/api';
2
3
  import { isKnownDoistbotLogin } from '../../core/bot-logins.js';
3
4
  import { recordTaskMetrics } from '../../core/datadog-metrics.js';
4
5
  import { logger } from '../../core/logging.js';
5
6
  import { invokePiPrompt } from '../../core/pi.js';
6
- import { loadPromptTemplateFile } from '../../core/prompt-template.js';
7
+ import { loadPromptTemplateFile, resolvePromptFile } from '../../core/prompt-template.js';
7
8
  import { removeIssueCommentReactionIfPresent } from '../../core/reactions.js';
8
9
  import { cloneRepository, createGitHubClients, parseBaseContext, requiredEnv, WORKSPACE_DIR, } from '../../core/shared.js';
10
+ import { withPhase } from '../../core/tracing.js';
9
11
  import { buildPrompt } from './prompt.js';
10
12
  const CHAT_PROMPT_FILE = 'chat-prompt.md';
11
13
  const CHAT_TIMEOUT_MS = 5 * 60 * 1000;
14
+ const CHAT_PROVIDER = 'zai';
15
+ const CHAT_MODEL = 'z-ai/glm-5.2';
12
16
  function parseContext() {
13
17
  const base = parseBaseContext();
14
18
  const reactionIdRaw = process.env.REACTION_ID;
@@ -19,6 +23,7 @@ function parseContext() {
19
23
  reactionId: Number.isFinite(reactionId) ? reactionId : undefined,
20
24
  commentType: requiredEnv('COMMENT_TYPE'),
21
25
  eventName: requiredEnv('EVENT_NAME'),
26
+ dryRun: process.env.DRY_RUN?.toLowerCase() === 'true',
22
27
  };
23
28
  }
24
29
  /**
@@ -122,26 +127,57 @@ async function gatherIssueCommentContext(octokit, owner, repo, prNumber, comment
122
127
  export async function invokeChatModel(prompt) {
123
128
  const apiKey = process.env.OPENROUTER_API_KEY?.trim();
124
129
  if (!apiKey) {
130
+ // Config error: no model call attempted, so no model span. The reason is
131
+ // returned so the caller's root span reports the real cause.
125
132
  logger.warn('Chat model API key missing');
126
- return null;
133
+ return { ok: false, reason: 'OPENROUTER_API_KEY missing' };
127
134
  }
128
- const result = await invokePiPrompt({
129
- provider: 'gemini',
130
- apiKey,
131
- piProviderId: 'openrouter',
132
- prompt,
133
- cwd: WORKSPACE_DIR,
134
- timeoutMs: CHAT_TIMEOUT_MS,
135
- capabilityPreset: 'workspace-inspection',
136
- });
137
- if (!result.ok) {
138
- logger.warn('Chat model request failed', {
139
- error: result.error,
140
- timedOut: result.timedOut,
135
+ return withPhase('chat.model', {
136
+ 'gen_ai.operation.name': 'chat',
137
+ 'gen_ai.provider.name': CHAT_PROVIDER,
138
+ 'gen_ai.request.model': CHAT_MODEL,
139
+ }, async (span) => {
140
+ const result = await invokePiPrompt({
141
+ provider: CHAT_PROVIDER,
142
+ apiKey,
143
+ piProviderId: 'openrouter',
144
+ prompt,
145
+ cwd: WORKSPACE_DIR,
146
+ timeoutMs: CHAT_TIMEOUT_MS,
147
+ capabilityPreset: 'workspace-inspection',
148
+ // Keep the executed model in sync with the recorded span attr.
149
+ modelName: CHAT_MODEL,
141
150
  });
142
- return null;
143
- }
144
- return result.output.trim() || null;
151
+ // Record why the model stopped, before the ok/failure branch so
152
+ // terminal "error"/"aborted"/"length" endings are reported too.
153
+ // stopReason values:
154
+ // stop Natural end — model finished or hit a stop sequence.
155
+ // length Hit the token cap (max_tokens or context limit).
156
+ // toolUse Paused to call a tool. Expected mid-agent-loop.
157
+ // error Provider errored mid-generation.
158
+ // aborted Generation was cancelled (e.g. signal/timeout).
159
+ if (result.stopReason) {
160
+ span.setAttribute('gen_ai.response.finish_reasons', [result.stopReason]);
161
+ }
162
+ if (!result.ok) {
163
+ // Failure returns a result rather than throwing — mark the span
164
+ // ERROR explicitly so it survives to phase.end (withPhase only
165
+ // auto-errors on a throw).
166
+ span.setStatus({ code: SpanStatusCode.ERROR, message: result.error });
167
+ logger.warn('Chat model request failed', {
168
+ error: result.error,
169
+ timedOut: result.timedOut,
170
+ });
171
+ return { ok: false, reason: result.error };
172
+ }
173
+ const output = result.output.trim();
174
+ if (!output) {
175
+ const reason = 'chat model produced no output';
176
+ span.setStatus({ code: SpanStatusCode.ERROR, message: reason });
177
+ return { ok: false, reason };
178
+ }
179
+ return { ok: true, output };
180
+ });
145
181
  }
146
182
  /**
147
183
  * Posts the response to the correct location.
@@ -170,6 +206,17 @@ async function postResponse(octokit, ctx, response) {
170
206
  prNumber: ctx.prNumber,
171
207
  });
172
208
  }
209
+ function formatChatDryRunOutput(ctx, response) {
210
+ return JSON.stringify({
211
+ task: 'chat',
212
+ repository: `${ctx.owner}/${ctx.repo}`,
213
+ prNumber: ctx.prNumber,
214
+ commentId: ctx.commentId,
215
+ commentType: ctx.commentType,
216
+ eventName: ctx.eventName,
217
+ response,
218
+ }, null, 2);
219
+ }
173
220
  /**
174
221
  * Removes the 👀 reaction from the triggering comment.
175
222
  */
@@ -210,102 +257,126 @@ async function main() {
210
257
  commentId: ctx.commentId,
211
258
  };
212
259
  logger.setBaseContext(logContext);
213
- logger.info('Chat task started', {
214
- commentType: ctx.commentType,
215
- eventName: ctx.eventName,
216
- });
217
- const { app, readOctokit, writeOctokit } = await createGitHubClients(ctx);
218
260
  const reactionId = ctx.reactionId;
219
261
  let exitCode = 0;
220
262
  let metricOutcome = 'failure';
221
- try {
222
- // Clone repository
223
- await cloneRepository({
224
- app,
225
- owner: ctx.owner,
226
- repo: ctx.repo,
227
- prNumber: ctx.prNumber,
228
- installationId: ctx.installationId,
229
- });
230
- // Gather thread context from GitHub API
231
- let threadContext;
232
- if (ctx.commentType === 'review') {
233
- threadContext = await gatherReviewCommentContext(readOctokit, ctx.owner, ctx.repo, ctx.commentId);
234
- }
235
- else {
236
- threadContext = await gatherIssueCommentContext(readOctokit, ctx.owner, ctx.repo, ctx.prNumber, ctx.commentId);
237
- }
238
- // Build prompt and invoke the chat model
239
- const promptTemplate = loadPromptTemplateFile({
240
- fileName: CHAT_PROMPT_FILE,
241
- promptLabel: 'chat',
242
- });
243
- const prompt = buildPrompt({
244
- promptTemplate,
245
- owner: ctx.owner,
246
- repo: ctx.repo,
247
- prNumber: ctx.prNumber,
248
- threadHistory: threadContext.threadHistory,
249
- userQuestion: threadContext.userQuestion,
250
- codeSnippet: threadContext.codeSnippet,
251
- prTitle: threadContext.prTitle,
252
- prAuthor: threadContext.prAuthor,
253
- });
254
- const response = await invokeChatModel(prompt);
255
- if (!response) {
256
- logger.warn('Chat model request returned no output');
257
- return;
258
- }
259
- // Post response
260
- await postResponse(writeOctokit, ctx, response);
261
- logger.info('Chat task completed');
262
- metricOutcome = 'success';
263
- }
264
- catch (error) {
265
- logger.error('Chat task failed', {
266
- error: error instanceof Error ? error.message : String(error),
267
- });
268
- // Post error message to the PR
263
+ await withPhase('chat', {
264
+ task: 'chat',
265
+ 'gen_ai.operation.name': 'invoke_agent',
266
+ 'gen_ai.agent.name': 'doistbot.chat',
267
+ 'pr.number': ctx.prNumber,
268
+ 'comment.id': ctx.commentId,
269
+ 'comment.type': ctx.commentType,
270
+ 'event.name': ctx.eventName,
271
+ dry_run: ctx.dryRun,
272
+ }, async (span) => {
273
+ // Inside the span so auth/setup failures are captured by the root span.
274
+ const { app, readOctokit, writeOctokit } = await createGitHubClients(ctx);
269
275
  try {
270
- const errorMessage = 'Sorry, I encountered an error while processing your request. Please try again or contact the team maintaining the bot.';
276
+ // Clone repository
277
+ await cloneRepository({
278
+ app,
279
+ owner: ctx.owner,
280
+ repo: ctx.repo,
281
+ prNumber: ctx.prNumber,
282
+ installationId: ctx.installationId,
283
+ });
284
+ // Gather thread context from GitHub API
285
+ let threadContext;
271
286
  if (ctx.commentType === 'review') {
272
- await writeOctokit.rest.pulls.createReplyForReviewComment({
273
- owner: ctx.owner,
274
- repo: ctx.repo,
275
- pull_number: ctx.prNumber,
276
- comment_id: ctx.commentId,
277
- body: errorMessage,
278
- });
287
+ threadContext = await gatherReviewCommentContext(readOctokit, ctx.owner, ctx.repo, ctx.commentId);
279
288
  }
280
289
  else {
281
- await writeOctokit.rest.issues.createComment({
282
- owner: ctx.owner,
283
- repo: ctx.repo,
284
- issue_number: ctx.prNumber,
285
- body: errorMessage,
290
+ threadContext = await gatherIssueCommentContext(readOctokit, ctx.owner, ctx.repo, ctx.prNumber, ctx.commentId);
291
+ }
292
+ // Build prompt and invoke the chat model
293
+ const promptTemplate = loadPromptTemplateFile({
294
+ fileName: resolvePromptFile(import.meta.url, CHAT_PROMPT_FILE),
295
+ promptLabel: 'chat',
296
+ });
297
+ const prompt = buildPrompt({
298
+ promptTemplate,
299
+ owner: ctx.owner,
300
+ repo: ctx.repo,
301
+ prNumber: ctx.prNumber,
302
+ threadHistory: threadContext.threadHistory,
303
+ userQuestion: threadContext.userQuestion,
304
+ codeSnippet: threadContext.codeSnippet,
305
+ prTitle: threadContext.prTitle,
306
+ prAuthor: threadContext.prAuthor,
307
+ });
308
+ const modelResult = await invokeChatModel(prompt);
309
+ if (!modelResult.ok) {
310
+ span.setStatus({ code: SpanStatusCode.ERROR, message: modelResult.reason });
311
+ logger.warn('Chat model produced no usable response', {
312
+ reason: modelResult.reason,
286
313
  });
314
+ return;
287
315
  }
316
+ const response = modelResult.output;
317
+ if (ctx.dryRun) {
318
+ logger.info('Dry run enabled, printing chat response instead of posting to GitHub');
319
+ process.stdout.write(`${formatChatDryRunOutput(ctx, response)}\n`);
320
+ }
321
+ else {
322
+ await postResponse(writeOctokit, ctx, response);
323
+ }
324
+ metricOutcome = 'success';
288
325
  }
289
- catch (replyError) {
290
- logger.error('Failed to post error message', {
291
- error: replyError instanceof Error ? replyError.message : String(replyError),
292
- });
326
+ catch (error) {
327
+ const message = error instanceof Error ? error.message : String(error);
328
+ logger.error('Chat task failed', { error: message });
329
+ // The catch swallows the error to post a user reply, so set the
330
+ // span status explicitly — withPhase only auto-errors on rethrow.
331
+ span.setStatus({ code: SpanStatusCode.ERROR, message });
332
+ if (ctx.dryRun) {
333
+ logger.info('Dry run enabled, skipping chat error response post');
334
+ }
335
+ else {
336
+ try {
337
+ const errorMessage = 'Sorry, I encountered an error while processing your request. Please try again or contact the team maintaining the bot.';
338
+ if (ctx.commentType === 'review') {
339
+ await writeOctokit.rest.pulls.createReplyForReviewComment({
340
+ owner: ctx.owner,
341
+ repo: ctx.repo,
342
+ pull_number: ctx.prNumber,
343
+ comment_id: ctx.commentId,
344
+ body: errorMessage,
345
+ });
346
+ }
347
+ else {
348
+ await writeOctokit.rest.issues.createComment({
349
+ owner: ctx.owner,
350
+ repo: ctx.repo,
351
+ issue_number: ctx.prNumber,
352
+ body: errorMessage,
353
+ });
354
+ }
355
+ }
356
+ catch (replyError) {
357
+ logger.error('Failed to post error message', {
358
+ error: replyError instanceof Error
359
+ ? replyError.message
360
+ : String(replyError),
361
+ });
362
+ }
363
+ }
364
+ exitCode = 1;
293
365
  }
294
- exitCode = 1;
295
- }
296
- finally {
297
- // Always remove reaction
298
- if (reactionId) {
299
- await removeReaction(writeOctokit, ctx, reactionId);
366
+ finally {
367
+ // Local dry runs must stay side-effect free.
368
+ if (reactionId && !ctx.dryRun) {
369
+ await removeReaction(writeOctokit, ctx, reactionId);
370
+ }
371
+ await recordTaskMetrics({
372
+ type: 'chat',
373
+ repository: `${ctx.owner}/${ctx.repo}`,
374
+ model: 'zai',
375
+ outcome: metricOutcome,
376
+ durationMs: performance.now() - startedAt,
377
+ });
300
378
  }
301
- await recordTaskMetrics({
302
- type: 'chat',
303
- repository: `${ctx.owner}/${ctx.repo}`,
304
- model: 'google',
305
- outcome: metricOutcome,
306
- durationMs: performance.now() - startedAt,
307
- });
308
- }
379
+ });
309
380
  if (exitCode !== 0) {
310
381
  process.exit(exitCode);
311
382
  }