@doist/doistbot-cli 1.0.9 → 1.0.11

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 (35) hide show
  1. package/dist/actions/auth.js +1 -1
  2. package/dist/actions/review-usage.js +4 -4
  3. package/dist/actions/review.js +32 -21
  4. package/dist/config.js +59 -13
  5. package/dist/index.js +8 -2
  6. package/dist/terminal.js +5 -0
  7. package/package.json +4 -4
  8. package/sandbox/dist/core/datadog-metrics.js +9 -2
  9. package/sandbox/dist/core/label-parsing.js +1 -22
  10. package/sandbox/dist/core/pi-execution-limits-extension.js +79 -0
  11. package/sandbox/dist/core/pi-progress-logging.js +4 -0
  12. package/sandbox/dist/core/pi.js +290 -67
  13. package/sandbox/dist/core/repository-map.js +1 -220
  14. package/sandbox/dist/core/repository-resolver.js +1 -166
  15. package/sandbox/dist/core/tracing.js +1 -1
  16. package/sandbox/dist/providers/config.js +1 -1
  17. package/sandbox/dist/providers/index.js +1 -0
  18. package/sandbox/dist/providers/pi-review.js +53 -3
  19. package/sandbox/dist/tasks/chat/chat.js +95 -45
  20. package/sandbox/dist/tasks/chat/prompt.js +5 -0
  21. package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +1 -0
  22. package/sandbox/dist/tasks/issue-summarize/model.js +1 -1
  23. package/sandbox/dist/tasks/issue-triage/context.js +2 -0
  24. package/sandbox/dist/tasks/issue-triage/fix-attempt.js +2 -1
  25. package/sandbox/dist/tasks/issue-triage/fix-loop/promotion-task.js +4 -7
  26. package/sandbox/dist/tasks/issue-triage/hero-group-map.js +65 -41
  27. package/sandbox/dist/tasks/issue-triage/output.js +1 -1
  28. package/sandbox/dist/tasks/issue-triage/resolution.js +1 -0
  29. package/sandbox/dist/tasks/issue-triage/shared.js +2 -12
  30. package/sandbox/dist/tasks/issue-triage/triage.js +2 -0
  31. package/sandbox/dist/tasks/review/engines/multi-focus.js +14 -0
  32. package/sandbox/dist/tasks/review/engines/shared.js +2 -0
  33. package/sandbox/dist/tasks/review/review.js +5 -3
  34. package/sandbox/dist/tasks/review/summary-model.js +4 -3
  35. package/sandbox/dist/tasks/review/thinking-level.js +2 -0
