@doist/doistbot-cli 1.0.5 → 1.0.6

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 (53) hide show
  1. package/dist/actions/auth.js +55 -21
  2. package/dist/actions/doctor.js +13 -6
  3. package/dist/actions/review.js +75 -10
  4. package/dist/auth.js +51 -6
  5. package/dist/config.js +34 -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 +257 -176
  16. package/sandbox/dist/core/tracing-setup.js +15 -0
  17. package/sandbox/dist/core/tracing.js +71 -0
  18. package/sandbox/dist/main.js +4 -1
  19. package/sandbox/dist/providers/credentials.js +12 -2
  20. package/sandbox/dist/providers/gemini.js +5 -1
  21. package/sandbox/dist/providers/helpers.js +12 -17
  22. package/sandbox/dist/providers/index.js +4 -0
  23. package/sandbox/dist/providers/openrouter-pi.js +25 -0
  24. package/sandbox/dist/providers/pi-review.js +12 -1
  25. package/sandbox/dist/tasks/chat/chat.js +161 -101
  26. package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +210 -185
  27. package/sandbox/dist/tasks/issue-summarize/model.js +1 -1
  28. package/sandbox/dist/tasks/issue-triage/clone.js +40 -0
  29. package/sandbox/dist/tasks/issue-triage/context.js +25 -1
  30. package/sandbox/dist/tasks/issue-triage/fix-attempt.js +31 -8
  31. package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +154 -101
  32. package/sandbox/dist/tasks/issue-triage/fix-loop/runners.js +15 -6
  33. package/sandbox/dist/tasks/issue-triage/model.js +120 -36
  34. package/sandbox/dist/tasks/issue-triage/output.js +27 -0
  35. package/sandbox/dist/tasks/issue-triage/pr-creator.js +3 -2
  36. package/sandbox/dist/tasks/issue-triage/resolution.js +26 -0
  37. package/sandbox/dist/tasks/issue-triage/routing.js +9 -9
  38. package/sandbox/dist/tasks/issue-triage/side-effects.js +9 -0
  39. package/sandbox/dist/tasks/issue-triage/triage.js +288 -418
  40. package/sandbox/dist/tasks/review/check-run.js +2 -1
  41. package/sandbox/dist/tasks/review/conversation-context.js +2 -1
  42. package/sandbox/dist/tasks/review/engines/dedupe.js +5 -4
  43. package/sandbox/dist/tasks/review/engines/multi-focus.js +40 -9
  44. package/sandbox/dist/tasks/review/engines/shared.js +8 -6
  45. package/sandbox/dist/tasks/review/github-comment-format.js +64 -10
  46. package/sandbox/dist/tasks/review/local.js +2 -4
  47. package/sandbox/dist/tasks/review/review-summary.js +45 -7
  48. package/sandbox/dist/tasks/review/review.js +38 -24
  49. package/sandbox/dist/tasks/review/runner.js +23 -2
  50. package/sandbox/dist/tasks/review/summary-model.js +164 -17
  51. package/sandbox/dist/tasks/review/thinking-level.js +4 -0
  52. package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +4 -8
  53. package/sandbox/node_modules/doistbot-repo-config/dist/index.js +8 -20
@@ -1,8 +1,14 @@
1
1
  import { logger } from '../../core/logging.js';
2
- import { invokePiPrompt } from '../../core/pi.js';
2
+ import { DEFAULT_OPENROUTER_MODELS, DEFAULT_PI_MODELS, invokePiPrompt, PI_PROVIDER_BY_DOISTBOT_PROVIDER, } from '../../core/pi.js';
3
3
  import { WORKSPACE_DIR } from '../../core/shared.js';
4
4
  import { resolveReviewProviderCredential } from '../../providers/credentials.js';
5
+ import { extractJsonCandidatesFromResponse } from '../../providers/helpers.js';
5
6
  const REVIEW_SUMMARY_TIMEOUT_MS = 5 * 60 * 1000;
