@doist/doistbot-cli 1.0.8 → 1.0.10

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 (40) hide show
  1. package/dist/actions/auth.js +6 -4
  2. package/dist/actions/review.js +1 -0
  3. package/dist/auth.js +67 -35
  4. package/dist/config.js +6 -3
  5. package/dist/input.js +11 -11
  6. package/dist/terminal.js +1 -0
  7. package/package.json +5 -4
  8. package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +1 -1
  9. package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +1 -1
  10. package/sandbox/dist/core/datadog-metrics.js +2 -0
  11. package/sandbox/dist/core/pi.js +13 -3
  12. package/sandbox/dist/core/shared.js +9 -9
  13. package/sandbox/dist/core/span-test-helpers.js +13 -0
  14. package/sandbox/dist/core/tracing.js +1 -1
  15. package/sandbox/dist/main.js +3 -0
  16. package/sandbox/dist/providers/index.js +1 -0
  17. package/sandbox/dist/tasks/issue-summarize/model.js +4 -4
  18. package/sandbox/dist/tasks/issue-triage/autofix-capabilities.js +22 -0
  19. package/sandbox/dist/tasks/issue-triage/fix-attempt.js +6 -0
  20. package/sandbox/dist/tasks/issue-triage/routing.js +124 -15
  21. package/sandbox/dist/tasks/issue-triage/triage.js +2 -0
  22. package/sandbox/dist/tasks/review/engines/multi-focus.js +23 -15
  23. package/sandbox/dist/tasks/review/engines/shared.js +2 -0
  24. package/sandbox/dist/tasks/review/multi-focus-prompt.js +2 -0
  25. package/sandbox/dist/tasks/review/review-summary.js +2 -2
  26. package/sandbox/dist/tasks/review/review.js +5 -3
  27. package/sandbox/dist/tasks/review/summary-model.js +5 -4
  28. package/sandbox/dist/tasks/review/thinking-level.js +2 -0
  29. package/sandbox/dist/tasks/review-slice-plan/context.js +69 -0
  30. package/sandbox/dist/tasks/review-slice-plan/plan.js +209 -0
  31. package/sandbox/dist/tasks/review-slice-plan/prompt.js +59 -0
  32. package/sandbox/dist/tasks/review-slice-plan/review-slice-plan.js +470 -0
  33. package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +1 -1
  34. package/sandbox/node_modules/doistbot-repo-config/dist/index.js +1 -1
  35. package/sandbox/src/tasks/review/prompts/review-focus-prompts/efficiency.md +10 -12
  36. package/sandbox/src/tasks/review/prompts/review-focus-prompts/general.md +24 -7
  37. package/sandbox/src/tasks/review/prompts/review-focus-prompts/quality.md +14 -22
  38. package/sandbox/src/tasks/review/prompts/review-focus-prompts/reuse.md +13 -0
  39. package/sandbox/src/tasks/review/prompts/review-focus-prompts/security.md +18 -0
  40. package/sandbox/src/tasks/review/prompts/review-focus-prompts/tests.md +12 -22
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import { deleteGeminiApiKey, deleteOpenRouterApiKey, getAuthStatus, loginOpenAICodex, logoutOpenAICodex, saveGeminiApiKey, saveOpenRouterApiKey, } from '../auth.js';
2
+ import { createPiAuthRuntime, deleteGeminiApiKey, deleteOpenRouterApiKey, getAuthStatus, loginOpenAICodex, logoutOpenAICodex, saveGeminiApiKey, saveOpenRouterApiKey, } from '../auth.js';
3
3
  import { getEnvApiKeys } from '../env.js';
4
4
  import { FILE_STORE_PATH } from '../file-store.js';
5
5
  import { promptHiddenInput, promptMultiSelect } from '../input.js';
@@ -58,6 +58,7 @@ function formatSkipInstruction() {
58
58
  }
