@doist/doistbot-cli 1.0.4 → 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 (82) hide show
  1. package/dist/actions/auth.js +62 -28
  2. package/dist/actions/doctor.js +16 -11
  3. package/dist/actions/review.js +77 -11
  4. package/dist/auth.js +53 -8
  5. package/dist/config.js +40 -6
  6. package/dist/env.js +1 -1
  7. package/dist/file-store.js +1 -1
  8. package/dist/runtime.js +1 -1
  9. package/dist/terminal.js +2 -7
  10. package/package.json +7 -7
  11. package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +5 -23
  12. package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +9 -85
  13. package/sandbox/dist/core/bot-logins.js +0 -13
  14. package/sandbox/dist/core/check-run.js +16 -0
  15. package/sandbox/dist/core/datadog-metrics.js +6 -54
  16. package/sandbox/dist/core/logger-span-processor.js +78 -0
  17. package/sandbox/dist/core/pi.js +270 -186
  18. package/sandbox/dist/core/tracing-setup.js +15 -0
  19. package/sandbox/dist/core/tracing.js +71 -0
  20. package/sandbox/dist/main.js +4 -1
  21. package/sandbox/dist/providers/config.js +1 -1
  22. package/sandbox/dist/providers/credentials.js +40 -0
  23. package/sandbox/dist/providers/gemini.js +8 -10
  24. package/sandbox/dist/providers/helpers.js +18 -38
  25. package/sandbox/dist/providers/index.js +4 -3
  26. package/sandbox/dist/providers/openai.js +1 -8
  27. package/sandbox/dist/providers/openrouter-pi.js +25 -0
  28. package/sandbox/dist/providers/pi-review.js +28 -55
  29. package/sandbox/dist/providers/schemas.js +0 -39
  30. package/sandbox/dist/tasks/chat/chat.js +162 -101
  31. package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +210 -185
  32. package/sandbox/dist/tasks/issue-summarize/context.js +1 -1
  33. package/sandbox/dist/tasks/issue-summarize/model.js +4 -2
  34. package/sandbox/dist/tasks/issue-summarize/summarize.js +1 -1
  35. package/sandbox/dist/tasks/issue-triage/clone.js +40 -0
  36. package/sandbox/dist/tasks/issue-triage/context.js +25 -2
  37. package/sandbox/dist/tasks/issue-triage/fix-attempt.js +31 -8
  38. package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +154 -101
  39. package/sandbox/dist/tasks/issue-triage/fix-loop/runners.js +15 -6
  40. package/sandbox/dist/tasks/issue-triage/model.js +120 -36
  41. package/sandbox/dist/tasks/issue-triage/output.js +27 -0
  42. package/sandbox/dist/tasks/issue-triage/pr-creator.js +3 -2
  43. package/sandbox/dist/tasks/issue-triage/resolution.js +26 -0
  44. package/sandbox/dist/tasks/issue-triage/routing.js +9 -9
  45. package/sandbox/dist/tasks/issue-triage/side-effects.js +9 -0
  46. package/sandbox/dist/tasks/issue-triage/triage.js +288 -418
  47. package/sandbox/dist/tasks/persist-logs/dd-proxy.js +2 -2
  48. package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +1 -1
  49. package/sandbox/dist/tasks/persist-logs/persist-logs.js +1 -1
  50. package/sandbox/dist/tasks/review/check-run.js +2 -1
  51. package/sandbox/dist/tasks/review/conversation-context.js +15 -8
  52. package/sandbox/dist/tasks/review/engines/dedupe.js +19 -9
  53. package/sandbox/dist/tasks/review/engines/multi-focus.js +62 -36
  54. package/sandbox/dist/tasks/review/engines/shared.js +15 -61
  55. package/sandbox/dist/tasks/review/github-comment-format.js +64 -10
  56. package/sandbox/dist/tasks/review/local.js +2 -29
  57. package/sandbox/dist/tasks/review/prompt.js +2 -49
  58. package/sandbox/dist/tasks/review/review-summary.js +120 -0
  59. package/sandbox/dist/tasks/review/review.js +49 -69
  60. package/sandbox/dist/tasks/review/runner.js +57 -52
  61. package/sandbox/dist/tasks/review/summary-model.js +172 -16
  62. package/sandbox/dist/tasks/review/thinking-level.js +4 -2
  63. package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +5 -23
  64. package/sandbox/node_modules/doistbot-repo-config/dist/index.js +9 -85
  65. package/sandbox/_node_modules/doistbot-repo-config/dist/review-engine.d.ts +0 -11
  66. package/sandbox/_node_modules/doistbot-repo-config/dist/review-engine.js +0 -41
  67. package/sandbox/dist/providers/anthropic.js +0 -30
  68. package/sandbox/dist/tasks/review/author-format.js +0 -14
  69. package/sandbox/dist/tasks/review/author-profile.js +0 -67
  70. package/sandbox/dist/tasks/review/council/normalizer.js +0 -104
  71. package/sandbox/dist/tasks/review/council/orchestrator.js +0 -194
  72. package/sandbox/dist/tasks/review/council/provider-health.js +0 -31
  73. package/sandbox/dist/tasks/review/council/summary.js +0 -192
  74. package/sandbox/dist/tasks/review/council/types.js +0 -4
  75. package/sandbox/dist/tasks/review/council/voter.js +0 -384
  76. package/sandbox/dist/tasks/review/engines/council.js +0 -126
  77. package/sandbox/dist/tasks/review/engines/index.js +0 -12
  78. package/sandbox/dist/tasks/review/review-engine.js +0 -5
  79. package/sandbox/node_modules/doistbot-repo-config/dist/review-engine.d.ts +0 -11
  80. package/sandbox/node_modules/doistbot-repo-config/dist/review-engine.js +0 -41
  81. package/sandbox/review-prompt.md +0 -209
  82. package/sandbox/vote-prompt.md +0 -69
