@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,15 +1,57 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import { fileURLToPath } from 'url';
3
4
  import { logger } from './logging.js';
5
+ const promptCache = new Map();
6
+ const resolveCache = new Map();
7
+ /** Swap the `dist` directory nearest to the file (not just the first `/dist/` in the path) for `src`. */
8
+ function distDirToSrcDir(dir) {
9
+ const distSeg = `${path.sep}dist${path.sep}`;
10
+ const idx = dir.lastIndexOf(distSeg);
11
+ if (idx === -1) {
12
+ return dir;
13
+ }
14
+ return `${dir.slice(0, idx)}${path.sep}src${path.sep}${dir.slice(idx + distSeg.length)}`;
15
+ }
16
+ /**
17
+ * Resolve a prompt file living in `<module>/prompts/`, relative to the calling
18
+ * module. At runtime the module lives under `dist/`, but the prompts are shipped
19
+ * under `src/`, so we map `dist`->`src`. The `dist` candidate is tried first so a
20
+ * future build step that copies prompts into `dist/` works without code changes.
21
+ */
22
+ export function resolvePromptFile(moduleUrl, fileName) {
23
+ const byFileName = resolveCache.get(moduleUrl) ?? new Map();
24
+ resolveCache.set(moduleUrl, byFileName);
25
+ const cached = byFileName.get(fileName);
26
+ if (cached !== undefined) {
27
+ return cached;
28
+ }
29
+ const dir = path.dirname(fileURLToPath(moduleUrl));
30
+ const candidates = [
31
+ path.join(dir, 'prompts', fileName),
32
+ path.join(distDirToSrcDir(dir), 'prompts', fileName),
33
+ ];
34
+ const resolved = candidates.find(fs.existsSync) ?? candidates[candidates.length - 1];
35
+ byFileName.set(fileName, resolved);
36
+ return resolved;
37
+ }
4
38
  export function loadPromptTemplateFile({ fileName, promptLabel, }) {
5
- const promptPath = path.resolve(fileName);
39
+ if (!path.isAbsolute(fileName)) {
40
+ throw new Error(`${promptLabel} prompt template path must be absolute (resolve it with resolvePromptFile first), got: ${fileName}`);
41
+ }
42
+ const cached = promptCache.get(fileName);
43
+ if (cached !== undefined) {
44
+ return cached;
45
+ }
6
46
  try {
7
- return fs.readFileSync(promptPath, 'utf8');
47
+ const contents = fs.readFileSync(fileName, 'utf8');
48
+ promptCache.set(fileName, contents);
49
+ return contents;
8
50
  }
9
51
  catch (error) {
10
52
  logger.error(`Failed to load ${promptLabel} prompt template`, {
11
53
  error: error instanceof Error ? error.message : String(error),
12
- promptPath,
54
+ promptPath: fileName,
13
55
  });
14
56
  throw error;
15
57
  }
@@ -64,6 +64,16 @@ const TODOIST_REPOSITORIES = [
64
64
  product: 'Todoist',
65
65
  surfaces: ['android', 'ios'],
66
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
+ }),
67
77
  ];