59
59
  export async function runAuthSetup(dependencies = {}) {
60
60
  const deps = {
61
+ createPiAuthRuntime,
61
62
  getAuthStatus,
62
63
  isInteractive: process.stdin.isTTY === true,
63
64
  loginOpenAICodex,
@@ -67,7 +68,8 @@ export async function runAuthSetup(dependencies = {}) {
67
68
  writeLine,
68
69
  ...dependencies,
69
70
  };
70
- const status = await deps.getAuthStatus();
71
+ const runtime = await deps.createPiAuthRuntime();
72
+ const status = await deps.getAuthStatus({ runtime });
71
73
  deps.writeLine('1. OpenAI Codex');
72
74
  deps.writeLine();
73
75
  if (status.openai.configured && !status.openai.refreshFailed) {
@@ -80,7 +82,7 @@ export async function runAuthSetup(dependencies = {}) {
80
82
  if (status.openai.refreshFailed) {
81
83
  deps.writeLine('OpenAI session could not be refreshed (expired, revoked, or offline). Reconnecting.');
82
84
  }
83
- await deps.loginOpenAICodex();
85
+ await deps.loginOpenAICodex(runtime);
84
86
  deps.writeLine();
85
87
  deps.writeLine('✓ OpenAI configured.');
86
88
  }
@@ -136,7 +138,7 @@ export async function runAuthSetup(dependencies = {}) {
136
138
  deps.writeLine('OpenRouter not configured. Skipping optional OpenRouter provider.');
137
139
  return;
138
140
  }
139
- deps.writeLine(`Doistbot can use OpenRouter for DeepSeek, Z.ai, and other open models. Paste an OpenRouter API key, or ${formatSkipInstruction()}.`);
141
+ deps.writeLine(`Doistbot can use OpenRouter for DeepSeek, Grok, and other open models. Paste an OpenRouter API key, or ${formatSkipInstruction()}.`);
140
142
  const openRouterApiKey = (await deps.promptHiddenInput(API_KEY_PROMPT)).trim();
141
143
  if (!openRouterApiKey) {
142
144
  deps.writeLine();
@@ -6,6 +6,7 @@ const JSON_SCHEMA_VERSION = 1;
6
6
  const PROVIDER_DISPLAY_NAMES = {
7
7
  deepseek: 'DeepSeek',
8
8
  gemini: 'Gemini',
9
+ grok: 'Grok',
9
10
  zai: 'Z.ai',
10
11
  openai: 'OpenAI',
11
12
  };
package/dist/auth.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import { spawn } from 'child_process';
2
2
  import { homedir } from 'os';
3
3
  import path from 'path';
4
+ import {} from '@earendil-works/pi-ai';
4
5
  import { getEnvApiKeys } from './env.js';
5
6
  import { createFileStore } from './file-store.js';
6
7
  import { promptInput } from './input.js';
7
8
  import { createCompositeStore, createKeyringStore, SecureStoreUnavailableError, } from './secure-store.js';
9
+ import { writeLine } from './terminal.js';
8
10
  const GEMINI_ACCOUNT_NAME = 'gemini-api-key';
9
11
  const OPENROUTER_ACCOUNT_NAME = 'openrouter-api-key';
10
12
  export function createGeminiKeyStore() {
@@ -15,9 +17,20 @@ export function createOpenRouterKeyStore() {
15
17
  }
16
18
  export const OPENAI_CODEX_PROVIDER_ID = 'openai-codex';
17
19
  export const PI_AUTH_API_KEY = `pi-auth://${OPENAI_CODEX_PROVIDER_ID}`;
18
- export async function createPiAuthStorage() {
19
- const { AuthStorage } = await import('@earendil-works/pi-coding-agent');
20
- return AuthStorage.create(path.join(homedir(), '.pi', 'agent', 'auth.json'));
20
+ const OPENAI_CODEX_BROWSER_LOGIN_METHOD = 'browser';
21
+ export async function createPiAuthRuntime() {
22
+ // Pi's own CLI installs its npm Undici implementation before initializing
23
+ // ModelRuntime. SDK consumers must do the same on Node 26 so compressed OAuth
24
+ // responses are decoded before response.json() parses them.
25
+ const { install } = await import('undici');
26
+ install();
27
+ const { ModelRuntime } = await import('@earendil-works/pi-coding-agent');
28
+ return await ModelRuntime.create({
29
+ authPath: path.join(homedir(), '.pi', 'agent', 'auth.json'),
30
+ allowModelNetwork: false,
31
+ modelsPath: null,
32
+ refreshOnCreate: false,
33
+ });
21
34
  }
22
35
  export function getBrowserOpenCommand(platform, url) {
23
36
  if (platform === 'darwin') {
@@ -30,33 +43,38 @@ export function getBrowserOpenCommand(platform, url) {
30
43
  }
31
44
  /**
32
45
  * Check that the stored OAuth session is usable, not just present.
33
- * getApiKey returns the cached access token when unexpired; when expired it
34
- * attempts one cross-process-locked refresh, persists the result, and returns
35
- * undefined when the refresh fails (revoked session, no network) while
36
- * preserving the stored credentials. Unexpected storage errors propagate.
46
+ * The Pi model runtime returns the cached access token when unexpired; when
47
+ * expired it attempts one cross-process-locked refresh and persists the result.
48
+ * Refresh failures preserve the stored credentials. Unexpected storage errors
49
+ * propagate.
37
50
  */
38
- export async function getOpenAICodexAuthState(storage) {
39
- const resolvedStorage = storage ?? (await createPiAuthStorage());
40
- if (resolvedStorage.get(OPENAI_CODEX_PROVIDER_ID)?.type !== 'oauth') {
51
+ export async function getOpenAICodexAuthState(runtime) {
52
+ const resolvedRuntime = runtime ?? (await createPiAuthRuntime());
53
+ const credentials = await resolvedRuntime.listCredentials();
54
+ if (!credentials.some(({ providerId, type }) => providerId === OPENAI_CODEX_PROVIDER_ID && type === 'oauth')) {
41
55
  return 'missing';
42
56
  }
43
- const apiKey = await resolvedStorage.getApiKey(OPENAI_CODEX_PROVIDER_ID, {
44
- includeFallback: false,
45
- });
46
- return apiKey ? 'ready' : 'refresh-failed';
47
- }
48
- export async function loginOpenAICodex(storage) {
49
- const resolvedStorage = storage ?? (await createPiAuthStorage());
50
- const { loginOpenAICodex: loginInBrowser } = await import('@earendil-works/pi-ai/oauth');
51
- const credentials = await loginInBrowser(createOpenAICodexBrowserLoginOptions());
52
- saveOpenAICodexCredentials(resolvedStorage, credentials);
57
+ try {
58
+ const auth = await resolvedRuntime.getAuth(OPENAI_CODEX_PROVIDER_ID);
59
+ return auth?.auth.apiKey ? 'ready' : 'refresh-failed';
60
+ }
61
+ catch (error) {
62
+ if (typeof error === 'object' &&
63
+ error !== null &&
64
+ 'code' in error &&
65
+ error.code === 'oauth') {
66
+ return 'refresh-failed';
67
+ }
68
+ throw error;
69
+ }
53
70
  }
54
- export function saveOpenAICodexCredentials(storage, credentials) {
55
- storage.set(OPENAI_CODEX_PROVIDER_ID, { type: 'oauth', ...credentials });
71
+ export async function loginOpenAICodex(runtime) {
72
+ const resolvedRuntime = runtime ?? (await createPiAuthRuntime());
73
+ await resolvedRuntime.login(OPENAI_CODEX_PROVIDER_ID, 'oauth', createOpenAICodexBrowserLoginInteraction());
56
74
  }
57
- export async function logoutOpenAICodex(storage) {
58
- const resolvedStorage = storage ?? (await createPiAuthStorage());
59
- resolvedStorage.logout(OPENAI_CODEX_PROVIDER_ID);
75
+ export async function logoutOpenAICodex(runtime) {
76
+ const resolvedRuntime = runtime ?? (await createPiAuthRuntime());
77
+ await resolvedRuntime.logout(OPENAI_CODEX_PROVIDER_ID);
60
78
  }
61
79
  export async function readStoredGeminiApiKey(store = createGeminiKeyStore()) {
62
80
  const result = await store.getSecret();
@@ -110,7 +128,7 @@ export async function deleteOpenRouterApiKey(store = createOpenRouterKeyStore())
110
128
  export async function deleteGeminiApiKey(store = createGeminiKeyStore()) {
111
129
  return await store.deleteSecret();
112
130
  }
113
- export async function getAuthStatus({ env = process.env, storage, geminiStore = createGeminiKeyStore(), openRouterStore = createOpenRouterKeyStore(), } = {}) {
131
+ export async function getAuthStatus({ env = process.env, runtime, geminiStore = createGeminiKeyStore(), openRouterStore = createOpenRouterKeyStore(), } = {}) {
114
132
  const envApiKeys = getEnvApiKeys(env);
115
133
  const geminiStored = envApiKeys.gemini
116
134
  ? { value: undefined }
@@ -118,7 +136,7 @@ export async function getAuthStatus({ env = process.env, storage, geminiStore =
118
136
  const openRouterStored = envApiKeys.openrouter
119
137
  ? { value: undefined }
120
138
  : await readStoredOpenRouterApiKey(openRouterStore).catch(() => ({ value: undefined }));
121
- const openAICodexAuthState = await getOpenAICodexAuthState(storage);
139
+ const openAICodexAuthState = await getOpenAICodexAuthState(runtime);
122
140
  return {
123
141
  openai: openAICodexAuthState === 'missing'
124
142
  ? { configured: false, source: undefined }
@@ -149,17 +167,31 @@ export async function getAuthStatus({ env = process.env, storage, geminiStore =
149
167
  },
150
168
  };
151
169
  }
152
- function createOpenAICodexBrowserLoginOptions() {
170
+ function createOpenAICodexBrowserLoginInteraction() {
153
171
  return {
154
- onAuth: (info) => {
155
- openUrlInBrowser(info.url);
156
- process.stdout.write('Open this URL in your browser:\n');
157
- process.stdout.write(`${info.url}\n`);
158
- if (info.instructions) {
159
- process.stdout.write(`${info.instructions}\n`);
172
+ notify: (event) => {
173
+ if (event.type === 'auth_url') {
174
+ openUrlInBrowser(event.url);
175
+ writeLine('Open this URL in your browser:');
176
+ writeLine(event.url);
177
+ if (event.instructions) {
178
+ writeLine(event.instructions);
179
+ }
180
+ }
181
+ else if (event.type === 'info' || event.type === 'progress') {
182
+ writeLine(event.message);
183
+ }
184
+ },
185
+ prompt: async (prompt) => {
186
+ if (prompt.type === 'select') {
187
+ const browserOption = prompt.options.find(({ id }) => id === OPENAI_CODEX_BROWSER_LOGIN_METHOD);
188
+ if (!browserOption) {
189
+ throw new Error('OpenAI Codex did not provide a browser login option');
190
+ }
191
+ return browserOption.id;
160
192
  }
193
+ return await promptInput(`${prompt.message} `, prompt.signal);
161
194
  },
162
- onPrompt: async (prompt) => await promptInput(`${prompt.message} `),
163
195
  };
164
196
  }
165
197
  function openUrlInBrowser(url) {
package/dist/config.js CHANGED
@@ -3,13 +3,15 @@ import { getEnvApiKeys } from './env.js';
3
3
  const REQUIRED_CLI_AUTH_PROVIDERS = [{ provider: 'openai', label: 'OpenAI' }];
4
4
  const ALL_REVIEW_FOCUSES = [
5
5
  'general',
6
+ 'security',
7
+ 'reuse',
6
8
  'quality',
7
9
  'efficiency',
8
10
  'tests',
9
11
  'standards',
10
12
  ];
11
- const OPENROUTER_REVIEW_MODELS = ['deepseek', 'zai'];
12
- const CLI_GEMINI_REVIEW_MODEL = 'gemini-3.5-flash';
13
+ const OPENROUTER_REVIEW_MODELS = ['deepseek', 'grok'];
14
+ const CLI_GEMINI_REVIEW_MODEL = 'gemini-3.7-flash';
13
15
  function createCliGeminiCredential(apiKey) {
14
16
  return {
15
17
  apiKey,
@@ -47,6 +49,7 @@ export async function getApiKeys(env = process.env, auth = {
47
49
  }
48
50
  if (openRouterApiKey) {
49
51
  apiKeys.deepseek = { apiKey: openRouterApiKey, piProviderId: 'openrouter' };
52
+ apiKeys.grok = { apiKey: openRouterApiKey, piProviderId: 'openrouter' };
50
53
  apiKeys.zai = { apiKey: openRouterApiKey, piProviderId: 'openrouter' };
51
54
  }
52
55
  if (openAICodexAuthState === 'ready') {
@@ -74,7 +77,7 @@ export function formatMissingRequiredCliAuthProviders(missingProviders) {
74
77
  }
75
78
  export function getCliReviewRouting(apiKeys) {
76
79
  const hasGemini = Boolean(apiKeys.gemini);
77
- const hasOpenRouter = Boolean(apiKeys.deepseek && apiKeys.zai);
80
+ const hasOpenRouter = Boolean(apiKeys.deepseek && apiKeys.grok);
78
81
  if (hasGemini && hasOpenRouter) {
79
82
  return {
80
83
  reviewModelsEnabled: ['openai', ...OPENROUTER_REVIEW_MODELS],
package/dist/input.js CHANGED
@@ -1,15 +1,15 @@
1
- import { createInterface } from 'node:readline';
2
- export function promptInput(prompt) {
3
- return new Promise((resolve) => {
4
- const rl = createInterface({
5
- input: process.stdin,
6
- output: process.stdout,
7
- });
8
- rl.question(prompt, (answer) => {
9
- rl.close();
10
- resolve(answer);
11
- });
1
+ import { createInterface } from 'node:readline/promises';
2
+ export async function promptInput(prompt, signal) {
3
+ const rl = createInterface({
4
+ input: process.stdin,
5
+ output: process.stdout,
12
6
  });
7
+ try {
8
+ return await rl.question(prompt, { signal });
9
+ }
10
+ finally {
11
+ rl.close();
12
+ }
13
13
  }
14
14
  export function promptHiddenInput(prompt) {
15
15
  if (!process.stdin.isTTY) {
package/dist/terminal.js CHANGED
@@ -116,6 +116,7 @@ export async function withSpinner(message, task, terminalConfig = getDefaultTerm
116
116
  const PROVIDER_DISPLAY_NAMES = {
117
117
  deepseek: 'DeepSeek',
118
118
  gemini: 'Gemini',
119
+ grok: 'Grok',
119
120
  zai: 'Z.ai',
120
121
  openai: 'OpenAI',
121
122
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doist/doistbot-cli",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "Local self-review CLI for Doistbot",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -42,14 +42,15 @@
42
42
  "dependencies": {
43
43
  "@inquirer/checkbox": "5.2.1",
44
44
  "@inquirer/password": "5.1.1",
45
- "@earendil-works/pi-ai": "0.80.6",
46
- "@earendil-works/pi-coding-agent": "0.80.6",
45
+ "@earendil-works/pi-ai": "0.84.2",
46
+ "@earendil-works/pi-coding-agent": "0.84.2",
47
47
  "@napi-rs/keyring": "1.3.0",
48
48
  "ajv": "8.20.0",
49
49
  "chalk": "4.1.2",
50
50
  "commander": "14.0.3",
51
- "js-yaml": "4.2.0",
51
+ "js-yaml": "4.3.1",
52
52
  "octokit": "5.0.5",
53
+ "undici": "8.10.0",
53
54
  "yocto-spinner": "1.2.0"
54
55
  },
55
56
  "devDependencies": {
@@ -1,7 +1,7 @@
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 ["openai", "deepseek", "zai"];
4
+ export declare const DEFAULT_REVIEW_MODELS: readonly ["openai", "deepseek", "grok"];
5
5
  export type DefaultReviewModel = (typeof DEFAULT_REVIEW_MODELS)[number];
6
6
  export type RepoConfig = {
7
7
  review?: {
@@ -2,7 +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 = ['openai', 'deepseek', 'zai'];
5
+ export const DEFAULT_REVIEW_MODELS = ['openai', 'deepseek', 'grok'];
6
6
  function parseRepoConfigContent(content) {
7
7
  return yamlLoad(content);
8
8
  }
@@ -11,6 +11,8 @@ function toTaskMetricBaseModel(model) {
11
11
  case 'gemini':
12
12
  case 'google':
13
13
  return 'google';
14
+ case 'grok':
15
+ return 'grok';
14
16
  case 'zai':
15
17
  return 'zai';
16
18
  case 'openai':
@@ -6,25 +6,29 @@ import { fileURLToPath } from 'url';
6
6
  export const PI_PROVIDER_BY_DOISTBOT_PROVIDER = {
7
7
  deepseek: 'openrouter',
8
8
  gemini: 'google',
9
+ grok: 'openrouter',
9
10
  zai: 'openrouter',
10
11
  openai: 'openai',
11
12
  };
12
13
  export const DEFAULT_PI_MODELS = {
13
14
  deepseek: 'deepseek-v4-pro',
14
15
  gemini: 'gemini-3.1-pro-preview',
16
+ grok: 'grok-4.6',
15
17
  zai: 'glm5.2',
16
18
  openai: 'gpt-5.6-terra',
17
19
  };
18
20
  export const DEFAULT_OPENROUTER_MODELS = {
19
- deepseek: 'deepseek/deepseek-v4-pro',
21
+ deepseek: 'deepseek/deepseek-v4-pro-0813',
20
22
  gemini: 'google/gemini-3.1-pro-preview',
23
+ grok: 'x-ai/grok-4.6',
21
24
  zai: 'z-ai/glm-5.2',
22
25
  };
23
26
  // Gemini flash fallback model.
24
- export const DEFAULT_GEMINI_FLASH_MODEL = 'gemini-3.5-flash';
27
+ export const DEFAULT_GEMINI_FLASH_MODEL = 'gemini-3.7-flash';
25
28
  export const MAX_PI_THINKING_BY_PROVIDER = {
26
29
  deepseek: 'high',
27
30
  gemini: 'high',
31
+ grok: 'high',
28
32
  zai: 'high',
29
33
  openai: 'xhigh',
30
34
  };
@@ -334,8 +338,14 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
334
338
  ...openRouterSessionExtension.args,
335
339
  ...imageArgs,
336
340
  ];
337
- const childEnv = {
341
+ const inheritedEnv = {
338
342
  ...process.env,
343
+ };
344
+ // Repository-scoped credentials must be passed explicitly through
345
+ // extraEnv by an authorized task invocation.
346
+ delete inheritedEnv.GH_PACKAGES_TOKEN;
347
+ const childEnv = {
348
+ ...inheritedEnv,
339
349
  HOME: process.env.HOME,
340
350
  PATH: process.env.PATH,
341
351
  GIT_TERMINAL_PROMPT: '0',
@@ -40,16 +40,16 @@ export function optionalPositiveInt(value) {
40
40
  /**
41
41
  * Parses the base environment variables common to all sandbox tasks.
42
42
  */
43
- export function parseBaseContext() {
43
+ export function parseBaseContext(env = process.env) {
44
44
  return {
45
- owner: requiredEnv('REPO_OWNER'),
46
- repo: requiredEnv('REPO_NAME'),
47
- prNumber: requiredPositiveInt('PR_NUMBER'),
48
- installationId: requiredPositiveInt('INSTALLATION_ID'),
49
- appId: requiredEnv('APP_ID'),
50
- privateKey: requiredEnv('PRIVATE_KEY'),
51
- githubToken: process.env.GITHUB_TOKEN?.trim() || undefined,
52
- deliveryId: process.env.DELIVERY_ID,
45
+ owner: requiredEnv('REPO_OWNER', env),
46
+ repo: requiredEnv('REPO_NAME', env),
47
+ prNumber: requiredPositiveInt('PR_NUMBER', env),
48
+ installationId: requiredPositiveInt('INSTALLATION_ID', env),
49
+ appId: requiredEnv('APP_ID', env),
50
+ privateKey: requiredEnv('PRIVATE_KEY', env),
51
+ githubToken: env.GITHUB_TOKEN?.trim() || undefined,
52
+ deliveryId: env.DELIVERY_ID?.trim() || undefined,
53
53
  };
54
54
  }
55
55
  export function resolveWriteOctokit({ githubToken, fallbackOctokit, }) {
@@ -0,0 +1,13 @@
1
+ import { vi } from 'vitest';
2
+ /**
3
+ * Shared assertion helper for the model/phase span tests, which all spy on
4
+ * `logger.info` and assert on the same `phase.*` envelope shape emitted by the
5
+ * LoggerSpanProcessor.
6
+ *
7
+ * Returns the `phase.end` envelopes for the given span name, in call order.
8
+ */
9
+ export function phaseEnds(infoSpy, name) {
10
+ return infoSpy.mock.calls
11
+ .map((call) => call[1])
12
+ .filter((meta) => meta?.event === 'phase.end' && meta?.name === name);
13
+ }
@@ -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) {
@@ -32,6 +32,9 @@ switch (taskType) {
32
32
  case 'review':
33
33
  await import('./tasks/review/review.js');
34
34
  break;
35
+ case 'review-slice-plan':
36
+ await import('./tasks/review-slice-plan/review-slice-plan.js');
37
+ break;
35
38
  default:
36
39
  process.stderr.write(`Unknown TASK_TYPE: ${taskType}\n`);
37
40
  process.exit(1);
@@ -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
  };
@@ -4,11 +4,11 @@ import { buildPiImageContent, createPiInvoker, DEFAULT_OPENROUTER_MODELS } from
4
4
  import { withPhase } from '../../core/tracing.js';
5
5
  const ISSUE_SUMMARIZE_TIMEOUT_MS = 5 * 60 * 1000;
6
6
  const ISSUE_SUMMARIZE_MULTIMODAL_MODEL = 'google/gemini-2.5-pro';
7
- const SUMMARIZE_TEXT_PROVIDER = 'zai';
8
- // Single source of truth: inherit the repo-wide ZAI default so a default-model
7
+ const SUMMARIZE_TEXT_PROVIDER = 'grok';
8
+ // Single source of truth: inherit the repo-wide Grok default so a default-model
9
9
  // bump in core/pi.ts moves summarize too, and the executed model matches the
10
10
  // recorded gen_ai.request.model.
11
- const SUMMARIZE_TEXT_MODEL = DEFAULT_OPENROUTER_MODELS.zai;
11
+ const SUMMARIZE_TEXT_MODEL = DEFAULT_OPENROUTER_MODELS.grok;
12
12
  const SUMMARIZE_MULTIMODAL_PROVIDER = 'gcp.gemini';
13
13
  // One model span per actual model.invoke. The final summary uses
14
14
  // `issue-summarize.model`; each chunk summary uses `issue-summarize.chunk`.
@@ -16,7 +16,7 @@ const SUMMARIZE_MODEL_PHASE = 'issue-summarize.model';
16
16
  const SUMMARIZE_CHUNK_PHASE = 'issue-summarize.chunk';
17
17
  export function createIssueSummarizeModel(apiKey) {
18
18
  return createPiInvoker({
19
- provider: 'zai',
19
+ provider: 'grok',
20
20
  apiKey,
21
21
  piProviderId: 'openrouter',
22
22
  cwd: process.cwd(),
@@ -0,0 +1,22 @@
1
+ const REPOSITORY_CAPABILITIES = {
2
+ 'doist/todoist-web': ['github-packages-read'],
3
+ };
4
+ export function resolveAutofixCapabilities(repository) {
5
+ return REPOSITORY_CAPABILITIES[repository.toLowerCase()] ?? [];
6
+ }
7
+ export function resolveAutofixCapabilityEnv(repository, env = process.env) {
8
+ const capabilityEnv = {};
9
+ for (const capability of resolveAutofixCapabilities(repository)) {
10
+ if (capability === 'github-packages-read') {
11
+ const token = env.GH_PACKAGES_TOKEN?.trim();
12
+ if (!token) {
13
+ return {
14
+ ok: false,
15
+ reason: `Missing GH_PACKAGES_TOKEN for required autofix capability "${capability}" on ${repository}.`,
16
+ };
17
+ }
18
+ capabilityEnv.GH_PACKAGES_TOKEN = token;
19
+ }
20
+ }
21
+ return { ok: true, env: capabilityEnv };
22
+ }
@@ -1,6 +1,7 @@
1
1
  import { execFileSync } from 'child_process';
2
2
  import { logger } from '../../core/logging.js';
3
3
  import { fillPromptTemplate, loadPromptTemplateFile, resolvePromptFile, } from '../../core/prompt-template.js';
4
+ import { resolveAutofixCapabilityEnv } from './autofix-capabilities.js';
4
5
  import { validateDiff } from './diff-validator.js';
5
6
  import { CODEX_CANNOT_FIX_REASON, FIX_LOOP_FAILED_REASON_PREFIX } from './fix-failure-reasons.js';
6
7
  import { CX_HERO_MANUAL_HANDLE, resolveHeroGroup } from './hero-group-map.js';
@@ -325,6 +326,10 @@ export async function runFixCore(input) {
325
326
  reason: buildFixAssessmentSkipReason(fixAssessment),
326
327
  };
327
328
  }
329
+ const capabilityEnvResult = resolveAutofixCapabilityEnv(targetRepo.repository.fullName);
330
+ if (!capabilityEnvResult.ok) {
331
+ return capabilityEnvResult;
332
+ }
328
333
  // Reset repo to clean state before fix attempt (triage Codex may have modified files).
329
334
  // Use `git reset --hard` to clear both the working tree and the index, since
330
335
  // captureGitDiff stages files with `git add -A`.
@@ -387,6 +392,7 @@ export async function runFixCore(input) {
387
392
  timeoutMs,
388
393
  extraEnv: {
389
394
  GH_TOKEN: input.ghToken,
395
+ ...capabilityEnvResult.env,
390
396
  },
391
397
  });
392
398
  if (!codexResult.ok) {