@doist/doistbot-cli 1.0.7 → 1.0.9

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 (29) hide show
  1. package/dist/actions/auth.js +19 -5
  2. package/dist/actions/doctor.js +3 -1
  3. package/dist/actions/review.js +9 -4
  4. package/dist/auth.js +79 -30
  5. package/dist/config.js +12 -7
  6. package/dist/input.js +11 -11
  7. package/package.json +5 -4
  8. package/sandbox/dist/core/pi.js +10 -4
  9. package/sandbox/dist/core/shared.js +9 -9
  10. package/sandbox/dist/core/span-test-helpers.js +13 -0
  11. package/sandbox/dist/main.js +3 -0
  12. package/sandbox/dist/tasks/chat/chat.js +7 -6
  13. package/sandbox/dist/tasks/issue-triage/autofix-capabilities.js +22 -0
  14. package/sandbox/dist/tasks/issue-triage/fix-attempt.js +7 -0
  15. package/sandbox/dist/tasks/issue-triage/routing.js +124 -15
  16. package/sandbox/dist/tasks/issue-triage/triage.js +2 -0
  17. package/sandbox/dist/tasks/review/engines/multi-focus.js +16 -8
  18. package/sandbox/dist/tasks/review/multi-focus-prompt.js +2 -0
  19. package/sandbox/dist/tasks/review/review-summary.js +1 -1
  20. package/sandbox/dist/tasks/review-slice-plan/context.js +69 -0
  21. package/sandbox/dist/tasks/review-slice-plan/plan.js +209 -0
  22. package/sandbox/dist/tasks/review-slice-plan/prompt.js +59 -0
  23. package/sandbox/dist/tasks/review-slice-plan/review-slice-plan.js +470 -0
  24. package/sandbox/src/tasks/review/prompts/review-focus-prompts/efficiency.md +10 -12
  25. package/sandbox/src/tasks/review/prompts/review-focus-prompts/general.md +24 -7
  26. package/sandbox/src/tasks/review/prompts/review-focus-prompts/quality.md +14 -22
  27. package/sandbox/src/tasks/review/prompts/review-focus-prompts/reuse.md +13 -0
  28. package/sandbox/src/tasks/review/prompts/review-focus-prompts/security.md +18 -0
  29. 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,17 +68,21 @@ 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