68
78
  const TWIST_REPOSITORIES = [
69
79
  createRepositoryTarget({
@@ -126,6 +136,16 @@ const TWIST_REPOSITORIES = [
126
136
  product: 'Twist',
127
137
  surfaces: ['desktop'],
128
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
+ }),
129
149
  ];
130
150
  function dedupeRepositories(repositories) {
131
151
  const unique = new Map();
@@ -145,8 +165,10 @@ export const APP_TO_REPO_SELECTORS = {
145
165
  all: (repositories) => dedupeRepositories(repositories),
146
166
  android: (repositories) => selectBySurface(repositories, ['android']),
147
167
  backend: (repositories) => selectBySurface(repositories, ['backend']),
168
+ cli: (repositories) => selectBySurface(repositories, ['cli']),
148
169
  desktop: (repositories) => selectBySurface(repositories, ['desktop']),
149
170
  ios: (repositories) => selectBySurface(repositories, ['ios']),
171
+ mcp: (repositories) => selectBySurface(repositories, ['mcp']),
150
172
  web: (repositories) => selectBySurface(repositories, ['web']),
151
173
  };
152
174
  export const TEAM_TO_REPO_SELECTORS = {
@@ -179,8 +201,10 @@ export const APP_LABEL_ALIASES = {
179
201
  all: 'all',
180
202
  android: 'android',
181
203
  backend: 'backend',
204
+ cli: 'cli',
182
205
  desktop: 'desktop',
183
206
  ios: 'ios',
207
+ mcp: 'mcp',
184
208
  web: 'web',
185
209
  };
186
210
  export const TEAM_LABEL_ALIASES = {
@@ -0,0 +1,15 @@
1
+ import { context, trace } from '@opentelemetry/api';
2
+ import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';
3
+ import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base';
4
+ import { LoggerSpanProcessor } from './logger-span-processor.js';
5
+ let registered = false;
6
+ export function setupTracing() {
7
+ if (registered)
8
+ return;
9
+ registered = true;
10
+ context.setGlobalContextManager(new AsyncLocalStorageContextManager());
11
+ const provider = new BasicTracerProvider({
12
+ spanProcessors: [new LoggerSpanProcessor()],
13
+ });
14
+ trace.setGlobalTracerProvider(provider);
15
+ }
@@ -0,0 +1,80 @@
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
+ }
72
+ /**
73
+ * Maps a doistbot provider key to the OTel `gen_ai.provider.name` value
74
+ * (docs/sandbox-instrumentation.md). Only `gemini` has a standard name
75
+ * (`gcp.gemini`); the OpenRouter-routed keys (`zai`, `deepseek`) have none, so
76
+ * they pass through. Single source so every span site stays consistent.
77
+ */
78
+ export function genAiProviderName(provider) {
79
+ return provider === 'gemini' ? 'gcp.gemini' : provider;
80
+ }
@@ -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 {};
@@ -1,7 +1,15 @@
1
+ import { parsePiAuthProviderId } from '../core/pi.js';
1
2
  export function resolveReviewProviderCredential(credential) {
2
3
  if (typeof credential === 'string') {
3
4
  const apiKey = credential.trim();
4
- return apiKey ? { apiKey } : undefined;
5
+ if (!apiKey) {
6
+ return undefined;
7
+ }
8
+ const piAuthProviderId = parsePiAuthProviderId(apiKey);
9
+ return {
10
+ apiKey,
11
+ ...(piAuthProviderId ? { piProviderId: piAuthProviderId } : {}),
12
+ };
5
13
  }
6
14
  if (!credential) {
7
15
  return undefined;
@@ -10,9 +18,11 @@ export function resolveReviewProviderCredential(credential) {
10
18
  if (!apiKey) {
11
19
  return undefined;
12
20
  }
21
+ const piAuthProviderId = parsePiAuthProviderId(apiKey);
22
+ const piProviderId = piAuthProviderId ?? credential.piProviderId;
13
23
  return {
14
24
  apiKey,
15
- ...(credential.piProviderId ? { piProviderId: credential.piProviderId } : {}),
25
+ ...(piProviderId ? { piProviderId } : {}),
16
26
  ...(credential.modelName ? { modelName: credential.modelName } : {}),
17
27
  };
18
28
  }
@@ -9,7 +9,11 @@ import { runPiReviewProvider } from './pi-review.js';
9
9
  export class GeminiProvider {
10
10
  name = 'gemini';
11
11
  async review(prompt, config) {
12
- const modelName = config.modelName ?? DEFAULT_OPENROUTER_MODELS.gemini ?? DEFAULT_PI_MODELS.gemini;
12
+ const modelName = config.modelName ??
13
+ (config.piProviderId === 'openrouter'
14
+ ? DEFAULT_OPENROUTER_MODELS.gemini
15
+ : DEFAULT_PI_MODELS.gemini) ??
16
+ DEFAULT_PI_MODELS.gemini;
13
17
  return await runPiReviewProvider({
14
18
  provider: this.name,
15
19
  modelName,
@@ -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]')
@@ -166,29 +217,24 @@ function extractBalancedJsonCandidates(content) {
166
217
  }
167
218
  return candidates;
168
219
  }
220
+ /**
221
+ * Returns JSON-looking candidates from a response string in parser preference order.
222
+ * Fenced JSON blocks are preferred over balanced objects because models often include
223
+ * code snippets or object-shaped prose before the final fenced response.
224
+ */
225
+ export function extractJsonCandidatesFromResponse(content) {
226
+ return [
227
+ ...extractFencedJsonCandidates(content).reverse(),
228
+ ...extractBalancedJsonCandidates(content).reverse(),
229
+ ];
230
+ }
169
231
  /**
170
232
  * Extracts JSON from a response string that may contain additional text.
171
233
  * Prefers valid fenced JSON blocks, then valid balanced JSON objects.
172
234
  * Throws when no valid JSON candidate can be parsed.
173
235
  */
174
236
  export function extractJsonFromResponse(content) {
175
- const fencedCandidates = extractFencedJsonCandidates(content);
176
- for (let index = fencedCandidates.length - 1; index >= 0; index--) {
177
- const candidate = fencedCandidates[index];
178
- if (!candidate) {
179
- continue;
180
- }
181
- try {
182
- JSON.parse(candidate);
183
- return candidate;
184
- }
185
- catch {
186
- continue;
187
- }
188
- }
189
- const balancedCandidates = extractBalancedJsonCandidates(content);
190
- for (let index = balancedCandidates.length - 1; index >= 0; index--) {
191
- const candidate = balancedCandidates[index];
237
+ for (const candidate of extractJsonCandidatesFromResponse(content)) {
192
238
  if (!candidate) {
193
239
  continue;
194
240
  }
@@ -3,11 +3,15 @@
3
3
  */
4
4
  import { GeminiProvider } from './gemini.js';
5
5
  import { OpenAIProvider } from './openai.js';
6
+ import { OpenRouterPiProvider } from './openrouter-pi.js';
6
7
  export { GeminiProvider } from './gemini.js';
7
8
  export { OpenAIProvider } from './openai.js';
9
+ export { OpenRouterPiProvider } from './openrouter-pi.js';
8
10
  export * from './types.js';
9
11
  const providers = {
12
+ deepseek: new OpenRouterPiProvider('deepseek'),
10
13
  gemini: new GeminiProvider(),
14
+ zai: new OpenRouterPiProvider('zai'),
11
15
  openai: new OpenAIProvider(),
12
16
  };
13
17
  /**
@@ -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,5 +1,5 @@
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';
@@ -47,7 +47,19 @@ function logReviewRawModelOutput(args) {
47
47
  }
48
48
  logRawModelOutput(args);
49
49
  }
50
- export async function runPiReviewProvider({ provider, modelName, prompt, config, prompts = [prompt], parseOutput, }) {
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(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, }) {
51
63
  const startTime = Date.now();
52
64
  const timeoutMs = config.timeout ?? 0;
53
65
  let stdout = '';
@@ -59,6 +71,9 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
59
71
  promptLength: prompt.length,
60
72
  modelName,
61
73
  timeoutMs,
74
+ ...(isOpenRouterBackedReviewRequest(provider, config) && config.openRouterSessionId
75
+ ? { openRouterSessionId: config.openRouterSessionId }
76
+ : {}),
62
77
  });
63
78
  try {
64
79
  const invoker = createPiInvoker({
@@ -72,6 +87,8 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
72
87
  capabilityPreset: 'read-only',
73
88
  thinkingLevel: config.thinkingLevel,
74
89
  systemPrompt: REVIEW_PI_SYSTEM_PROMPT,
90
+ noContextFiles: true,
91
+ openRouterSessionId: config.openRouterSessionId,
75
92
  });
76
93
  for (let attempt = 0; attempt < prompts.length; attempt += 1) {
77
94
  const result = await invoker.invoke({