@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.
- package/dist/actions/auth.js +19 -5
- package/dist/actions/doctor.js +3 -1
- package/dist/actions/review.js +9 -4
- package/dist/auth.js +79 -30
- package/dist/config.js +12 -7
- package/dist/input.js +11 -11
- package/package.json +5 -4
- package/sandbox/dist/core/pi.js +10 -4
- package/sandbox/dist/core/shared.js +9 -9
- package/sandbox/dist/core/span-test-helpers.js +13 -0
- package/sandbox/dist/main.js +3 -0
- package/sandbox/dist/tasks/chat/chat.js +7 -6
- package/sandbox/dist/tasks/issue-triage/autofix-capabilities.js +22 -0
- package/sandbox/dist/tasks/issue-triage/fix-attempt.js +7 -0
- package/sandbox/dist/tasks/issue-triage/routing.js +124 -15
- package/sandbox/dist/tasks/issue-triage/triage.js +2 -0
- package/sandbox/dist/tasks/review/engines/multi-focus.js +16 -8
- package/sandbox/dist/tasks/review/multi-focus-prompt.js +2 -0
- package/sandbox/dist/tasks/review/review-summary.js +1 -1
- package/sandbox/dist/tasks/review-slice-plan/context.js +69 -0
- package/sandbox/dist/tasks/review-slice-plan/plan.js +209 -0
- package/sandbox/dist/tasks/review-slice-plan/prompt.js +59 -0
- package/sandbox/dist/tasks/review-slice-plan/review-slice-plan.js +470 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/efficiency.md +10 -12
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/general.md +24 -7
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/quality.md +14 -22
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/reuse.md +13 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/security.md +18 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/tests.md +12 -22
package/dist/actions/auth.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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: ${
|
|
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,
|
package/dist/actions/doctor.js
CHANGED
|
@@ -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
|
|
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,
|
package/dist/actions/review.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
42
|
-
|
|
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(
|
|
45
|
-
const
|
|
46
|
-
|
|
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,
|
|
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
|
|
139
|
+
const openAICodexAuthState = await getOpenAICodexAuthState(runtime);
|
|
109
140
|
return {
|
|
110
|
-
openai:
|
|
111
|
-
? { configured:
|
|
112
|
-
: {
|
|
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
|
|
170
|
+
function createOpenAICodexBrowserLoginInteraction() {
|
|
136
171
|
return {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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,
|
|
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.
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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 }) =>
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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.
|
|
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.
|
|
46
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
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.
|
|
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": {
|
package/sandbox/dist/core/pi.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
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:
|
|
52
|
-
deliveryId:
|
|
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
|
+
}
|
package/sandbox/dist/main.js
CHANGED
|
@@ -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 = '
|
|
15
|
-
const CHAT_MODEL = '
|
|
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.
|
|
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: '
|
|
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: '
|
|
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) {
|