@link-assistant/hive-mind 0.39.0
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/CHANGELOG.md +20 -0
- package/LICENSE +24 -0
- package/README.md +769 -0
- package/package.json +58 -0
- package/src/agent.lib.mjs +705 -0
- package/src/agent.prompts.lib.mjs +196 -0
- package/src/buildUserMention.lib.mjs +71 -0
- package/src/claude-limits.lib.mjs +389 -0
- package/src/claude.lib.mjs +1445 -0
- package/src/claude.prompts.lib.mjs +203 -0
- package/src/codex.lib.mjs +552 -0
- package/src/codex.prompts.lib.mjs +194 -0
- package/src/config.lib.mjs +207 -0
- package/src/contributing-guidelines.lib.mjs +268 -0
- package/src/exit-handler.lib.mjs +205 -0
- package/src/git.lib.mjs +145 -0
- package/src/github-issue-creator.lib.mjs +246 -0
- package/src/github-linking.lib.mjs +152 -0
- package/src/github.batch.lib.mjs +272 -0
- package/src/github.graphql.lib.mjs +258 -0
- package/src/github.lib.mjs +1479 -0
- package/src/hive.config.lib.mjs +254 -0
- package/src/hive.mjs +1500 -0
- package/src/instrument.mjs +191 -0
- package/src/interactive-mode.lib.mjs +1000 -0
- package/src/lenv-reader.lib.mjs +206 -0
- package/src/lib.mjs +490 -0
- package/src/lino.lib.mjs +176 -0
- package/src/local-ci-checks.lib.mjs +324 -0
- package/src/memory-check.mjs +419 -0
- package/src/model-mapping.lib.mjs +145 -0
- package/src/model-validation.lib.mjs +278 -0
- package/src/opencode.lib.mjs +479 -0
- package/src/opencode.prompts.lib.mjs +194 -0
- package/src/protect-branch.mjs +159 -0
- package/src/review.mjs +433 -0
- package/src/reviewers-hive.mjs +643 -0
- package/src/sentry.lib.mjs +284 -0
- package/src/solve.auto-continue.lib.mjs +568 -0
- package/src/solve.auto-pr.lib.mjs +1374 -0
- package/src/solve.branch-errors.lib.mjs +341 -0
- package/src/solve.branch.lib.mjs +230 -0
- package/src/solve.config.lib.mjs +342 -0
- package/src/solve.error-handlers.lib.mjs +256 -0
- package/src/solve.execution.lib.mjs +291 -0
- package/src/solve.feedback.lib.mjs +436 -0
- package/src/solve.mjs +1128 -0
- package/src/solve.preparation.lib.mjs +210 -0
- package/src/solve.repo-setup.lib.mjs +114 -0
- package/src/solve.repository.lib.mjs +961 -0
- package/src/solve.results.lib.mjs +558 -0
- package/src/solve.session.lib.mjs +135 -0
- package/src/solve.validation.lib.mjs +325 -0
- package/src/solve.watch.lib.mjs +572 -0
- package/src/start-screen.mjs +324 -0
- package/src/task.mjs +308 -0
- package/src/telegram-bot.mjs +1481 -0
- package/src/telegram-markdown.lib.mjs +64 -0
- package/src/usage-limit.lib.mjs +218 -0
- package/src/version.lib.mjs +41 -0
- package/src/youtrack/solve.youtrack.lib.mjs +116 -0
- package/src/youtrack/youtrack-sync.mjs +219 -0
- package/src/youtrack/youtrack.lib.mjs +425 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Codex CLI-related utility functions
|
|
3
|
+
|
|
4
|
+
// Check if use is already defined (when imported from solve.mjs)
|
|
5
|
+
// If not, fetch it (when running standalone)
|
|
6
|
+
if (typeof globalThis.use === 'undefined') {
|
|
7
|
+
globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { $ } = await use('command-stream');
|
|
11
|
+
const fs = (await use('fs')).promises;
|
|
12
|
+
const path = (await use('path')).default;
|
|
13
|
+
const os = (await use('os')).default;
|
|
14
|
+
|
|
15
|
+
// Import log from general lib
|
|
16
|
+
import { log } from './lib.mjs';
|
|
17
|
+
import { reportError } from './sentry.lib.mjs';
|
|
18
|
+
import { timeouts } from './config.lib.mjs';
|
|
19
|
+
import { detectUsageLimit, formatUsageLimitMessage } from './usage-limit.lib.mjs';
|
|
20
|
+
|
|
21
|
+
// Model mapping to translate aliases to full model IDs for Codex
|
|
22
|
+
export const mapModelToId = (model) => {
|
|
23
|
+
const modelMap = {
|
|
24
|
+
'gpt5': 'gpt-5',
|
|
25
|
+
'gpt5-codex': 'gpt-5-codex',
|
|
26
|
+
'o3': 'o3',
|
|
27
|
+
'o3-mini': 'o3-mini',
|
|
28
|
+
'gpt4': 'gpt-4',
|
|
29
|
+
'gpt4o': 'gpt-4o',
|
|
30
|
+
'claude': 'claude-3-5-sonnet',
|
|
31
|
+
'sonnet': 'claude-3-5-sonnet',
|
|
32
|
+
'opus': 'claude-3-opus',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Return mapped model ID if it's an alias, otherwise return as-is
|
|
36
|
+
return modelMap[model] || model;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Function to validate Codex CLI connection
|
|
40
|
+
export const validateCodexConnection = async (model = 'gpt-5') => {
|
|
41
|
+
// Map model alias to full ID
|
|
42
|
+
const mappedModel = mapModelToId(model);
|
|
43
|
+
|
|
44
|
+
// Retry configuration
|
|
45
|
+
const maxRetries = 3;
|
|
46
|
+
let retryCount = 0;
|
|
47
|
+
|
|
48
|
+
const attemptValidation = async () => {
|
|
49
|
+
try {
|
|
50
|
+
if (retryCount === 0) {
|
|
51
|
+
await log('š Validating Codex CLI connection...');
|
|
52
|
+
} else {
|
|
53
|
+
await log(`š Retry attempt ${retryCount}/${maxRetries} for Codex validation...`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check if Codex CLI is installed and get version
|
|
57
|
+
try {
|
|
58
|
+
const versionResult = await $`timeout ${Math.floor(timeouts.codexCli / 1000)} codex --version`;
|
|
59
|
+
if (versionResult.code === 0) {
|
|
60
|
+
const version = versionResult.stdout?.toString().trim();
|
|
61
|
+
if (retryCount === 0) {
|
|
62
|
+
await log(`š¦ Codex CLI version: ${version}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch (versionError) {
|
|
66
|
+
if (retryCount === 0) {
|
|
67
|
+
await log(`ā ļø Codex CLI version check failed (${versionError.code}), proceeding with connection test...`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Test basic Codex functionality with a simple "echo hi" command
|
|
72
|
+
// Using exec mode with JSON output for validation
|
|
73
|
+
const testResult = await $`printf "echo hi" | timeout ${Math.floor(timeouts.codexCli / 1000)} codex exec --model ${mappedModel} --json --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox`;
|
|
74
|
+
|
|
75
|
+
if (testResult.code !== 0) {
|
|
76
|
+
const stderr = testResult.stderr?.toString() || '';
|
|
77
|
+
const stdout = testResult.stdout?.toString() || '';
|
|
78
|
+
|
|
79
|
+
// Check for authentication errors in both stderr and stdout
|
|
80
|
+
// Codex CLI may return auth errors in JSON format on stdout
|
|
81
|
+
if (stderr.includes('auth') || stderr.includes('login') ||
|
|
82
|
+
stdout.includes('Not logged in') || stdout.includes('401 Unauthorized')) {
|
|
83
|
+
const authError = new Error('Codex authentication failed - 401 Unauthorized');
|
|
84
|
+
authError.isAuthError = true;
|
|
85
|
+
await log('ā Codex authentication failed', { level: 'error' });
|
|
86
|
+
await log(' š” Please run: codex login', { level: 'error' });
|
|
87
|
+
throw authError;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
await log(`ā Codex validation failed with exit code ${testResult.code}`, { level: 'error' });
|
|
91
|
+
if (stderr) await log(` Error: ${stderr.trim()}`, { level: 'error' });
|
|
92
|
+
if (stdout && !stderr) await log(` Output: ${stdout.trim()}`, { level: 'error' });
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Success
|
|
97
|
+
await log('ā
Codex CLI connection validated successfully');
|
|
98
|
+
return true;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
await log(`ā Failed to validate Codex CLI connection: ${error.message}`, { level: 'error' });
|
|
101
|
+
await log(' š” Make sure Codex CLI is installed and accessible', { level: 'error' });
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Start the validation
|
|
107
|
+
return await attemptValidation();
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Function to handle Codex runtime switching (if applicable)
|
|
111
|
+
export const handleCodexRuntimeSwitch = async () => {
|
|
112
|
+
// Codex is typically run as a CLI tool, runtime switching may not be applicable
|
|
113
|
+
// This function can be used for any runtime-specific configurations if needed
|
|
114
|
+
await log('ā¹ļø Codex runtime handling not required for this operation');
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Main function to execute Codex with prompts and settings
|
|
118
|
+
export const executeCodex = async (params) => {
|
|
119
|
+
const {
|
|
120
|
+
issueUrl,
|
|
121
|
+
issueNumber,
|
|
122
|
+
prNumber,
|
|
123
|
+
prUrl,
|
|
124
|
+
branchName,
|
|
125
|
+
tempDir,
|
|
126
|
+
isContinueMode,
|
|
127
|
+
mergeStateStatus,
|
|
128
|
+
forkedRepo,
|
|
129
|
+
feedbackLines,
|
|
130
|
+
forkActionsUrl,
|
|
131
|
+
owner,
|
|
132
|
+
repo,
|
|
133
|
+
argv,
|
|
134
|
+
log,
|
|
135
|
+
formatAligned,
|
|
136
|
+
getResourceSnapshot,
|
|
137
|
+
codexPath = 'codex',
|
|
138
|
+
$
|
|
139
|
+
} = params;
|
|
140
|
+
|
|
141
|
+
// Import prompt building functions from codex.prompts.lib.mjs
|
|
142
|
+
const { buildUserPrompt, buildSystemPrompt } = await import('./codex.prompts.lib.mjs');
|
|
143
|
+
|
|
144
|
+
// Build the user prompt
|
|
145
|
+
const prompt = buildUserPrompt({
|
|
146
|
+
issueUrl,
|
|
147
|
+
issueNumber,
|
|
148
|
+
prNumber,
|
|
149
|
+
prUrl,
|
|
150
|
+
branchName,
|
|
151
|
+
tempDir,
|
|
152
|
+
isContinueMode,
|
|
153
|
+
mergeStateStatus,
|
|
154
|
+
forkedRepo,
|
|
155
|
+
feedbackLines,
|
|
156
|
+
forkActionsUrl,
|
|
157
|
+
owner,
|
|
158
|
+
repo,
|
|
159
|
+
argv
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Build the system prompt
|
|
163
|
+
const systemPrompt = buildSystemPrompt({
|
|
164
|
+
owner,
|
|
165
|
+
repo,
|
|
166
|
+
issueNumber,
|
|
167
|
+
prNumber,
|
|
168
|
+
branchName,
|
|
169
|
+
tempDir,
|
|
170
|
+
isContinueMode,
|
|
171
|
+
forkedRepo,
|
|
172
|
+
argv
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Log prompt details in verbose mode
|
|
176
|
+
if (argv.verbose) {
|
|
177
|
+
await log('\nš Final prompt structure:', { verbose: true });
|
|
178
|
+
await log(` Characters: ${prompt.length}`, { verbose: true });
|
|
179
|
+
await log(` System prompt characters: ${systemPrompt.length}`, { verbose: true });
|
|
180
|
+
if (feedbackLines && feedbackLines.length > 0) {
|
|
181
|
+
await log(' Feedback info: Included', { verbose: true });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (argv.dryRun) {
|
|
185
|
+
await log('\nš User prompt content:', { verbose: true });
|
|
186
|
+
await log('---BEGIN USER PROMPT---', { verbose: true });
|
|
187
|
+
await log(prompt, { verbose: true });
|
|
188
|
+
await log('---END USER PROMPT---', { verbose: true });
|
|
189
|
+
await log('\nš System prompt content:', { verbose: true });
|
|
190
|
+
await log('---BEGIN SYSTEM PROMPT---', { verbose: true });
|
|
191
|
+
await log(systemPrompt, { verbose: true });
|
|
192
|
+
await log('---END SYSTEM PROMPT---', { verbose: true });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Execute the Codex command
|
|
197
|
+
return await executeCodexCommand({
|
|
198
|
+
tempDir,
|
|
199
|
+
branchName,
|
|
200
|
+
prompt,
|
|
201
|
+
systemPrompt,
|
|
202
|
+
argv,
|
|
203
|
+
log,
|
|
204
|
+
formatAligned,
|
|
205
|
+
getResourceSnapshot,
|
|
206
|
+
forkedRepo,
|
|
207
|
+
feedbackLines,
|
|
208
|
+
codexPath,
|
|
209
|
+
$
|
|
210
|
+
});
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export const executeCodexCommand = async (params) => {
|
|
214
|
+
const {
|
|
215
|
+
tempDir,
|
|
216
|
+
branchName,
|
|
217
|
+
prompt,
|
|
218
|
+
systemPrompt,
|
|
219
|
+
argv,
|
|
220
|
+
log,
|
|
221
|
+
formatAligned,
|
|
222
|
+
getResourceSnapshot,
|
|
223
|
+
forkedRepo,
|
|
224
|
+
feedbackLines,
|
|
225
|
+
codexPath,
|
|
226
|
+
$
|
|
227
|
+
} = params;
|
|
228
|
+
|
|
229
|
+
// Retry configuration
|
|
230
|
+
const maxRetries = 3;
|
|
231
|
+
let retryCount = 0;
|
|
232
|
+
|
|
233
|
+
const executeWithRetry = async () => {
|
|
234
|
+
// Execute codex command from the cloned repository directory
|
|
235
|
+
if (retryCount === 0) {
|
|
236
|
+
await log(`\n${formatAligned('š¤', 'Executing Codex:', argv.model.toUpperCase())}`);
|
|
237
|
+
} else {
|
|
238
|
+
await log(`\n${formatAligned('š', 'Retry attempt:', `${retryCount}/${maxRetries}`)}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (argv.verbose) {
|
|
242
|
+
await log(` Model: ${argv.model}`, { verbose: true });
|
|
243
|
+
await log(` Working directory: ${tempDir}`, { verbose: true });
|
|
244
|
+
await log(` Branch: ${branchName}`, { verbose: true });
|
|
245
|
+
await log(` Prompt length: ${prompt.length} chars`, { verbose: true });
|
|
246
|
+
await log(` System prompt length: ${systemPrompt.length} chars`, { verbose: true });
|
|
247
|
+
if (feedbackLines && feedbackLines.length > 0) {
|
|
248
|
+
await log(` Feedback info included: Yes (${feedbackLines.length} lines)`, { verbose: true });
|
|
249
|
+
} else {
|
|
250
|
+
await log(' Feedback info included: No', { verbose: true });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Take resource snapshot before execution
|
|
255
|
+
const resourcesBefore = await getResourceSnapshot();
|
|
256
|
+
await log('š System resources before execution:', { verbose: true });
|
|
257
|
+
await log(` Memory: ${resourcesBefore.memory.split('\n')[1]}`, { verbose: true });
|
|
258
|
+
await log(` Load: ${resourcesBefore.load}`, { verbose: true });
|
|
259
|
+
|
|
260
|
+
// Build Codex command
|
|
261
|
+
let execCommand;
|
|
262
|
+
|
|
263
|
+
// Map model alias to full ID
|
|
264
|
+
const mappedModel = mapModelToId(argv.model);
|
|
265
|
+
|
|
266
|
+
// Build codex command arguments
|
|
267
|
+
// Codex uses exec mode for non-interactive execution
|
|
268
|
+
// --json provides structured output
|
|
269
|
+
// --full-auto enables automatic execution with workspace-write sandbox
|
|
270
|
+
let codexArgs = `exec --model ${mappedModel} --json --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox`;
|
|
271
|
+
|
|
272
|
+
if (argv.resume) {
|
|
273
|
+
// Codex supports resuming sessions
|
|
274
|
+
await log(`š Resuming from session: ${argv.resume}`);
|
|
275
|
+
codexArgs = `exec resume ${argv.resume} --json --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// For Codex, we combine system and user prompts into a single message
|
|
279
|
+
// Codex doesn't have separate system prompt support in CLI mode
|
|
280
|
+
const combinedPrompt = systemPrompt ? `${systemPrompt}\n\n${prompt}` : prompt;
|
|
281
|
+
|
|
282
|
+
// Write the combined prompt to a file for piping
|
|
283
|
+
// Use OS temporary directory instead of repository workspace to avoid polluting the repo
|
|
284
|
+
const promptFile = path.join(os.tmpdir(), `codex_prompt_${Date.now()}_${process.pid}.txt`);
|
|
285
|
+
await fs.writeFile(promptFile, combinedPrompt);
|
|
286
|
+
|
|
287
|
+
// Build the full command - pipe the prompt file to codex
|
|
288
|
+
const fullCommand = `(cd "${tempDir}" && cat "${promptFile}" | ${codexPath} ${codexArgs})`;
|
|
289
|
+
|
|
290
|
+
await log(`\n${formatAligned('š', 'Raw command:', '')}`);
|
|
291
|
+
await log(`${fullCommand}`);
|
|
292
|
+
await log('');
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
// Pipe the prompt file to codex via stdin
|
|
296
|
+
if (argv.resume) {
|
|
297
|
+
execCommand = $({
|
|
298
|
+
cwd: tempDir,
|
|
299
|
+
mirror: false
|
|
300
|
+
})`cat ${promptFile} | ${codexPath} exec resume ${argv.resume} --json --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox`;
|
|
301
|
+
} else {
|
|
302
|
+
execCommand = $({
|
|
303
|
+
cwd: tempDir,
|
|
304
|
+
mirror: false
|
|
305
|
+
})`cat ${promptFile} | ${codexPath} exec --model ${mappedModel} --json --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox`;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
await log(`${formatAligned('š', 'Command details:', '')}`);
|
|
309
|
+
await log(formatAligned('š', 'Working directory:', tempDir, 2));
|
|
310
|
+
await log(formatAligned('šæ', 'Branch:', branchName, 2));
|
|
311
|
+
await log(formatAligned('š¤', 'Model:', `Codex ${argv.model.toUpperCase()}`, 2));
|
|
312
|
+
if (argv.fork && forkedRepo) {
|
|
313
|
+
await log(formatAligned('š“', 'Fork:', forkedRepo, 2));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
await log(`\n${formatAligned('ā¶ļø', 'Streaming output:', '')}\n`);
|
|
317
|
+
|
|
318
|
+
let exitCode = 0;
|
|
319
|
+
let sessionId = null;
|
|
320
|
+
let limitReached = false;
|
|
321
|
+
let limitResetTime = null;
|
|
322
|
+
let lastMessage = '';
|
|
323
|
+
let authError = false;
|
|
324
|
+
|
|
325
|
+
for await (const chunk of execCommand.stream()) {
|
|
326
|
+
if (chunk.type === 'stdout') {
|
|
327
|
+
const output = chunk.data.toString();
|
|
328
|
+
await log(output);
|
|
329
|
+
lastMessage = output;
|
|
330
|
+
|
|
331
|
+
// Try to parse JSON output to extract session info
|
|
332
|
+
// Codex CLI uses thread_id instead of session_id
|
|
333
|
+
try {
|
|
334
|
+
const lines = output.split('\n');
|
|
335
|
+
for (const line of lines) {
|
|
336
|
+
if (!line.trim()) continue;
|
|
337
|
+
const data = JSON.parse(line);
|
|
338
|
+
// Check for both thread_id (codex) and session_id (legacy)
|
|
339
|
+
if ((data.thread_id || data.session_id) && !sessionId) {
|
|
340
|
+
sessionId = data.thread_id || data.session_id;
|
|
341
|
+
await log(`š Session ID: ${sessionId}`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Check for authentication errors (401 Unauthorized)
|
|
345
|
+
// These should never be retried as they indicate missing/invalid credentials
|
|
346
|
+
if (data.type === 'error' && data.message &&
|
|
347
|
+
(data.message.includes('401 Unauthorized') ||
|
|
348
|
+
data.message.includes('401') ||
|
|
349
|
+
data.message.includes('Unauthorized'))) {
|
|
350
|
+
authError = true;
|
|
351
|
+
await log('\nā Authentication error detected: 401 Unauthorized', { level: 'error' });
|
|
352
|
+
await log(' This error cannot be resolved by retrying.', { level: 'error' });
|
|
353
|
+
await log(' š” Please run: codex login', { level: 'error' });
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Also check turn.failed events for auth errors
|
|
357
|
+
if (data.type === 'turn.failed' && data.error && data.error.message &&
|
|
358
|
+
(data.error.message.includes('401 Unauthorized') ||
|
|
359
|
+
data.error.message.includes('401') ||
|
|
360
|
+
data.error.message.includes('Unauthorized'))) {
|
|
361
|
+
authError = true;
|
|
362
|
+
await log('\nā Authentication error detected in turn.failed event', { level: 'error' });
|
|
363
|
+
await log(' This error cannot be resolved by retrying.', { level: 'error' });
|
|
364
|
+
await log(' š” Please run: codex login', { level: 'error' });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
} catch {
|
|
368
|
+
// Not JSON, continue
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (chunk.type === 'stderr') {
|
|
373
|
+
const errorOutput = chunk.data.toString();
|
|
374
|
+
if (errorOutput) {
|
|
375
|
+
await log(errorOutput, { stream: 'stderr' });
|
|
376
|
+
}
|
|
377
|
+
} else if (chunk.type === 'exit') {
|
|
378
|
+
exitCode = chunk.code;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Check for authentication errors first - these should never be retried
|
|
383
|
+
if (authError) {
|
|
384
|
+
const resourcesAfter = await getResourceSnapshot();
|
|
385
|
+
await log('\nš System resources after execution:', { verbose: true });
|
|
386
|
+
await log(` Memory: ${resourcesAfter.memory.split('\n')[1]}`, { verbose: true });
|
|
387
|
+
await log(` Load: ${resourcesAfter.load}`, { verbose: true });
|
|
388
|
+
|
|
389
|
+
// Throw an error to stop retries and propagate the auth failure
|
|
390
|
+
const error = new Error('Codex authentication failed - 401 Unauthorized. Please run: codex login');
|
|
391
|
+
error.isAuthError = true;
|
|
392
|
+
throw error;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (exitCode !== 0) {
|
|
396
|
+
// Check for usage limit errors first (more specific)
|
|
397
|
+
const limitInfo = detectUsageLimit(lastMessage);
|
|
398
|
+
if (limitInfo.isUsageLimit) {
|
|
399
|
+
limitReached = true;
|
|
400
|
+
limitResetTime = limitInfo.resetTime;
|
|
401
|
+
|
|
402
|
+
// Format and display user-friendly message
|
|
403
|
+
const messageLines = formatUsageLimitMessage({
|
|
404
|
+
tool: 'Codex',
|
|
405
|
+
resetTime: limitInfo.resetTime,
|
|
406
|
+
sessionId,
|
|
407
|
+
resumeCommand: sessionId ? `${process.argv[0]} ${process.argv[1]} ${argv.url} --resume ${sessionId}` : null
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
for (const line of messageLines) {
|
|
411
|
+
await log(line, { level: 'warning' });
|
|
412
|
+
}
|
|
413
|
+
} else {
|
|
414
|
+
await log(`\n\nā Codex command failed with exit code ${exitCode}`, { level: 'error' });
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const resourcesAfter = await getResourceSnapshot();
|
|
418
|
+
await log('\nš System resources after execution:', { verbose: true });
|
|
419
|
+
await log(` Memory: ${resourcesAfter.memory.split('\n')[1]}`, { verbose: true });
|
|
420
|
+
await log(` Load: ${resourcesAfter.load}`, { verbose: true });
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
success: false,
|
|
424
|
+
sessionId,
|
|
425
|
+
limitReached,
|
|
426
|
+
limitResetTime
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
await log('\n\nā
Codex command completed');
|
|
431
|
+
|
|
432
|
+
return {
|
|
433
|
+
success: true,
|
|
434
|
+
sessionId,
|
|
435
|
+
limitReached,
|
|
436
|
+
limitResetTime
|
|
437
|
+
};
|
|
438
|
+
} catch (error) {
|
|
439
|
+
// Don't report auth errors to Sentry as they are user configuration issues
|
|
440
|
+
if (!error.isAuthError) {
|
|
441
|
+
reportError(error, {
|
|
442
|
+
context: 'execute_codex',
|
|
443
|
+
command: params.command,
|
|
444
|
+
codexPath: params.codexPath,
|
|
445
|
+
operation: 'run_codex_command'
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
await log(`\n\nā Error executing Codex command: ${error.message}`, { level: 'error' });
|
|
450
|
+
|
|
451
|
+
// Re-throw auth errors to stop any outer retry loops
|
|
452
|
+
if (error.isAuthError) {
|
|
453
|
+
throw error;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
success: false,
|
|
458
|
+
sessionId: null,
|
|
459
|
+
limitReached: false,
|
|
460
|
+
limitResetTime: null
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
// Start the execution with retry logic
|
|
466
|
+
return await executeWithRetry();
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
export const checkForUncommittedChanges = async (tempDir, owner, repo, branchName, $, log, autoCommit = false, autoRestartEnabled = true) => {
|
|
470
|
+
// Similar to Claude and OpenCode version, check for uncommitted changes
|
|
471
|
+
await log('\nš Checking for uncommitted changes...');
|
|
472
|
+
try {
|
|
473
|
+
const gitStatusResult = await $({ cwd: tempDir })`git status --porcelain 2>&1`;
|
|
474
|
+
|
|
475
|
+
if (gitStatusResult.code === 0) {
|
|
476
|
+
const statusOutput = gitStatusResult.stdout.toString().trim();
|
|
477
|
+
|
|
478
|
+
if (statusOutput) {
|
|
479
|
+
await log('š Found uncommitted changes');
|
|
480
|
+
await log('Changes:');
|
|
481
|
+
for (const line of statusOutput.split('\n')) {
|
|
482
|
+
await log(` ${line}`);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (autoCommit) {
|
|
486
|
+
await log('š¾ Auto-committing changes (--auto-commit-uncommitted-changes is enabled)...');
|
|
487
|
+
|
|
488
|
+
const addResult = await $({ cwd: tempDir })`git add -A`;
|
|
489
|
+
if (addResult.code === 0) {
|
|
490
|
+
const commitMessage = 'Auto-commit: Changes made by Codex during problem-solving session';
|
|
491
|
+
const commitResult = await $({ cwd: tempDir })`git commit -m ${commitMessage}`;
|
|
492
|
+
|
|
493
|
+
if (commitResult.code === 0) {
|
|
494
|
+
await log('ā
Changes committed successfully');
|
|
495
|
+
|
|
496
|
+
const pushResult = await $({ cwd: tempDir })`git push origin ${branchName}`;
|
|
497
|
+
|
|
498
|
+
if (pushResult.code === 0) {
|
|
499
|
+
await log('ā
Changes pushed successfully');
|
|
500
|
+
} else {
|
|
501
|
+
await log(`ā ļø Warning: Could not push changes: ${pushResult.stderr?.toString().trim()}`, { level: 'warning' });
|
|
502
|
+
}
|
|
503
|
+
} else {
|
|
504
|
+
await log(`ā ļø Warning: Could not commit changes: ${commitResult.stderr?.toString().trim()}`, { level: 'warning' });
|
|
505
|
+
}
|
|
506
|
+
} else {
|
|
507
|
+
await log(`ā ļø Warning: Could not stage changes: ${addResult.stderr?.toString().trim()}`, { level: 'warning' });
|
|
508
|
+
}
|
|
509
|
+
return false;
|
|
510
|
+
} else if (autoRestartEnabled) {
|
|
511
|
+
await log('');
|
|
512
|
+
await log('ā ļø IMPORTANT: Uncommitted changes detected!');
|
|
513
|
+
await log(' Codex made changes that were not committed.');
|
|
514
|
+
await log('');
|
|
515
|
+
await log('š AUTO-RESTART: Restarting Codex to handle uncommitted changes...');
|
|
516
|
+
await log(' Codex will review the changes and decide what to commit.');
|
|
517
|
+
await log('');
|
|
518
|
+
return true;
|
|
519
|
+
} else {
|
|
520
|
+
await log('');
|
|
521
|
+
await log('ā ļø Uncommitted changes detected but auto-restart is disabled.');
|
|
522
|
+
await log(' Use --auto-restart-on-uncommitted-changes to enable or commit manually.');
|
|
523
|
+
await log('');
|
|
524
|
+
return false;
|
|
525
|
+
}
|
|
526
|
+
} else {
|
|
527
|
+
await log('ā
No uncommitted changes found');
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
} else {
|
|
531
|
+
await log(`ā ļø Warning: Could not check git status: ${gitStatusResult.stderr?.toString().trim()}`, { level: 'warning' });
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
} catch (gitError) {
|
|
535
|
+
reportError(gitError, {
|
|
536
|
+
context: 'check_uncommitted_changes_codex',
|
|
537
|
+
tempDir,
|
|
538
|
+
operation: 'git_status_check'
|
|
539
|
+
});
|
|
540
|
+
await log(`ā ļø Warning: Error checking for uncommitted changes: ${gitError.message}`, { level: 'warning' });
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
// Export all functions as default object too
|
|
546
|
+
export default {
|
|
547
|
+
validateCodexConnection,
|
|
548
|
+
handleCodexRuntimeSwitch,
|
|
549
|
+
executeCodex,
|
|
550
|
+
executeCodexCommand,
|
|
551
|
+
checkForUncommittedChanges
|
|
552
|
+
};
|