@@ -0,0 +1,71 @@
1
+ import { context, SpanStatusCode, trace } from '@opentelemetry/api';
2
+ // recordDecision/recordEvent emit directly (bypassing the processor), so they
3
+ // apply the same per-attribute cap themselves to keep large arrays/objects
4
+ // from blowing the log-size budget.
5
+ import { truncateAttrs } from './logger-span-processor.js';
6
+ import { logger } from './logging.js';
7
+ const tracer = trace.getTracer('doistbot-sandbox');
8
+ /**
9
+ * THE phase-boundary API. Works for sequential AND parallel phases — OTel's
10
+ * async-hooks context manager (registered by setupTracing) parents nested
11
+ * spans correctly even across Promise.all.
12
+ *
13
+ * On throw: records the exception, sets span status ERROR, re-throws.
14
+ * On normal return: status is left UNSET (OTel's recommended success default;
15
+ * the LoggerSpanProcessor maps UNSET → 'ok'). We deliberately do NOT force
16
+ * OK, so a callback that returns a failure result (instead of throwing) and
17
+ * sets ERROR itself keeps its ERROR status.
18
+ * Always ends the span.
19
+ */
20
+ export async function withPhase(name, attributes, fn) {
21
+ return tracer.startActiveSpan(name, { attributes: { ...attributes, 'doistbot.kind': 'phase' } }, async (span) => {
22
+ try {
23
+ return await fn(span);
24
+ }
25
+ catch (err) {
26
+ span.recordException(err);
27
+ span.setStatus({
28
+ code: SpanStatusCode.ERROR,
29
+ message: err instanceof Error ? err.message : String(err),
30
+ });
31
+ throw err;
32
+ }
33
+ finally {
34
+ span.end();
35
+ }
36
+ });
37
+ }
38
+ /**
39
+ * Start a tool span manually. The ONE exception to "withPhase everywhere":
40
+ * pi emits tool start/end across separate async callbacks (onProgress), so
41
+ * there is no single scope to wrap. Parent = the currently-active span (the
42
+ * model phase). Caller MUST call `span.end()` when the matching tool_end
43
+ * arrives (and may `setStatus({ code: ERROR })` first on failure).
44
+ */
45
+ export function startToolSpan(name, attributes) {
46
+ return tracer.startSpan(name, { attributes: { ...attributes, 'doistbot.kind': 'tool' } }, context.active());
47
+ }
48
+ // Point-in-time lines bypass OTel spans entirely; they read the active span
49
+ // id only for correlation. Meta is spread FIRST so callers can never clobber
50
+ // the envelope fields (event/name/span_id). Timestamps come from the logging
51
+ // system, never from meta.
52
+ function emitPointInTime(kind, name, meta) {
53
+ const span = trace.getActiveSpan();
54
+ logger.info(`${kind}: ${name}`, {
55
+ ...truncateAttrs(meta),
56
+ event: kind,
57
+ name,
58
+ span_id: span?.spanContext().spanId ?? null,
59
+ });
60
+ }
61
+ /**
62
+ * Record a point-in-time decision (branch taken, scope resolved, gate passed)
63
+ * correlated to the active span. Not a span — no duration.
64
+ */
65
+ export function recordDecision(name, meta) {
66
+ emitPointInTime('decision', name, meta);
67
+ }
68
+ /** Generic point-in-time annotation. Same mechanics as recordDecision. */
69
+ export function recordEvent(name, meta) {
70
+ emitPointInTime('event', name, meta);
71
+ }
@@ -1,3 +1,7 @@
1
+ import { setupTracing } from './core/tracing-setup.js';
2
+ // Register the OTel SDK before any task module loads — withPhase/startToolSpan
3
+ // are no-ops until a tracer provider is registered.
4
+ setupTracing();
1
5
  const taskType = process.env.TASK_TYPE ?? 'review';