- if (status.openai.configured) {
75
+ if (status.openai.configured && !status.openai.refreshFailed) {
74
76
  deps.writeLine('✓ OpenAI already configured (pi-auth).');
75
77
  }
76
78
  else {
77
79
  if (!deps.isInteractive) {
78
80
  throw new Error('Cannot configure OpenAI Codex auth in non-interactive mode.');
79
81
  }
80
- await deps.loginOpenAICodex();
82
+ if (status.openai.refreshFailed) {
83
+ deps.writeLine('OpenAI session could not be refreshed (expired, revoked, or offline). Reconnecting.');
84
+ }
85
+ await deps.loginOpenAICodex(runtime);
81
86
  deps.writeLine();
82
87
  deps.writeLine('✓ OpenAI configured.');
83
88
  }
@@ -149,10 +154,19 @@ export async function runAuthSetup(dependencies = {}) {
149
154
  }
150
155
  export async function runAuthStatus() {
151
156
  const status = await getAuthStatus();
152
- writeLine(`OpenAI: ${status.openai.configured ? `configured (${status.openai.source ?? 'unknown'})` : 'not configured'}`);
157
+ writeLine(`OpenAI: ${formatOpenAIStatus(status.openai)}`);
153
158
  writeLine(`Gemini ${formatOptionalLabel()}: ${status.gemini.configured ? `configured (${status.gemini.source ?? 'unknown'})` : 'not configured'}`);
154
159
  writeLine(`OpenRouter ${formatOptionalLabel()}: ${status.openrouter.configured ? `configured (${status.openrouter.source ?? 'unknown'})` : 'not configured'}`);
155
160
  }
161
+ function formatOpenAIStatus(status) {
162
+ if (!status.configured) {
163
+ return 'not configured';
164
+ }
165
+ const base = `configured (${status.source ?? 'unknown'})`;
166
+ return status.refreshFailed
167
+ ? `${base} — session could not be refreshed; check your connection or run \`doistbot auth login\``
168
+ : base;
169
+ }
156
170
  export async function runAuthLogout(target, dependencies = {}) {
157
171
  const deps = {
158
172
  deleteGeminiApiKey,
@@ -4,7 +4,9 @@ import { checkGit, checkPiDependency, checkRepository, checkSandboxBuild } from
4
4
  import { renderDiagnosticCheck } from '../terminal.js';
5
5
  export function getConfiguredProviders({ status, }) {
6
6
  return [
7
- status.openai.configured ? `OpenAI (${status.openai.source ?? 'configured'})` : undefined,
7
+ status.openai.configured && !status.openai.refreshFailed
8
+ ? `OpenAI (${status.openai.source ?? 'configured'})`
9
+ : undefined,
8
10
  status.gemini.configured
9
11
  ? `Gemini optional (${status.gemini.source ?? 'configured'})`
10
12
  : undefined,
@@ -21,10 +21,14 @@ function getReadableProviderFailure(failure) {
21
21
  if (failure.model === 'openai' &&
22
22
  (normalized.includes('no api key found for openai') ||
23
23
  normalized.includes('no api key found for openai-codex') ||
24
+ normalized.includes('no api key for provider') ||
24
25
  normalized.includes('use /login') ||
25
26
  normalized.includes('invalid api key') ||
26
- normalized.includes('incorrect api key'))) {
27
- return 'OpenAI Codex is not authenticated. Run `doistbot auth` to reconnect OpenAI.';
27
+ normalized.includes('incorrect api key') ||
28
+ normalized.includes('refresh_token_invalidated') ||
29
+ normalized.includes('unauthorized') ||
30
+ /\b401\b/.test(normalized))) {
31
+ return 'OpenAI Codex is not authenticated. Run `doistbot auth login` to reconnect OpenAI.';
28
32
  }
29
33
  if (failure.model === 'gemini' &&
30
34
  (normalized.includes('429') ||
@@ -40,7 +44,8 @@ function getReadableProviderFailure(failure) {
40
44
  normalized.includes('invalid_argument') ||
41
45
  normalized.includes('invalid api key') ||
42
46
  normalized.includes('no api key found for google') ||
43
- normalized.includes('no api key found for gemini'))) {
47
+ normalized.includes('no api key found for gemini') ||
48
+ normalized.includes('no api key for provider'))) {
44
49
  return 'Gemini API key is invalid or missing. Run `doistbot auth` and paste a valid Gemini key.';
45
50
  }
46
51
  const firstLine = error
@@ -205,7 +210,7 @@ function getMissingAuthProviderFailures(missingProviders) {
205
210
  if (missingProviders.includes('OpenAI')) {
206
211
  failures.push({
207
212
  model: 'openai',
208
- message: 'OpenAI not configured; run `doistbot auth`',
213
+ message: 'OpenAI authentication unavailable; run `doistbot auth login`',
209
214
  });
210
215
  }
211
216
  return failures;
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') {
@@ -28,22 +41,40 @@ export function getBrowserOpenCommand(platform, url) {
28
41
  }
29
42
  return { command: 'xdg-open', args: [url] };
30
43
  }
31
- export async function hasOpenAICodexAuth(storage) {
32
- const resolvedStorage = storage ?? (await createPiAuthStorage());
33
- return resolvedStorage.get(OPENAI_CODEX_PROVIDER_ID)?.type === 'oauth';
34
- }
35
- export async function loginOpenAICodex(storage) {
36
- const resolvedStorage = storage ?? (await createPiAuthStorage());
37
- const { loginOpenAICodex: loginInBrowser } = await import('@earendil-works/pi-ai/oauth');
38
- const credentials = await loginInBrowser(createOpenAICodexBrowserLoginOptions());
39
- saveOpenAICodexCredentials(resolvedStorage, credentials);
44
+ /**
45
+ * Check that the stored OAuth session is usable, not just present.
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.
50
+ */
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')) {
55
+ return 'missing';
56
+ }
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
+ }
40
70
  }
41
- export function saveOpenAICodexCredentials(storage, credentials) {
42
- 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());
43
74
  }
44
- export async function logoutOpenAICodex(storage) {
45
- const resolvedStorage = storage ?? (await createPiAuthStorage());
46
- 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);
47
78
  }
48
79
  export async function readStoredGeminiApiKey(store = createGeminiKeyStore()) {
49
80
  const result = await store.getSecret();
@@ -97,7 +128,7 @@ export async function deleteOpenRouterApiKey(store = createOpenRouterKeyStore())
97
128
  export async function deleteGeminiApiKey(store = createGeminiKeyStore()) {
98
129
  return await store.deleteSecret();
99
130
  }
100
- 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(), } = {}) {
101
132
  const envApiKeys = getEnvApiKeys(env);
102
133
  const geminiStored = envApiKeys.gemini
103
134
  ? { value: undefined }
@@ -105,11 +136,15 @@ export async function getAuthStatus({ env = process.env, storage, geminiStore =
105
136
  const openRouterStored = envApiKeys.openrouter
106
137
  ? { value: undefined }
107
138
  : await readStoredOpenRouterApiKey(openRouterStore).catch(() => ({ value: undefined }));
108
- const openAICodexAuthConfigured = await hasOpenAICodexAuth(storage);
139
+ const openAICodexAuthState = await getOpenAICodexAuthState(runtime);
109
140
  return {
110
- openai: openAICodexAuthConfigured
111
- ? { configured: true, source: 'pi-auth' }
112
- : { configured: false, source: undefined },
141
+ openai: openAICodexAuthState === 'missing'
142
+ ? { configured: false, source: undefined }
143
+ : {
144
+ configured: true,
145
+ source: 'pi-auth',
146
+ refreshFailed: openAICodexAuthState === 'refresh-failed',
147
+ },
113
148
  gemini: envApiKeys.gemini
114
149
  ? { configured: true, source: 'env' }
115
150
  : {
@@ -132,17 +167,31 @@ export async function getAuthStatus({ env = process.env, storage, geminiStore =
132
167
  },
133
168
  };
134
169
  }
135
- function createOpenAICodexBrowserLoginOptions() {
170
+ function createOpenAICodexBrowserLoginInteraction() {
136
171
  return {
137
- onAuth: (info) => {
138
- openUrlInBrowser(info.url);
139
- process.stdout.write('Open this URL in your browser:\n');
140
- process.stdout.write(`${info.url}\n`);
141
- if (info.instructions) {
142
- 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;
143
192
  }
193
+ return await promptInput(`${prompt.message} `, prompt.signal);
144
194
  },
145
- onPrompt: async (prompt) => await promptInput(`${prompt.message} `),
146
195
  };
147
196
  }
148
197
  function openUrlInBrowser(url) {
package/dist/config.js CHANGED
@@ -1,15 +1,17 @@
1
- import { getStoredGeminiApiKey, getStoredOpenRouterApiKey, hasOpenAICodexAuth, PI_AUTH_API_KEY, } from './auth.js';
1
+ import { getOpenAICodexAuthState, getStoredGeminiApiKey, getStoredOpenRouterApiKey, PI_AUTH_API_KEY, } from './auth.js';
2
2
  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
13
  const OPENROUTER_REVIEW_MODELS = ['deepseek', 'zai'];
12
- const CLI_GEMINI_REVIEW_MODEL = 'gemini-3.5-flash';
14
+ const CLI_GEMINI_REVIEW_MODEL = 'gemini-3.7-flash';
13
15
  function createCliGeminiCredential(apiKey) {
14
16
  return {
15
17
  apiKey,
@@ -29,7 +31,7 @@ function createCliGeminiSummaryModel(credential) {
29
31
  export async function getApiKeys(env = process.env, auth = {
30
32
  getStoredGeminiApiKey,
31
33
  getStoredOpenRouterApiKey,
32
- hasOpenAICodexAuth,
34
+ getOpenAICodexAuthState,
33
35
  }) {
34
36
  const envApiKeys = getEnvApiKeys(env);
35
37
  const storedGeminiApiKey = envApiKeys.gemini
@@ -38,7 +40,7 @@ export async function getApiKeys(env = process.env, auth = {
38
40
  const storedOpenRouterApiKey = envApiKeys.openrouter
39
41
  ? undefined
40
42
  : await auth.getStoredOpenRouterApiKey().catch(() => undefined);
41
- const openAICodexAuthConfigured = await auth.hasOpenAICodexAuth();
43
+ const openAICodexAuthState = await auth.getOpenAICodexAuthState();
42
44
  const openRouterApiKey = envApiKeys.openrouter || storedOpenRouterApiKey;
43
45
  const apiKeys = {};
44
46
  const geminiApiKey = envApiKeys.gemini || storedGeminiApiKey;
@@ -49,13 +51,16 @@ export async function getApiKeys(env = process.env, auth = {
49
51
  apiKeys.deepseek = { apiKey: openRouterApiKey, piProviderId: 'openrouter' };
50
52
  apiKeys.zai = { apiKey: openRouterApiKey, piProviderId: 'openrouter' };
51
53
  }
52
- if (openAICodexAuthConfigured) {
54
+ if (openAICodexAuthState === 'ready') {
53
55
  apiKeys.openai = PI_AUTH_API_KEY;
54
56
  }
55
57
  return apiKeys;
56
58
  }
57
59
  export function getMissingRequiredCliAuthProviders(status) {
58
- return REQUIRED_CLI_AUTH_PROVIDERS.filter(({ provider }) => !status[provider].configured).map(({ label }) => label);
60
+ return REQUIRED_CLI_AUTH_PROVIDERS.filter(({ provider }) => {
61
+ const providerStatus = status[provider];
62
+ return !providerStatus.configured || providerStatus.refreshFailed === true;
63
+ }).map(({ label }) => label);
59
64
  }
60
65
  export function getMissingRequiredCliReviewAuthProviders(apiKeys) {
61
66
  return REQUIRED_CLI_AUTH_PROVIDERS.filter(({ provider }) => {
@@ -67,7 +72,7 @@ export function getMissingRequiredCliReviewAuthProviders(apiKeys) {
67
72
  }).map(({ label }) => label);
68
73
  }
69
74
  export function formatMissingRequiredCliAuthProviders(missingProviders) {
70
- return `${missingProviders.join(' and ')} not configured; run \`doistbot auth\``;
75
+ return `${missingProviders.join(' and ')} authentication unavailable (not configured, or the session could not be refreshed); run \`doistbot auth login\``;
71
76
  }
72
77
  export function getCliReviewRouting(apiKeys) {
73
78
  const hasGemini = Boolean(apiKeys.gemini);
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doist/doistbot-cli",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
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.79.6",
46
- "@earendil-works/pi-coding-agent": "0.79.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.9.0",
53
54
  "yocto-spinner": "1.2.0"
54
55
  },
55
56
  "devDependencies": {
@@ -13,15 +13,15 @@ export const DEFAULT_PI_MODELS = {
13
13
  deepseek: 'deepseek-v4-pro',
14
14
  gemini: 'gemini-3.1-pro-preview',
15
15
  zai: 'glm5.2',
16
- openai: 'gpt-5.4',
16
+ openai: 'gpt-5.6-terra',
17
17
  };
18
18
  export const DEFAULT_OPENROUTER_MODELS = {
19
- deepseek: 'deepseek/deepseek-v4-pro',
19
+ deepseek: 'deepseek/deepseek-v4-pro-0813',
20
20
  gemini: 'google/gemini-3.1-pro-preview',
21
21
  zai: 'z-ai/glm-5.2',
22
22
  };
23
23
  // Gemini flash fallback model.
24
- export const DEFAULT_GEMINI_FLASH_MODEL = 'gemini-3.5-flash';
24
+ export const DEFAULT_GEMINI_FLASH_MODEL = 'gemini-3.7-flash';
25
25
  export const MAX_PI_THINKING_BY_PROVIDER = {
26
26
  deepseek: 'high',
27
27
  gemini: 'high',
@@ -334,8 +334,14 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
334
334
  ...openRouterSessionExtension.args,
335
335
  ...imageArgs,
336
336
  ];
337
- const childEnv = {
337
+ const inheritedEnv = {
338
338
  ...process.env,
339
+ };
340
+ // Repository-scoped credentials must be passed explicitly through
341
+ // extraEnv by an authorized task invocation.
342
+ delete inheritedEnv.GH_PACKAGES_TOKEN;
343
+ const childEnv = {
344
+ ...inheritedEnv,
339
345
  HOME: process.env.HOME,
340
346
  PATH: process.env.PATH,
341
347
  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
+ }
@@ -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,8 +11,9 @@ import { withPhase } from '../../core/tracing.js';
11
11
  import { buildPrompt } from './prompt.js';
12
12
  const CHAT_PROMPT_FILE = 'chat-prompt.md';
13
13
  const CHAT_TIMEOUT_MS = 5 * 60 * 1000;
14
- const CHAT_PROVIDER = 'zai';
15
- const CHAT_MODEL = 'z-ai/glm-5.2';
14
+ const CHAT_PROVIDER = 'openai';
15
+ const CHAT_MODEL = 'gpt-5.6-luna';
16
+ const CHAT_THINKING_LEVEL = 'high';
16
17
  function parseContext() {
17
18
  const base = parseBaseContext();
18
19
  const reactionIdRaw = process.env.REACTION_ID;
@@ -125,12 +126,12 @@ async function gatherIssueCommentContext(octokit, owner, repo, prNumber, comment
125
126
  };
126
127
  }
127
128
  export async function invokeChatModel(prompt) {
128
- const apiKey = process.env.OPENROUTER_API_KEY?.trim();
129
+ const apiKey = process.env.OPENAI_KEY?.trim();
129
130
  if (!apiKey) {
130
131
  // Config error: no model call attempted, so no model span. The reason is
131
132
  // returned so the caller's root span reports the real cause.
132
133
  logger.warn('Chat model API key missing');
133
- return { ok: false, reason: 'OPENROUTER_API_KEY missing' };
134
+ return { ok: false, reason: 'OPENAI_KEY missing' };
134
135
  }
135
136
  return withPhase('chat.model', {
136
137
  'gen_ai.operation.name': 'chat',
@@ -140,11 +141,11 @@ export async function invokeChatModel(prompt) {
140
141
  const result = await invokePiPrompt({
141
142
  provider: CHAT_PROVIDER,
142
143
  apiKey,
143
- piProviderId: 'openrouter',
144
144
  prompt,
145
145
  cwd: WORKSPACE_DIR,
146
146
  timeoutMs: CHAT_TIMEOUT_MS,
147
147
  capabilityPreset: 'workspace-inspection',
148
+ thinkingLevel: CHAT_THINKING_LEVEL,
148
149
  // Keep the executed model in sync with the recorded span attr.
149
150
  modelName: CHAT_MODEL,
150
151
  });
@@ -371,7 +372,7 @@ async function main() {
371
372
  await recordTaskMetrics({
372
373
  type: 'chat',
373
374
  repository: `${ctx.owner}/${ctx.repo}`,
374
- model: 'zai',
375
+ model: 'openai',
375
376
  outcome: metricOutcome,
376
377
  durationMs: performance.now() - startedAt,
377
378
  });
@@ -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`.
@@ -383,9 +388,11 @@ export async function runFixCore(input) {
383
388
  prompt,
384
389
  apiKey: ctx.openaiApiKey,
385
390
  cwd: targetRepo.localPath,
391
+ thinkingLevel: 'xhigh',
386
392
  timeoutMs,
387
393
  extraEnv: {
388
394
  GH_TOKEN: input.ghToken,
395
+ ...capabilityEnvResult.env,
389
396
  },
390
397
  });
391
398
  if (!codexResult.ok) {