7
+ const REVIEW_SUMMARY_THINKING_LEVEL = 'medium';
8
+ const GEMINI_FLASH_SUMMARY_MODEL_BY_PI_PROVIDER = {
9
+ google: 'gemini-3-flash-preview',
10
+ openrouter: 'google/gemini-3-flash-preview',
11
+ };
6
12
  const DIFF_SIDE = {
7
13
  LEFT: 'LEFT',
8
14
  RIGHT: 'RIGHT',
@@ -10,53 +16,194 @@ const DIFF_SIDE = {
10
16
  function getWorkspaceDir() {
11
17
  return WORKSPACE_DIR;
12
18
  }
13
- export async function requestReviewSummary({ credential, prompt, workspaceDir, }) {
19
+ function getReviewSummaryModelNameForLog({ provider, piProviderId, modelName, }) {
20
+ if (modelName) {
21
+ return modelName;
22
+ }
23
+ const resolvedPiProviderId = piProviderId ?? PI_PROVIDER_BY_DOISTBOT_PROVIDER[provider];
24
+ return ((resolvedPiProviderId === 'openrouter' ? DEFAULT_OPENROUTER_MODELS[provider] : undefined) ??
25
+ DEFAULT_PI_MODELS[provider]);
26
+ }
27
+ export function buildGeminiFlashReviewSummaryModel(credential) {
28
+ if (credential === undefined) {
29
+ return undefined;
30
+ }
31
+ const resolvedCredential = resolveReviewProviderCredential(credential);
32
+ if (!resolvedCredential) {
33
+ return undefined;
34
+ }
35
+ const piProviderId = resolvedCredential.piProviderId ?? 'google';
36
+ const modelName = GEMINI_FLASH_SUMMARY_MODEL_BY_PI_PROVIDER[piProviderId] ??
37
+ GEMINI_FLASH_SUMMARY_MODEL_BY_PI_PROVIDER.google;
38
+ return {
39
+ provider: 'gemini',
40
+ credential,
41
+ modelName,
42
+ };
43
+ }
44
+ function failedReviewSummaryRequest({ reason, error, timedOut, durationMs, outputChars, }) {
45
+ return {
46
+ status: 'failed',
47
+ reason,
48
+ ...(error ? { error } : {}),
49
+ ...(timedOut !== undefined ? { timedOut } : {}),
50
+ ...(durationMs !== undefined ? { durationMs } : {}),
51
+ ...(outputChars !== undefined ? { outputChars } : {}),
52
+ };
53
+ }
54
+ function getRequestFailureReason(result) {
55
+ if (result.timedOut) {
56
+ return 'timeout';
57
+ }
58
+ return result.error.toLowerCase().includes('did not produce an assistant response')
59
+ ? 'no_assistant_response'
60
+ : 'request_failed';
61
+ }
62
+ export async function requestReviewSummary({ credential, provider = 'zai', modelName, prompt, workspaceDir, }) {
14
63
  try {
15
64
  const resolvedCredential = resolveReviewProviderCredential(credential);
16
65
  if (!resolvedCredential) {
17
- return null;
66
+ return failedReviewSummaryRequest({ reason: 'missing_credential' });
18
67
  }
68
+ const resolvedModelName = modelName ?? resolvedCredential.modelName;
69
+ const logModelName = getReviewSummaryModelNameForLog({
70
+ provider,
71
+ piProviderId: resolvedCredential.piProviderId,
72
+ modelName: resolvedModelName,
73
+ });
74
+ logger.info('Review summary model request', {
75
+ provider,
76
+ modelName: logModelName,
77
+ promptLength: prompt.length,
78
+ timeoutMs: REVIEW_SUMMARY_TIMEOUT_MS,
79
+ thinkingLevel: REVIEW_SUMMARY_THINKING_LEVEL,
80
+ });
19
81
  const result = await invokePiPrompt({
20
- provider: 'gemini',
82
+ provider,
21
83
  apiKey: resolvedCredential.apiKey,
22
84
  ...(resolvedCredential.piProviderId
23
85
  ? { piProviderId: resolvedCredential.piProviderId }
24
86
  : {}),
25
- ...(resolvedCredential.modelName ? { modelName: resolvedCredential.modelName } : {}),
87
+ ...(resolvedModelName ? { modelName: resolvedModelName } : {}),
26
88
  prompt,
27
89
  cwd: workspaceDir ?? getWorkspaceDir(),
28
90
  timeoutMs: REVIEW_SUMMARY_TIMEOUT_MS,
29
91
  capabilityPreset: 'read-only',
92
+ thinkingLevel: REVIEW_SUMMARY_THINKING_LEVEL,
30
93
  });
31
94
  if (!result.ok) {
95
+ const reason = getRequestFailureReason(result);
32
96
  logger.warn('Review summary model request failed', {
97
+ reason,
98
+ provider,
99
+ modelName: logModelName,
33
100
  error: result.error,
34
101
  timedOut: result.timedOut,
102
+ durationMs: result.durationMs,
103
+ promptLength: prompt.length,
104
+ });
105
+ return failedReviewSummaryRequest({
106
+ reason,
107
+ error: result.error,
108
+ timedOut: result.timedOut,
109
+ durationMs: result.durationMs,
35
110
  });
36
- return null;
37
111
  }
38
112
  const output = result.output;
39
113
  if (!output?.trim()) {
40
- return null;
114
+ return failedReviewSummaryRequest({
115
+ reason: 'empty_output',
116
+ durationMs: result.durationMs,
117
+ outputChars: output?.length ?? 0,
118
+ });
41
119
  }
42
- const start = output.indexOf('{');
43
- const end = output.lastIndexOf('}');
44
- if (start === -1 || end === -1 || end <= start) {
45
- logger.warn('Review summary model output missing JSON delimiters');
46
- return null;
120
+ const jsonCandidates = extractJsonCandidatesFromResponse(output);
121
+ if (jsonCandidates.length === 0) {
122
+ logger.warn('Review summary model output missing JSON', {
123
+ provider,
124
+ modelName: logModelName,
125
+ durationMs: result.durationMs,
126
+ outputChars: output.length,
127
+ });
128
+ return failedReviewSummaryRequest({
129
+ reason: 'missing_json',
130
+ durationMs: result.durationMs,
131
+ outputChars: output.length,
132
+ });
133
+ }
134
+ let parsed;
135
+ let parsedSuccessfully = false;
136
+ let parseError;
137
+ for (const jsonCandidate of jsonCandidates) {
138
+ try {
139
+ parsed = JSON.parse(jsonCandidate);
140
+ parsedSuccessfully = true;
141
+ break;
142
+ }
143
+ catch (error) {
144
+ parseError = error;
145
+ }
146
+ }
147
+ if (!parsedSuccessfully) {
148
+ logger.warn('Review summary model output contained invalid JSON', {
149
+ provider,
150
+ modelName: logModelName,
151
+ error: parseError instanceof Error ? parseError.message : String(parseError),
152
+ durationMs: result.durationMs,
153
+ outputChars: output.length,
154
+ });
155
+ return failedReviewSummaryRequest({
156
+ reason: 'invalid_json',
157
+ error: parseError instanceof Error ? parseError.message : String(parseError),
158
+ durationMs: result.durationMs,
159
+ outputChars: output.length,
160
+ });
161
+ }
162
+ if (!parsed || typeof parsed !== 'object') {
163
+ return failedReviewSummaryRequest({
164
+ reason: 'invalid_schema',
165
+ durationMs: result.durationMs,
166
+ outputChars: output.length,
167
+ });
47
168
  }
48
- const jsonText = output.slice(start, end + 1);
49
- const parsed = JSON.parse(jsonText);
169
+ const summaryResult = parsed;
170
+ if (typeof summaryResult.summary !== 'string') {
171
+ return failedReviewSummaryRequest({
172
+ reason: 'invalid_schema',
173
+ durationMs: result.durationMs,
174
+ outputChars: output.length,
175
+ });
176
+ }
177
+ if (!summaryResult.summary.trim()) {
178
+ return failedReviewSummaryRequest({
179
+ reason: 'empty_summary',
180
+ durationMs: result.durationMs,
181
+ outputChars: output.length,
182
+ });
183
+ }
184
+ logger.info('Review summary model response', {
185
+ provider,
186
+ modelName: logModelName,
187
+ durationMs: result.durationMs,
188
+ outputChars: output.length,
189
+ summaryChars: summaryResult.summary.trim().length,
190
+ });
50
191
  return {
51
- summary: typeof parsed.summary === 'string' ? parsed.summary : '',
52
- comments: Array.isArray(parsed.comments) ? parsed.comments : [],
192
+ status: 'succeeded',
193
+ summary: summaryResult.summary,
194
+ comments: Array.isArray(summaryResult.comments) ? summaryResult.comments : [],
195
+ durationMs: result.durationMs,
196
+ outputChars: output.length,
53
197
  };
54
198
  }
55
199
  catch (error) {
56
200
  logger.error('Review summary model request or parsing failed', {
57
201
  error: error instanceof Error ? error.message : String(error),
58
202
  });
59
- return null;
203
+ return failedReviewSummaryRequest({
204
+ reason: 'request_failed',
205
+ error: error instanceof Error ? error.message : String(error),
206
+ });
60
207
  }
61
208
  }
62
209
  export function sanitizeInlineComments(comments, lineLookup) {
@@ -1,7 +1,11 @@
1
1
  export function getReviewThinkingLevel(model) {
2
2
  switch (model) {
3
+ case 'deepseek':
4
+ return 'high';
3
5
  case 'openai':
4
6
  return 'high';
7
+ case 'zai':
8
+ return 'high';
5
9
  case 'gemini':
6
10
  return 'high';
7
11
  }
@@ -1,12 +1,8 @@
1
1
  import type { Logger } from 'doistbot-logging';
2
2
  import type { Octokit } from 'octokit';
3
3
  export declare const CONFIG_FILE_PATH = ".github/doistbot.yml";
4
- export declare const DEFAULT_REVIEW_MODELS: readonly ["gemini", "openai"];
5
- export declare const REQUIRED_REVIEW_MODELS: readonly ["gemini", "openai"];
6
- type ParseReviewModelsEnabledOptions = {
7
- defaultModels?: readonly string[];
8
- requiredModels?: readonly string[];
9
- };
4
+ export declare const DEFAULT_REVIEW_MODELS: readonly ["gemini", "openai", "deepseek", "zai"];
5
+ export type DefaultReviewModel = (typeof DEFAULT_REVIEW_MODELS)[number];
10
6
  export type RepoConfig = {
11
7
  review?: {
12
8
  skip_forks?: boolean;
@@ -50,6 +46,6 @@ export declare function getLocalRepoConfig({ workspaceDir, logger, }: {
50
46
  workspaceDir: string;
51
47
  logger: Logger;
52
48
  }): RepoConfig | null;
53
- export declare function parseReviewModelsEnabled(raw: string | undefined, { defaultModels, requiredModels, }?: ParseReviewModelsEnabledOptions): string[];
54
- export declare function getDefaultReviewModels(): string[];
49
+ export declare function getDefaultReviewModels(): DefaultReviewModel[];
50
+ export declare function parseReviewModelsEnabled(value: string | undefined): string[];
55
51
  export {};
@@ -2,8 +2,7 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { load as yamlLoad } from 'js-yaml';
4
4
  export const CONFIG_FILE_PATH = '.github/doistbot.yml';
5
- export const DEFAULT_REVIEW_MODELS = ['gemini', 'openai'];
6
- export const REQUIRED_REVIEW_MODELS = DEFAULT_REVIEW_MODELS;
5
+ export const DEFAULT_REVIEW_MODELS = ['gemini', 'openai', 'deepseek', 'zai'];
7
6
  function parseRepoConfigContent(content) {
8
7
  return yamlLoad(content);
9
8
  }
@@ -91,24 +90,13 @@ export function getLocalRepoConfig({ workspaceDir, logger, }) {
91
90
  return null;
92
91
  }
93
92
  }
94
- export function parseReviewModelsEnabled(raw, { defaultModels = DEFAULT_REVIEW_MODELS, requiredModels = REQUIRED_REVIEW_MODELS, } = {}) {
95
- if (!raw?.trim()) {
96
- return [...defaultModels];
97
- }
98
- const allowedModels = new Set(DEFAULT_REVIEW_MODELS);
99
- const models = [];
100
- for (const token of raw.split(',')) {
101
- const model = token.trim().toLowerCase();
102
- if (!model || !allowedModels.has(model)) {
103
- continue;
104
- }
105
- if (!models.includes(model)) {
106
- models.push(model);
107
- }
108
- }
109
- const hasRequiredModels = requiredModels.every((model) => models.includes(model));
110
- return models.length > 0 && hasRequiredModels ? models : [...defaultModels];
111
- }
112
93
  export function getDefaultReviewModels() {
113
94
  return [...DEFAULT_REVIEW_MODELS];
114
95
  }
96
+ export function parseReviewModelsEnabled(value) {
97
+ const models = value
98
+ ?.split(',')
99
+ .map((model) => model.trim())
100
+ .filter(Boolean) ?? [];
101
+ return models.length > 0 ? [...new Set(models)] : getDefaultReviewModels();
102
+ }