@@ -1,220 +1 @@
1
- const DEFAULT_REPOSITORY_OWNER = 'Doist';
2
- function createRepositoryTarget({ owner = DEFAULT_REPOSITORY_OWNER, repo, product, surfaces, }) {
3
- return {
4
- owner,
5
- repo,
6
- fullName: `${owner}/${repo}`,
7
- product,
8
- surfaces,
9
- };
10
- }
11
- const TODOIST_REPOSITORIES = [
12
- createRepositoryTarget({
13
- repo: 'Todoist',
14
- product: 'Todoist',
15
- surfaces: ['backend'],
16
- }),
17
- createRepositoryTarget({
18
- repo: 'todoist-web',
19
- product: 'Todoist',
20
- surfaces: ['web'],
21
- }),
22
- createRepositoryTarget({
23
- repo: 'reactist',
24
- product: 'Todoist',
25
- surfaces: ['web'],
26
- }),
27
- createRepositoryTarget({
28
- repo: 'todoist-ios',
29
- product: 'Todoist',
30
- surfaces: ['ios'],
31
- }),
32
- createRepositoryTarget({
33
- repo: 'dateist-ios',
34
- product: 'Todoist',
35
- surfaces: ['ios'],
36
- }),
37
- createRepositoryTarget({
38
- repo: 'textist-swift',
39
- product: 'Todoist',
40
- surfaces: ['ios'],
41
- }),
42
- createRepositoryTarget({
43
- repo: 'Todoist-Android',
44
- product: 'Todoist',
45
- surfaces: ['android'],
46
- }),
47
- createRepositoryTarget({
48
- repo: 'Androist',
49
- product: 'Todoist',
50
- surfaces: ['android'],
51
- }),
52
- createRepositoryTarget({
53
- repo: 'todoist-electron',
54
- product: 'Todoist',
55
- surfaces: ['desktop'],
56
- }),
57
- createRepositoryTarget({
58
- repo: 'todoist-macos',
59
- product: 'Todoist',
60
- surfaces: ['desktop'],
61
- }),
62
- createRepositoryTarget({
63
- repo: 'FilteristKt',
64
- product: 'Todoist',
65
- surfaces: ['android', 'ios'],
66
- }),
67
- createRepositoryTarget({
68
- repo: 'todoist-mcp',
69
- product: 'Todoist',
70
- surfaces: ['mcp'],
71
- }),
72
- createRepositoryTarget({
73
- repo: 'todoist-cli',
74
- product: 'Todoist',
75
- surfaces: ['cli'],
76
- }),
77
- ];
78
- const TWIST_REPOSITORIES = [
79
- createRepositoryTarget({
80
- repo: 'Twist',
81
- product: 'Twist',
82
- surfaces: ['backend'],
83
- }),
84
- createRepositoryTarget({
85
- repo: 'twist-web',
86
- product: 'Twist',
87
- surfaces: ['web'],
88
- }),
89
- createRepositoryTarget({
90
- repo: 'reactist',
91
- product: 'Twist',
92
- surfaces: ['web'],
93
- }),
94
- createRepositoryTarget({
95
- repo: 'twist-ios',
96
- product: 'Twist',
97
- surfaces: ['ios'],
98
- }),
99
- createRepositoryTarget({
100
- repo: 'doistui-swift',
101
- product: 'Twist',
102
- surfaces: ['ios'],
103
- }),
104
- createRepositoryTarget({
105
- repo: 'emojist-swift',
106
- product: 'Twist',
107
- surfaces: ['ios'],
108
- }),
109
- createRepositoryTarget({
110
- repo: 'keyboardist-swift',
111
- product: 'Twist',
112
- surfaces: ['ios'],
113
- }),
114
- createRepositoryTarget({
115
- repo: 'participantist-swift',
116
- product: 'Twist',
117
- surfaces: ['ios'],
118
- }),
119
- createRepositoryTarget({
120
- repo: 'reactionist-swift',
121
- product: 'Twist',
122
- surfaces: ['ios'],
123
- }),
124
- createRepositoryTarget({
125
- repo: 'Twist-Android',
126
- product: 'Twist',
127
- surfaces: ['android'],
128
- }),
129
- createRepositoryTarget({
130
- repo: 'Androist',
131
- product: 'Twist',
132
- surfaces: ['android'],
133
- }),
134
- createRepositoryTarget({
135
- repo: 'twist-electron',
136
- product: 'Twist',
137
- surfaces: ['desktop'],
138
- }),
139
- createRepositoryTarget({
140
- repo: 'twist-ai',
141
- product: 'Twist',
142
- surfaces: ['mcp'],
143
- }),
144
- createRepositoryTarget({
145
- repo: 'twist-cli',
146
- product: 'Twist',
147
- surfaces: ['cli'],
148
- }),
149
- ];
150
- function dedupeRepositories(repositories) {
151
- const unique = new Map();
152
- for (const repository of repositories) {
153
- unique.set(repository.fullName, repository);
154
- }
155
- return [...unique.values()];
156
- }
157
- function selectBySurface(repositories, surfaces) {
158
- return dedupeRepositories(repositories.filter((repository) => repository.surfaces.some((surface) => surfaces.includes(surface))));
159
- }
160
- export const PRODUCT_REPOSITORIES = {
161
- Todoist: TODOIST_REPOSITORIES,
162
- Twist: TWIST_REPOSITORIES,
163
- };
164
- export const APP_TO_REPO_SELECTORS = {
165
- all: (repositories) => dedupeRepositories(repositories),
166
- android: (repositories) => selectBySurface(repositories, ['android']),
167
- backend: (repositories) => selectBySurface(repositories, ['backend']),
168
- cli: (repositories) => selectBySurface(repositories, ['cli']),
169
- desktop: (repositories) => selectBySurface(repositories, ['desktop']),
170
- ios: (repositories) => selectBySurface(repositories, ['ios']),
171
- mcp: (repositories) => selectBySurface(repositories, ['mcp']),
172
- web: (repositories) => selectBySurface(repositories, ['web']),
173
- };
174
- export const TEAM_TO_REPO_SELECTORS = {
175
- all: (repositories) => dedupeRepositories(repositories),
176
- android: (repositories) => selectBySurface(repositories, ['android']),
177
- apple: (repositories) => selectBySurface(repositories, ['ios']),
178
- backend: (repositories) => selectBySurface(repositories, ['backend']),
179
- desktop: (repositories) => selectBySurface(repositories, ['desktop']),
180
- frontend: (repositories) => selectBySurface(repositories, ['web']),
181
- ios: (repositories) => selectBySurface(repositories, ['ios']),
182
- mobile: (repositories) => dedupeRepositories([
183
- ...selectBySurface(repositories, ['ios']),
184
- ...selectBySurface(repositories, ['android']),
185
- ]),
186
- web: (repositories) => selectBySurface(repositories, ['web']),
187
- };
188
- export const RESOLUTION_PRECEDENCE = [
189
- 'Product label is required to resolve product-specific repositories.',
190
- 'App selectors take precedence over team selectors when App:* labels are present.',
191
- 'App:All expands to every repository in the selected product inventory.',
192
- 'Specific app labels remain authoritative even when team labels are broad.',
193
- 'Team selectors expand scope only when App:* labels are absent.',
194
- 'Missing, contradictory, or unsupported label sets return an unresolved result.',
195
- ];
196
- export const PRODUCT_LABEL_ALIASES = {
197
- todoist: 'Todoist',
198
- twist: 'Twist',
199
- };
200
- export const APP_LABEL_ALIASES = {
201
- all: 'all',
202
- android: 'android',
203
- backend: 'backend',
204
- cli: 'cli',
205
- desktop: 'desktop',
206
- ios: 'ios',
207
- mcp: 'mcp',
208
- web: 'web',
209
- };
210
- export const TEAM_LABEL_ALIASES = {
211
- all: 'all',
212
- android: 'android',
213
- apple: 'apple',
214
- backend: 'backend',
215
- desktop: 'desktop',
216
- frontend: 'frontend',
217
- ios: 'ios',
218
- mobile: 'mobile',
219
- web: 'web',
220
- };
1
+ export { APP_LABEL_ALIASES, APP_TO_ROUTE_SELECTORS, findIssueRoute, findRepositoryRoutes, findRepositoryTarget, PRODUCT_LABEL_ALIASES, PRODUCT_ROUTES, RESOLUTION_PRECEDENCE, TEAM_LABEL_ALIASES, TEAM_TO_ROUTE_SELECTORS, } from 'doistbot-issue-routing';
@@ -1,166 +1 @@
1
- import { extractScopedLabelValues, normalizeScopedLabelValue } from './label-parsing.js';
2
- import { APP_LABEL_ALIASES, APP_TO_REPO_SELECTORS, PRODUCT_LABEL_ALIASES, PRODUCT_REPOSITORIES, RESOLUTION_PRECEDENCE, TEAM_LABEL_ALIASES, TEAM_TO_REPO_SELECTORS, } from './repository-map.js';
3
- function uniqueValues(values) {
4
- return [...new Set(values)];
5
- }
6
- function resolveProductLabels(productLabels) {
7
- const products = [];
8
- const unknown = [];
9
- for (const rawLabel of productLabels) {
10
- const canonical = PRODUCT_LABEL_ALIASES[normalizeScopedLabelValue(rawLabel)];
11
- if (canonical) {
12
- products.push(canonical);
13
- }
14
- else {
15
- unknown.push(rawLabel);
16
- }
17
- }
18
- return {
19
- products: uniqueValues(products),
20
- unknown: uniqueValues(unknown),
21
- };
22
- }
23
- function resolveScopedLabels(labels, aliases) {
24
- const resolved = [];
25
- const unknown = [];
26
- for (const rawLabel of labels) {
27
- const canonical = aliases[normalizeScopedLabelValue(rawLabel)];
28
- if (canonical) {
29
- resolved.push(canonical);
30
- }
31
- else {
32
- unknown.push(rawLabel);
33
- }
34
- }
35
- return {
36
- resolved: uniqueValues(resolved),
37
- unknown: uniqueValues(unknown),
38
- };
39
- }
40
- function dedupeRepositories(repositories) {
41
- const unique = new Map();
42
- for (const repository of repositories) {
43
- unique.set(repository.fullName, repository);
44
- }
45
- return [...unique.values()];
46
- }
47
- function parseLabelScopes(labels) {
48
- return {
49
- productLabels: extractScopedLabelValues(labels, 'Product'),
50
- appLabels: extractScopedLabelValues(labels, 'App'),
51
- teamLabels: extractScopedLabelValues(labels, 'Team'),
52
- };
53
- }
54
- export function resolveIssueRepositories(labels) {
55
- const decisionPath = [...RESOLUTION_PRECEDENCE];
56
- const { productLabels, appLabels, teamLabels } = parseLabelScopes(labels);
57
- const productResolution = resolveProductLabels(productLabels);
58
- const appResolution = resolveScopedLabels(appLabels, APP_LABEL_ALIASES);
59
- const teamResolution = resolveScopedLabels(teamLabels, TEAM_LABEL_ALIASES);
60
- const parsedLabels = {
61
- allLabels: labels,
62
- productLabels: productResolution.products,
63
- appLabels: appResolution.resolved,
64
- teamLabels: teamResolution.resolved,
65
- unknownProductLabels: productResolution.unknown,
66
- unknownAppLabels: appResolution.unknown,
67
- unknownTeamLabels: teamResolution.unknown,
68
- };
69
- if (productResolution.products.length === 0) {
70
- return {
71
- status: 'unresolved',
72
- reason: 'Missing Product:* label. Please add Product:Todoist or Product:Twist.',
73
- confidence: 'low',
74
- decisionPath,
75
- repositories: [],
76
- parsedLabels,
77
- };
78
- }
79
- if (productResolution.products.length > 1) {
80
- return {
81
- status: 'unresolved',
82
- reason: `Contradictory product labels: ${productResolution.products.join(', ')}`,
83
- confidence: 'low',
84
- decisionPath,
85
- repositories: [],
86
- parsedLabels,
87
- };
88
- }
89
- const product = productResolution.products[0];
90
- if (!product) {
91
- return {
92
- status: 'unresolved',
93
- reason: 'Unable to resolve product labels.',
94
- confidence: 'low',
95
- decisionPath,
96
- repositories: [],
97
- parsedLabels,
98
- };
99
- }
100
- const repositoriesForProduct = PRODUCT_REPOSITORIES[product];
101
- if (appResolution.resolved.length > 0) {
102
- decisionPath.push('Applied app selectors and skipped team selectors due App:* precedence.');
103
- const selectedRepositories = dedupeRepositories(appResolution.resolved.flatMap((appLabel) => {
104
- const selector = APP_TO_REPO_SELECTORS[appLabel];
105
- return selector ? selector(repositoriesForProduct) : [];
106
- }));
107
- if (selectedRepositories.length === 0) {
108
- return {
109
- status: 'unresolved',
110
- reason: `App labels did not map to repositories for product ${product}.`,
111
- confidence: 'low',
112
- decisionPath,
113
- product,
114
- repositories: [],
115
- parsedLabels,
116
- };
117
- }
118
- return {
119
- status: 'resolved',
120
- reason: appResolution.resolved.includes('all')
121
- ? `Resolved using Product:${product} + App:All.`
122
- : `Resolved using Product:${product} + App selectors (${appResolution.resolved.join(', ')}).`,
123
- confidence: 'high',
124
- decisionPath,
125
- product,
126
- repositories: selectedRepositories,
127
- parsedLabels,
128
- };
129
- }
130
- if (teamResolution.resolved.length > 0) {
131
- decisionPath.push('No App:* labels found; applied team selectors.');
132
- const selectedRepositories = dedupeRepositories(teamResolution.resolved.flatMap((teamLabel) => {
133
- const selector = TEAM_TO_REPO_SELECTORS[teamLabel];
134
- return selector ? selector(repositoriesForProduct) : [];
135
- }));
136
- if (selectedRepositories.length === 0) {
137
- return {
138
- status: 'unresolved',
139
- reason: `Team labels did not map to repositories for product ${product}.`,
140
- confidence: 'low',
141
- decisionPath,
142
- product,
143
- repositories: [],
144
- parsedLabels,
145
- };
146
- }
147
- return {
148
- status: 'resolved',
149
- reason: `Resolved using Product:${product} + Team selectors (${teamResolution.resolved.join(', ')}).`,
150
- confidence: 'medium',
151
- decisionPath,
152
- product,
153
- repositories: selectedRepositories,
154
- parsedLabels,
155
- };
156
- }
157
- return {
158
- status: 'unresolved',
159
- reason: 'Missing App:* and Team:* labels. Add scope labels to continue triage.',
160
- confidence: 'low',
161
- decisionPath,
162
- product,
163
- repositories: [],
164
- parsedLabels,
165
- };
166
- }
1
+ export { isKnownIssueRoutingLabel, resolveIssueRepositories } from 'doistbot-issue-routing';
@@ -72,7 +72,7 @@ export function recordEvent(name, meta) {
72
72
  /**
73
73
  * Maps a doistbot provider key to the OTel `gen_ai.provider.name` value
74
74
  * (docs/sandbox-instrumentation.md). Only `gemini` has a standard name
75
- * (`gcp.gemini`); the OpenRouter-routed keys (`zai`, `deepseek`) have none, so
75
+ * (`gcp.gemini`); the OpenRouter-routed keys (`grok`, `zai`, `deepseek`) have none, so
76
76
  * they pass through. Single source so every span site stays consistent.
77
77
  */
78
78
  export function genAiProviderName(provider) {
@@ -1,4 +1,4 @@
1
- const DEFAULT_PROVIDER_TIMEOUT_MS = 10 * 60 * 1000;
1
+ export const DEFAULT_PROVIDER_TIMEOUT_MS = 10 * 60 * 1000;
2
2
  function parseTimeoutMs(value) {
3
3
  if (!value) {
4
4
  return null;
@@ -11,6 +11,7 @@ export * from './types.js';
11
11
  const providers = {
12
12
  deepseek: new OpenRouterPiProvider('deepseek'),
13
13
  gemini: new GeminiProvider(),
14
+ grok: new OpenRouterPiProvider('grok'),
14
15
  zai: new OpenRouterPiProvider('zai'),
15
16
  openai: new OpenAIProvider(),
16
17
  };
@@ -1,13 +1,21 @@
1
1
  import { logger } from '../core/logging.js';
2
- import { createPiInvoker, parsePiAuthProviderId, PI_PROVIDER_BY_DOISTBOT_PROVIDER, } from '../core/pi.js';
2
+ import { createPiInvoker, parsePiAuthProviderId, PI_PROVIDER_BY_DOISTBOT_PROVIDER, toPiInvocationTelemetryFields, } from '../core/pi.js';
3
3
  import { logPiProgressEvent } from '../core/pi-progress-logging.js';
4
4
  import { WORKSPACE_DIR } from '../core/shared.js';
5
+ import { DEFAULT_PROVIDER_TIMEOUT_MS } from './config.js';
5
6
  import { logRawModelOutput, normalizeParsedModelOutput } from './helpers.js';
6
7
  const VERBOSE_PI_PROGRESS_LOGS = process.env.DOISTBOT_VERBOSE_PI_PROGRESS === 'true';
7
8
  const VERBOSE_PI_RAW_OUTPUT_LOGS = process.env.DOISTBOT_VERBOSE_PI_RAW_OUTPUT === 'true';
8
9
  const REVIEW_LOG_ENGINE = 'multi_focus';
10
+ export const REVIEW_PI_EXECUTION_LIMITS = {
11
+ softTimeMs: 5 * 60 * 1000,
12
+ softToolCalls: 40,
13
+ hardTimeMs: 8 * 60 * 1000,
14
+ hardToolCalls: 65,
15
+ };
16
+ export const REVIEW_PI_PROCESS_KILL_MS = DEFAULT_PROVIDER_TIMEOUT_MS;
9
17
  const REVIEW_PI_SYSTEM_PROMPT = 'You are running inside doistbot in a non-interactive environment. Stay tightly focused on the review task in the user prompt. Read only the files that are directly relevant to the changed code, the explicitly named guidance files, and any immediately connected definitions needed to verify a concrete finding. Do not broadly explore the repository, inspect unrelated modules, or search history unless it is necessary to validate a specific review concern. Stop gathering context once you have enough evidence to answer the review task accurately.';
10
- function buildModelFailureResult({ model, error, durationMs, timedOut = false, }) {
18
+ function buildModelFailureResult({ model, error, durationMs, timedOut = false, telemetry, }) {
11
19
  return {
12
20
  model,
13
21
  summary: '',
@@ -15,9 +23,36 @@ function buildModelFailureResult({ model, error, durationMs, timedOut = false, }
15
23
  error,
16
24
  timedOut,
17
25
  durationMs,
26
+ ...(telemetry ? { telemetry } : {}),
18
27
  };
19
28
  }
29
+ function logReviewLatency({ provider, modelName, attempt, attemptsTotal, result, }) {
30
+ const telemetry = result.telemetry;
31
+ logger.info('Review model latency', {
32
+ model: provider,
33
+ engine: REVIEW_LOG_ENGINE,
34
+ model_name: modelName,
35
+ attempt,
36
+ attempts_total: attemptsTotal,
37
+ success: result.ok,
38
+ timed_out: !result.ok && result.timedOut,
39
+ stop_reason: result.stopReason,
40
+ duration_ms: result.durationMs,
41
+ ...(telemetry ? toPiInvocationTelemetryFields(telemetry) : {}),
42
+ });
43
+ }
20
44
  function logReviewModelProgress(model, event) {
45
+ if (event.type === 'limit_reached') {
46
+ logger.warn('Review model execution limit reached', {
47
+ model,
48
+ engine: REVIEW_LOG_ENGINE,
49
+ level: event.level,
50
+ trigger: event.trigger,
51
+ elapsed_ms: event.elapsedMs,
52
+ tool_call_count: event.toolCallCount,
53
+ });
54
+ return;
55
+ }
21
56
  logPiProgressEvent({
22
57
  event,
23
58
  messages: {
@@ -61,10 +96,11 @@ export async function runPiReviewProvider(params) {
61
96
  }
62
97
  async function runPiReviewProviderCore({ provider, modelName, prompt, config, prompts = [prompt], parseOutput, }) {
63
98
  const startTime = Date.now();
64
- const timeoutMs = config.timeout ?? 0;
99
+ const timeoutMs = Math.min(config.timeout ?? REVIEW_PI_PROCESS_KILL_MS, REVIEW_PI_PROCESS_KILL_MS);
65
100
  let stdout = '';
66
101
  let stderr = '';
67
102
  let status = null;
103
+ let latestTelemetry;
68
104
  logger.info('Review model request', {
69
105
  model: provider,
70
106
  engine: REVIEW_LOG_ENGINE,
@@ -89,6 +125,7 @@ async function runPiReviewProviderCore({ provider, modelName, prompt, config, pr
89
125
  systemPrompt: REVIEW_PI_SYSTEM_PROMPT,
90
126
  noContextFiles: true,
91
127
  openRouterSessionId: config.openRouterSessionId,
128
+ executionLimits: REVIEW_PI_EXECUTION_LIMITS,
92
129
  });
93
130
  for (let attempt = 0; attempt < prompts.length; attempt += 1) {
94
131
  const result = await invoker.invoke({
@@ -97,6 +134,7 @@ async function runPiReviewProviderCore({ provider, modelName, prompt, config, pr
97
134
  timeoutMs,
98
135
  });
99
136
  const durationMs = Date.now() - startTime;
137
+ latestTelemetry = result.telemetry;
100
138
  stdout = result.ok ? result.output : '';
101
139
  stderr = result.ok ? '' : result.error;
102
140
  status = result.ok ? 0 : null;
@@ -108,6 +146,13 @@ async function runPiReviewProviderCore({ provider, modelName, prompt, config, pr
108
146
  attemptDurationMs: result.durationMs,
109
147
  toolUsage: result.toolUsage,
110
148
  });
149
+ logReviewLatency({
150
+ provider,
151
+ modelName,
152
+ attempt: attempt + 1,
153
+ attemptsTotal: prompts.length,
154
+ result,
155
+ });
111
156
  if (!result.ok && result.timedOut) {
112
157
  logReviewRawModelOutput({
113
158
  model: provider,
@@ -130,6 +175,7 @@ async function runPiReviewProviderCore({ provider, modelName, prompt, config, pr
130
175
  error,
131
176
  durationMs,
132
177
  timedOut: true,
178
+ telemetry: result.telemetry,
133
179
  });
134
180
  }
135
181
  if (!result.ok) {
@@ -153,6 +199,7 @@ async function runPiReviewProviderCore({ provider, modelName, prompt, config, pr
153
199
  model: provider,
154
200
  error,
155
201
  durationMs,
202
+ telemetry: result.telemetry,
156
203
  });
157
204
  }
158
205
  const output = result.output;
@@ -170,6 +217,7 @@ async function runPiReviewProviderCore({ provider, modelName, prompt, config, pr
170
217
  model: provider,
171
218
  error: `Empty response from Pi ${provider} request`,
172
219
  durationMs,
220
+ telemetry: result.telemetry,
173
221
  });
174
222
  }
175
223
  try {
@@ -202,6 +250,7 @@ async function runPiReviewProviderCore({ provider, modelName, prompt, config, pr
202
250
  summary: normalized.summary,
203
251
  findings: normalized.findings,
204
252
  durationMs,
253
+ telemetry: result.telemetry,
205
254
  };
206
255
  }
207
256
  catch (error) {
@@ -246,6 +295,7 @@ async function runPiReviewProviderCore({ provider, modelName, prompt, config, pr
246
295
  error: errorMessage,
247
296
  durationMs,
248
297
  timedOut: errorMessage.toLowerCase().includes('timed out'),
298
+ telemetry: latestTelemetry,
249
299
  });
250
300
  }
251
301
  }