2
6
  switch (taskType) {
3
7
  case 'chat':
@@ -32,4 +36,3 @@ switch (taskType) {
32
36
  process.stderr.write(`Unknown TASK_TYPE: ${taskType}\n`);
33
37
  process.exit(1);
34
38
  }
35
- export {};
@@ -15,7 +15,7 @@ export function resolveProviderTimeoutMs(explicitTimeoutMs) {
15
15
  explicitTimeoutMs > 0) {
16
16
  return explicitTimeoutMs;
17
17
  }
18
- const sharedTimeout = parseTimeoutMs(process.env.COUNCIL_MODEL_TIMEOUT_MS);
18
+ const sharedTimeout = parseTimeoutMs(process.env.REVIEW_MODEL_TIMEOUT_MS);
19
19
  if (sharedTimeout != null) {
20
20
  return sharedTimeout;
21
21
  }
@@ -0,0 +1,40 @@
1
+ import { parsePiAuthProviderId } from '../core/pi.js';
2
+ export function resolveReviewProviderCredential(credential) {
3
+ if (typeof credential === 'string') {
4
+ const apiKey = credential.trim();
5
+ if (!apiKey) {
6
+ return undefined;
7
+ }
8
+ const piAuthProviderId = parsePiAuthProviderId(apiKey);
9
+ return {
10
+ apiKey,
11
+ ...(piAuthProviderId ? { piProviderId: piAuthProviderId } : {}),
12
+ };
13
+ }
14
+ if (!credential) {
15
+ return undefined;
16
+ }
17
+ const apiKey = credential.apiKey.trim();
18
+ if (!apiKey) {
19
+ return undefined;
20
+ }
21
+ const piAuthProviderId = parsePiAuthProviderId(apiKey);
22
+ const piProviderId = piAuthProviderId ?? credential.piProviderId;
23
+ return {
24
+ apiKey,
25
+ ...(piProviderId ? { piProviderId } : {}),
26
+ ...(credential.modelName ? { modelName: credential.modelName } : {}),
27
+ };
28
+ }
29
+ export function hasReviewProviderCredential(credential) {
30
+ return resolveReviewProviderCredential(credential) !== undefined;
31
+ }
32
+ export function getReviewProviderCredential(apiKeys, provider) {
33
+ return resolveReviewProviderCredential(apiKeys[provider]);
34
+ }
35
+ export function createOpenRouterCredential(apiKey) {
36
+ return {
37
+ apiKey,
38
+ piProviderId: 'openrouter',
39
+ };
40
+ }
@@ -2,29 +2,27 @@
2
2
  * Gemini provider for the multi-model code review system.
3
3
  * Uses the Pi harness instead of the Gemini CLI.
4
4
  */
5
- import { DEFAULT_PI_MODELS } from '../core/pi.js';
5
+ import { DEFAULT_OPENROUTER_MODELS, DEFAULT_PI_MODELS } from '../core/pi.js';
6
6
  import { resolveProviderTimeoutMs } from './config.js';
7
7
  import { extractJsonFromResponse } from './helpers.js';
8
8
  import { runPiReviewProvider } from './pi-review.js';
9
9
  export class GeminiProvider {
10
10
  name = 'gemini';
11
- async review(prompt, config, logOptions) {
11
+ async review(prompt, config) {
12
+ const modelName = config.modelName ??
13
+ (config.piProviderId === 'openrouter'
14
+ ? DEFAULT_OPENROUTER_MODELS.gemini
15
+ : DEFAULT_PI_MODELS.gemini) ??
16
+ DEFAULT_PI_MODELS.gemini;
12
17
  return await runPiReviewProvider({
13
18
  provider: this.name,
14
- modelName: config.modelName ?? DEFAULT_PI_MODELS.gemini,
19
+ modelName,
15
20
  prompt,
16
21
  config: {
17
22
  ...config,
18
23
  timeout: resolveProviderTimeoutMs(config.timeout),
19
24
  },
20
- prompts: config.jsonSchema
21
- ? [
22
- prompt,
23
- `${prompt}\n\nReturn only JSON that matches the provided schema. Do not include analysis text.`,
24
- ]
25
- : [prompt],
26
25
  parseOutput: (output) => JSON.parse(extractJsonFromResponse(output)),
27
- logOptions,
28
26
  });
29
27
  }
30
28
  }
@@ -3,8 +3,9 @@
3
3
  */
4
4
  import { randomUUID } from 'crypto';
5
5
  import { logger } from '../core/logging.js';
6
- import { isBatchVoteResponse, isRawReviewResult, isVoteResponse } from './schemas.js';
6
+ import { isRawReviewResult } from './schemas.js';
7
7
  const RAW_MODEL_LOG_CHUNK_CHARS = 8000;
8
+ const REVIEW_LOG_ENGINE = 'multi_focus';
8
9
  function redactSensitiveStrings(content) {
9
10
  return content
10
11
  .replace(/(sk-[A-Za-z0-9_-]{10,})/g, '[REDACTED_OPENAI_KEY]')
@@ -34,7 +35,7 @@ export function buildRawModelOutputEntries(content) {
34
35
  originalLength: content.length,
35
36
  }));
36
37
  }
37
- export function logRawModelOutput({ model, engine = 'council', stage, success, status, durationMs, stdout, stderr, }) {
38
+ export function logRawModelOutput({ model, stage, success, status, durationMs, stdout, stderr, }) {
38
39
  const streams = [
39
40
  { name: 'stdout', value: stdout },
40
41
  { name: 'stderr', value: stderr },
@@ -48,7 +49,7 @@ export function logRawModelOutput({ model, engine = 'council', stage, success, s
48
49
  for (const entry of entries) {
49
50
  logger.info('Review model raw output', {
50
51
  model,
51
- engine,
52
+ engine: REVIEW_LOG_ENGINE,
52
53
  stage,
53
54
  success,
54
55
  status: status ?? null,
@@ -95,30 +96,14 @@ export function parseRawFindings(comments, modelName) {
95
96
  /**
96
97
  * Normalizes structured model output into a single provider response shape.
97
98
  */
98
- export function normalizeParsedModelOutput(parsed, modelName, structuredOutputKind = 'auto') {
99
+ export function normalizeParsedModelOutput(parsed, modelName) {
99
100
  if (isRawReviewResult(parsed)) {
100
101
  return {
101
102
  summary: parsed.summary,
102
103
  findings: parseRawFindings(parsed.comments, modelName),
103
104
  };
104
105
  }
105
- if ((structuredOutputKind === 'auto' || structuredOutputKind === 'vote') &&
106
- isVoteResponse(parsed)) {
107
- return {
108
- summary: '',
109
- findings: [],
110
- structuredOutput: parsed,
111
- };
112
- }
113
- if ((structuredOutputKind === 'auto' || structuredOutputKind === 'batch-vote') &&
114
- isBatchVoteResponse(parsed)) {
115
- return {
116
- summary: '',
117
- findings: [],
118
- structuredOutput: parsed,
119
- };
120
- }
121
- throw new Error('Model output did not match review or vote schema');
106
+ throw new Error('Model output did not match review schema');
122
107
  }
123
108
  function extractFencedJsonCandidates(content) {
124
109
  const matches = content.match(/```json\s*([\s\S]*?)\s*```/gi);
@@ -181,29 +166,24 @@ function extractBalancedJsonCandidates(content) {
181
166
  }
182
167
  return candidates;
183
168
  }
169
+ /**
170
+ * Returns JSON-looking candidates from a response string in parser preference order.
171
+ * Fenced JSON blocks are preferred over balanced objects because models often include
172
+ * code snippets or object-shaped prose before the final fenced response.
173
+ */
174
+ export function extractJsonCandidatesFromResponse(content) {
175
+ return [
176
+ ...extractFencedJsonCandidates(content).reverse(),
177
+ ...extractBalancedJsonCandidates(content).reverse(),
178
+ ];
179
+ }
184
180
  /**
185
181
  * Extracts JSON from a response string that may contain additional text.
186
182
  * Prefers valid fenced JSON blocks, then valid balanced JSON objects.
187
183
  * Throws when no valid JSON candidate can be parsed.
188
184
  */
189
185
  export function extractJsonFromResponse(content) {
190
- const fencedCandidates = extractFencedJsonCandidates(content);
191
- for (let index = fencedCandidates.length - 1; index >= 0; index--) {
192
- const candidate = fencedCandidates[index];
193
- if (!candidate) {
194
- continue;
195
- }
196
- try {
197
- JSON.parse(candidate);
198
- return candidate;
199
- }
200
- catch {
201
- continue;
202
- }
203
- }
204
- const balancedCandidates = extractBalancedJsonCandidates(content);
205
- for (let index = balancedCandidates.length - 1; index >= 0; index--) {
206
- const candidate = balancedCandidates[index];
186
+ for (const candidate of extractJsonCandidatesFromResponse(content)) {
207
187
  if (!candidate) {
208
188
  continue;
209
189
  }
@@ -1,17 +1,18 @@
1
1
  /**
2
2
  * Provider factory and exports for the multi-model code review system.
3
3
  */
4
- import { AnthropicProvider } from './anthropic.js';
5
4
  import { GeminiProvider } from './gemini.js';
6
5
  import { OpenAIProvider } from './openai.js';
7
- export { AnthropicProvider } from './anthropic.js';
6
+ import { OpenRouterPiProvider } from './openrouter-pi.js';
8
7
  export { GeminiProvider } from './gemini.js';
9
8
  export { OpenAIProvider } from './openai.js';
9
+ export { OpenRouterPiProvider } from './openrouter-pi.js';
10
10
  export * from './types.js';
11
11
  const providers = {
12
+ deepseek: new OpenRouterPiProvider('deepseek'),
12
13
  gemini: new GeminiProvider(),
14
+ zai: new OpenRouterPiProvider('zai'),
13
15
  openai: new OpenAIProvider(),
14
- anthropic: new AnthropicProvider(),
15
16
  };
16
17
  /**
17
18
  * Gets a provider instance by name.
@@ -8,7 +8,7 @@ import { extractJsonFromResponse } from './helpers.js';
8
8
  import { runPiReviewProvider } from './pi-review.js';
9
9
  export class OpenAIProvider {
10
10
  name = 'openai';
11
- async review(prompt, config, logOptions) {
11
+ async review(prompt, config) {
12
12
  return await runPiReviewProvider({
13
13
  provider: this.name,
14
14
  modelName: config.modelName ?? DEFAULT_PI_MODELS.openai,
@@ -17,14 +17,7 @@ export class OpenAIProvider {
17
17
  ...config,
18
18
  timeout: resolveProviderTimeoutMs(config.timeout),
19
19
  },
20
- prompts: config.jsonSchema
21
- ? [
22
- prompt,
23
- `${prompt}\n\nReturn only JSON that matches the provided schema. Do not include analysis text.`,
24
- ]
25
- : [prompt],
26
20
  parseOutput: (output) => JSON.parse(extractJsonFromResponse(output)),
27
- logOptions,
28
21
  });
29
22
  }
30
23
  }
@@ -0,0 +1,25 @@
1
+ import { DEFAULT_OPENROUTER_MODELS, DEFAULT_PI_MODELS } from '../core/pi.js';
2
+ import { resolveProviderTimeoutMs } from './config.js';
3
+ import { extractJsonFromResponse } from './helpers.js';
4
+ import { runPiReviewProvider } from './pi-review.js';
5
+ export class OpenRouterPiProvider {
6
+ name;
7
+ fallbackModelName;
8
+ constructor(name, fallbackModelName = DEFAULT_OPENROUTER_MODELS[name] ??
9
+ DEFAULT_PI_MODELS[name]) {
10
+ this.name = name;
11
+ this.fallbackModelName = fallbackModelName;
12
+ }
13
+ async review(prompt, config) {
14
+ return await runPiReviewProvider({
15
+ provider: this.name,
16
+ modelName: config.modelName ?? this.fallbackModelName,
17
+ prompt,
18
+ config: {
19
+ ...config,
20
+ timeout: resolveProviderTimeoutMs(config.timeout),
21
+ },
22
+ parseOutput: (output) => JSON.parse(extractJsonFromResponse(output)),
23
+ });
24
+ }
25
+ }
@@ -1,22 +1,12 @@
1
1
  import { logger } from '../core/logging.js';
2
- import { createPiInvoker } from '../core/pi.js';
2
+ import { createPiInvoker, parsePiAuthProviderId, PI_PROVIDER_BY_DOISTBOT_PROVIDER, } from '../core/pi.js';
3
3
  import { logPiProgressEvent } from '../core/pi-progress-logging.js';
4
4
  import { WORKSPACE_DIR } from '../core/shared.js';
5
5
  import { logRawModelOutput, normalizeParsedModelOutput } from './helpers.js';
6
- import { BATCH_VOTE_SCHEMA, VOTE_SCHEMA } from './schemas.js';
7
6
  const VERBOSE_PI_PROGRESS_LOGS = process.env.DOISTBOT_VERBOSE_PI_PROGRESS === 'true';
8
7
  const VERBOSE_PI_RAW_OUTPUT_LOGS = process.env.DOISTBOT_VERBOSE_PI_RAW_OUTPUT === 'true';
8
+ const REVIEW_LOG_ENGINE = 'multi_focus';
9
9
  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
- const DEFAULT_REVIEW_LOG_ENGINE = 'council';
11
- function getStructuredOutputKind(jsonSchema) {
12
- if (jsonSchema === BATCH_VOTE_SCHEMA) {
13
- return 'batch-vote';
14
- }
15
- if (jsonSchema === VOTE_SCHEMA) {
16
- return 'vote';
17
- }
18
- return 'auto';
19
- }
20
10
  function buildModelFailureResult({ model, error, durationMs, timedOut = false, }) {
21
11
  return {
22
12
  model,
@@ -27,7 +17,7 @@ function buildModelFailureResult({ model, error, durationMs, timedOut = false, }
27
17
  durationMs,
28
18
  };
29
19
  }
30
- function logReviewModelProgress(model, event, engine) {
20
+ function logReviewModelProgress(model, event) {
31
21
  logPiProgressEvent({
32
22
  event,
33
23
  messages: {
@@ -39,17 +29,17 @@ function logReviewModelProgress(model, event, engine) {
39
29
  toolUpdate: 'Review model tool still running',
40
30
  toolEnd: 'Review model tool finished',
41
31
  },
42
- meta: { model, engine },
32
+ meta: { model, engine: REVIEW_LOG_ENGINE },
43
33
  logResponding: VERBOSE_PI_PROGRESS_LOGS,
44
34
  logTools: VERBOSE_PI_PROGRESS_LOGS,
45
35
  });
46
36
  }
47
- function emitReviewModelProgress(provider, event, config, engine) {
37
+ function emitReviewModelProgress(provider, event, config) {
48
38
  config.onProgress?.({
49
39
  ...event,
50
40
  provider,
51
41
  });
52
- logReviewModelProgress(provider, event, engine);
42
+ logReviewModelProgress(provider, event);
53
43
  }
54
44
  function logReviewRawModelOutput(args) {
55
45
  if (!VERBOSE_PI_RAW_OUTPUT_LOGS) {
@@ -57,31 +47,42 @@ function logReviewRawModelOutput(args) {
57
47
  }
58
48
  logRawModelOutput(args);
59
49
  }
60
- export async function runPiReviewProvider({ provider, modelName, prompt, config, prompts = [prompt], parseOutput, logOptions, }) {
50
+ function isOpenRouterBackedReviewRequest(provider, config) {
51
+ const resolvedPiProviderId = parsePiAuthProviderId(config.apiKey) ??
52
+ config.piProviderId ??
53
+ PI_PROVIDER_BY_DOISTBOT_PROVIDER[provider];
54
+ return resolvedPiProviderId === 'openrouter';
55
+ }
56
+ export async function runPiReviewProvider({ provider, modelName, prompt, config, prompts = [prompt], parseOutput, }) {
61
57
  const startTime = Date.now();
62
58
  const timeoutMs = config.timeout ?? 0;
63
- const engine = logOptions?.engine ?? DEFAULT_REVIEW_LOG_ENGINE;
64
59
  let stdout = '';
65
60
  let stderr = '';
66
61
  let status = null;
67
62
  logger.info('Review model request', {
68
63
  model: provider,
69
- engine,
64
+ engine: REVIEW_LOG_ENGINE,
70
65
  promptLength: prompt.length,
71
66
  modelName,
72
67
  timeoutMs,
68
+ ...(isOpenRouterBackedReviewRequest(provider, config) && config.openRouterSessionId
69
+ ? { openRouterSessionId: config.openRouterSessionId }
70
+ : {}),
73
71
  });
74
72
  try {
75
73
  const invoker = createPiInvoker({
76
74
  provider,
77
75
  apiKey: config.apiKey,
76
+ piProviderId: config.piProviderId,
78
77
  cwd: config.workspaceDir ?? WORKSPACE_DIR,
79
78
  onProgress: (event) => {
80
- emitReviewModelProgress(provider, event, config, engine);
79
+ emitReviewModelProgress(provider, event, config);
81
80
  },
82
81
  capabilityPreset: 'read-only',
83
82
  thinkingLevel: config.thinkingLevel,
84
83
  systemPrompt: REVIEW_PI_SYSTEM_PROMPT,
84
+ noContextFiles: true,
85
+ openRouterSessionId: config.openRouterSessionId,
85
86
  });
86
87
  for (let attempt = 0; attempt < prompts.length; attempt += 1) {
87
88
  const result = await invoker.invoke({
@@ -95,7 +96,7 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
95
96
  status = result.ok ? 0 : null;
96
97
  logger.info('Review model tool usage', {
97
98
  model: provider,
98
- engine,
99
+ engine: REVIEW_LOG_ENGINE,
99
100
  attempt: attempt + 1,
100
101
  attemptsTotal: prompts.length,
101
102
  attemptDurationMs: result.durationMs,
@@ -104,7 +105,6 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
104
105
  if (!result.ok && result.timedOut) {
105
106
  logReviewRawModelOutput({
106
107
  model: provider,
107
- engine,
108
108
  stage: 'timeout',
109
109
  success: false,
110
110
  status,
@@ -115,7 +115,7 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
115
115
  const error = result.error || `Pi ${provider} request timed out after ${timeoutMs}ms`;
116
116
  logger.error('Review model failed', {
117
117
  model: provider,
118
- engine,
118
+ engine: REVIEW_LOG_ENGINE,
119
119
  error,
120
120
  durationMs,
121
121
  });
@@ -129,7 +129,6 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
129
129
  if (!result.ok) {
130
130
  logReviewRawModelOutput({
131
131
  model: provider,
132
- engine,
133
132
  stage: 'non-zero-exit',
134
133
  success: false,
135
134
  status,
@@ -140,7 +139,7 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
140
139
  const error = `Pi ${provider} request failed: ${result.error.slice(0, 500)}`;
141
140
  logger.error('Review model failed', {
142
141
  model: provider,
143
- engine,
142
+ engine: REVIEW_LOG_ENGINE,
144
143
  error,
145
144
  durationMs,
146
145
  });
@@ -154,7 +153,6 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
154
153
  if (!output.trim()) {
155
154
  logReviewRawModelOutput({
156
155
  model: provider,
157
- engine,
158
156
  stage: 'empty-output',
159
157
  success: false,
160
158
  status,
@@ -169,10 +167,10 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
169
167
  });
170
168
  }
171
169
  try {
172
- const normalized = normalizeParsedModelOutput(parseOutput(output), provider, getStructuredOutputKind(config.jsonSchema));
170
+ const normalized = normalizeParsedModelOutput(parseOutput(output), provider);
173
171
  logger.info('Review model response', {
174
172
  model: provider,
175
- engine,
173
+ engine: REVIEW_LOG_ENGINE,
176
174
  findingsCount: normalized.findings.length,
177
175
  durationMs,
178
176
  parseMethod: 'extract_json',
@@ -180,13 +178,12 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
180
178
  if (VERBOSE_PI_RAW_OUTPUT_LOGS) {
181
179
  logger.info('Review model findings', {
182
180
  model: provider,
183
- engine,
181
+ engine: REVIEW_LOG_ENGINE,
184
182
  findings: normalized.findings,
185
183
  });
186
184
  }
187
185
  logReviewRawModelOutput({
188
186
  model: provider,
189
- engine,
190
187
  stage: 'success',
191
188
  success: true,
192
189
  status,
@@ -198,35 +195,12 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
198
195
  model: provider,
199
196
  summary: normalized.summary,
200
197
  findings: normalized.findings,
201
- structuredOutput: normalized.structuredOutput,
202
198
  durationMs,
203
199
  };
204
200
  }
205
201
  catch (error) {
206
- if (config.jsonSchema && attempt < prompts.length - 1) {
207
- const retryError = error instanceof Error ? error.message : String(error);
208
- logReviewRawModelOutput({
209
- model: provider,
210
- engine,
211
- stage: 'schema-retry',
212
- success: false,
213
- status,
214
- durationMs,
215
- stdout,
216
- stderr,
217
- });
218
- logger.warn('Review model schema retry', {
219
- model: provider,
220
- engine,
221
- durationMs,
222
- attempt: attempt + 1,
223
- error: retryError,
224
- });
225
- continue;
226
- }
227
202
  logReviewRawModelOutput({
228
203
  model: provider,
229
- engine,
230
204
  stage: 'parse-error',
231
205
  success: false,
232
206
  status,
@@ -248,7 +222,6 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
248
222
  const errorMessage = error instanceof Error ? error.message : String(error);
249
223
  logReviewRawModelOutput({
250
224
  model: provider,
251
- engine,
252
225
  stage: 'exception',
253
226
  success: false,
254
227
  status,
@@ -258,7 +231,7 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
258
231
  });
259
232
  logger.error('Review model failed', {
260
233
  model: provider,
261
- engine,
234
+ engine: REVIEW_LOG_ENGINE,
262
235
  error: errorMessage,
263
236
  durationMs,
264
237
  });
@@ -21,37 +21,6 @@ export const REVIEW_SCHEMA = {
21
21
  },
22
22
  required: ['summary', 'comments'],
23
23
  };
24
- export const VOTE_SCHEMA = {
25
- type: 'object',
26
- additionalProperties: false,
27
- properties: {
28
- approve: { type: 'boolean' },
29
- confidence: { type: 'number', minimum: 0, maximum: 1 },
30
- reasoning: { type: 'string', minLength: 1 },
31
- },
32
- required: ['approve', 'confidence', 'reasoning'],
33
- };
34
- export const BATCH_VOTE_SCHEMA = {
35
- type: 'object',
36
- additionalProperties: false,
37
- properties: {
38
- votes: {
39
- type: 'array',
40
- items: {
41
- type: 'object',
42
- additionalProperties: false,
43
- properties: {
44
- findingId: { type: 'string', minLength: 1 },
45
- approve: { type: 'boolean' },
46
- confidence: { type: 'number', minimum: 0, maximum: 1 },
47
- reasoning: { type: 'string', minLength: 1 },
48
- },
49
- required: ['findingId', 'approve', 'confidence', 'reasoning'],
50
- },
51
- },
52
- },
53
- required: ['votes'],
54
- };
55
24
  export const DEDUPE_SCHEMA = {
56
25
  type: 'object',
57
26
  additionalProperties: false,
@@ -76,18 +45,10 @@ export const DEDUPE_SCHEMA = {
76
45
  };
77
46
  const ajv = new Ajv();
78
47
  const validateRawReviewResult = ajv.compile(REVIEW_SCHEMA);
79
- const validateVoteResponse = ajv.compile(VOTE_SCHEMA);
80
- const validateBatchVoteResponse = ajv.compile(BATCH_VOTE_SCHEMA);
81
48
  const validateDedupeResponse = ajv.compile(DEDUPE_SCHEMA);
82
49
  export function isRawReviewResult(value) {
83
50
  return validateRawReviewResult(value);
84
51
  }
85
- export function isVoteResponse(value) {
86
- return validateVoteResponse(value);
87
- }
88
- export function isBatchVoteResponse(value) {
89
- return validateBatchVoteResponse(value);
90
- }
91
52
  export function isDedupeResponse(value) {
92
53
  return validateDedupeResponse(value);
93
54
  }