@coralai/sps-cli 0.10.2 → 0.11.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/dist/commands/workerDashboard.d.ts.map +1 -1
- package/dist/commands/workerDashboard.js +39 -11
- package/dist/commands/workerDashboard.js.map +1 -1
- package/dist/core/config.d.ts +1 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +1 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/state.d.ts +10 -0
- package/dist/core/state.d.ts.map +1 -1
- package/dist/core/state.js +5 -0
- package/dist/core/state.js.map +1 -1
- package/dist/engines/CloseoutEngine.d.ts.map +1 -1
- package/dist/engines/CloseoutEngine.js +79 -28
- package/dist/engines/CloseoutEngine.js.map +1 -1
- package/dist/engines/ExecutionEngine.d.ts +5 -0
- package/dist/engines/ExecutionEngine.d.ts.map +1 -1
- package/dist/engines/ExecutionEngine.js +62 -14
- package/dist/engines/ExecutionEngine.js.map +1 -1
- package/dist/engines/MonitorEngine.d.ts.map +1 -1
- package/dist/engines/MonitorEngine.js +6 -1
- package/dist/engines/MonitorEngine.js.map +1 -1
- package/dist/interfaces/WorkerProvider.d.ts +68 -15
- package/dist/interfaces/WorkerProvider.d.ts.map +1 -1
- package/dist/providers/ClaudePrintProvider.d.ts +50 -0
- package/dist/providers/ClaudePrintProvider.d.ts.map +1 -0
- package/dist/providers/ClaudePrintProvider.js +220 -0
- package/dist/providers/ClaudePrintProvider.js.map +1 -0
- package/dist/providers/ClaudeTmuxProvider.d.ts +94 -0
- package/dist/providers/ClaudeTmuxProvider.d.ts.map +1 -0
- package/dist/providers/ClaudeTmuxProvider.js +331 -0
- package/dist/providers/ClaudeTmuxProvider.js.map +1 -0
- package/dist/providers/ClaudeWorkerProvider.d.ts +5 -93
- package/dist/providers/ClaudeWorkerProvider.d.ts.map +1 -1
- package/dist/providers/ClaudeWorkerProvider.js +3 -303
- package/dist/providers/ClaudeWorkerProvider.js.map +1 -1
- package/dist/providers/CodexExecProvider.d.ts +31 -0
- package/dist/providers/CodexExecProvider.d.ts.map +1 -0
- package/dist/providers/CodexExecProvider.js +180 -0
- package/dist/providers/CodexExecProvider.js.map +1 -0
- package/dist/providers/CodexTmuxProvider.d.ts +71 -0
- package/dist/providers/CodexTmuxProvider.d.ts.map +1 -0
- package/dist/providers/CodexTmuxProvider.js +351 -0
- package/dist/providers/CodexTmuxProvider.js.map +1 -0
- package/dist/providers/CodexWorkerProvider.d.ts +5 -70
- package/dist/providers/CodexWorkerProvider.d.ts.map +1 -1
- package/dist/providers/CodexWorkerProvider.js +3 -328
- package/dist/providers/CodexWorkerProvider.js.map +1 -1
- package/dist/providers/outputParser.d.ts +39 -0
- package/dist/providers/outputParser.d.ts.map +1 -0
- package/dist/providers/outputParser.js +183 -0
- package/dist/providers/outputParser.js.map +1 -0
- package/dist/providers/registry.d.ts.map +1 -1
- package/dist/providers/registry.js +18 -6
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/streamRenderer.d.ts +13 -0
- package/dist/providers/streamRenderer.d.ts.map +1 -0
- package/dist/providers/streamRenderer.js +106 -0
- package/dist/providers/streamRenderer.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,71 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
constructor(config: ProjectConfig);
|
|
7
|
-
prepareEnv(worktree: string, _seq: string): Promise<void>;
|
|
8
|
-
/**
|
|
9
|
-
* Launch Codex in a tmux session (interactive mode).
|
|
10
|
-
* Handles session reuse and update prompt auto-skip.
|
|
11
|
-
*/
|
|
12
|
-
launch(session: string, worktree: string): Promise<void>;
|
|
13
|
-
/**
|
|
14
|
-
* Wait for Codex to be ready.
|
|
15
|
-
* Must handle the update prompt blocker (auto-skip).
|
|
16
|
-
*/
|
|
17
|
-
waitReady(session: string, timeoutMs?: number): Promise<boolean>;
|
|
18
|
-
/**
|
|
19
|
-
* Send task prompt to Codex via tmux paste-buffer.
|
|
20
|
-
*/
|
|
21
|
-
sendTask(session: string, promptFile: string): Promise<void>;
|
|
22
|
-
inspect(session: string): Promise<{
|
|
23
|
-
alive: boolean;
|
|
24
|
-
paneText: string;
|
|
25
|
-
}>;
|
|
26
|
-
/**
|
|
27
|
-
* Codex with --dangerously-bypass-approvals-and-sandbox doesn't need confirmation.
|
|
28
|
-
* But if run without that flag, it may show approval prompts.
|
|
29
|
-
* For now, always return not waiting since we use bypass mode.
|
|
30
|
-
*/
|
|
31
|
-
detectWaiting(session: string): Promise<{
|
|
32
|
-
waiting: boolean;
|
|
33
|
-
destructive: boolean;
|
|
34
|
-
prompt: string;
|
|
35
|
-
}>;
|
|
36
|
-
/**
|
|
37
|
-
* Detect completion by checking if Codex returned to › prompt after working.
|
|
38
|
-
*/
|
|
39
|
-
detectCompleted(session: string, logDir: string, _branch: string): Promise<WorkerStatus>;
|
|
40
|
-
detectBlocked(session: string): Promise<boolean>;
|
|
41
|
-
sendFix(session: string, fixPrompt: string): Promise<void>;
|
|
42
|
-
resolveConflict(session: string, worktree: string, branch: string): Promise<void>;
|
|
43
|
-
/**
|
|
44
|
-
* Release a worker session after task completion.
|
|
45
|
-
*
|
|
46
|
-
* WORKER_SESSION_REUSE=true: do nothing — keep Codex running so the
|
|
47
|
-
* next task can hot-reuse the session via /clear + cd.
|
|
48
|
-
*
|
|
49
|
-
* WORKER_SESSION_REUSE=false: quit Codex but keep tmux session alive.
|
|
50
|
-
*/
|
|
51
|
-
release(session: string): Promise<void>;
|
|
52
|
-
/**
|
|
53
|
-
* Force-stop a worker session (error recovery, cleanup).
|
|
54
|
-
* Always quits Codex and kills the tmux session.
|
|
55
|
-
*/
|
|
56
|
-
stop(session: string): Promise<void>;
|
|
57
|
-
collectSummary(session: string): Promise<string>;
|
|
58
|
-
/**
|
|
59
|
-
* Dismiss the rate-limit model switch prompt by selecting "Keep current model".
|
|
60
|
-
*
|
|
61
|
-
* The prompt shows:
|
|
62
|
-
* › 1. Switch to gpt-5.1-codex-mini
|
|
63
|
-
* 2. Keep current model
|
|
64
|
-
* 3. Keep current model (never show again)
|
|
65
|
-
*
|
|
66
|
-
* Strategy: press Down once to select option 2, then Enter.
|
|
67
|
-
*/
|
|
68
|
-
private dismissRateLimitPrompt;
|
|
69
|
-
private sleep;
|
|
70
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @deprecated Use CodexTmuxProvider or CodexExecProvider directly.
|
|
3
|
+
* This re-export exists for backward compatibility with existing imports.
|
|
4
|
+
*/
|
|
5
|
+
export { CodexTmuxProvider as CodexWorkerProvider } from './CodexTmuxProvider.js';
|
|
71
6
|
//# sourceMappingURL=CodexWorkerProvider.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CodexWorkerProvider.d.ts","sourceRoot":"","sources":["../../src/providers/CodexWorkerProvider.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"CodexWorkerProvider.d.ts","sourceRoot":"","sources":["../../src/providers/CodexWorkerProvider.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,iBAAiB,IAAI,mBAAmB,EAAE,MAAM,wBAAwB,CAAC"}
|
|
@@ -1,331 +1,6 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
3
1
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Ready prompt: › <placeholder>
|
|
7
|
-
* gpt-5.3-codex default · ...
|
|
8
|
-
*
|
|
9
|
-
* Completion: returns to › prompt after task
|
|
10
|
-
*
|
|
11
|
-
* Update blocker: ✨ Update available!
|
|
12
|
-
* › 1. Update now
|
|
13
|
-
* 2. Skip
|
|
14
|
-
* 3. Skip until next version
|
|
15
|
-
* Press enter to continue
|
|
16
|
-
*
|
|
17
|
-
* Confirmation: (none with --dangerously-bypass-approvals-and-sandbox)
|
|
18
|
-
*
|
|
19
|
-
* Exit command: /quit (Claude uses /exit)
|
|
2
|
+
* @deprecated Use CodexTmuxProvider or CodexExecProvider directly.
|
|
3
|
+
* This re-export exists for backward compatibility with existing imports.
|
|
20
4
|
*/
|
|
21
|
-
|
|
22
|
-
const COMPLETION_KEYWORDS = /\b(done|completed|finished|Next step:|committed|pushed|MR created|merge request)\b/i;
|
|
23
|
-
/** Codex ready prompt: › at start of line + model info line */
|
|
24
|
-
const CODEX_READY = /›\s.*$/m;
|
|
25
|
-
const CODEX_MODEL_LINE = /codex.*default.*·/i;
|
|
26
|
-
/** Codex update blocker pattern */
|
|
27
|
-
const CODEX_UPDATE_PROMPT = /Update available|Skip until next version/i;
|
|
28
|
-
/** Codex rate-limit model switch prompt */
|
|
29
|
-
const CODEX_RATE_LIMIT_PROMPT = /rate limit|Switch to .+codex-mini|Keep current model/i;
|
|
30
|
-
/**
|
|
31
|
-
* Run a tmux command. Returns null on failure.
|
|
32
|
-
*/
|
|
33
|
-
function tmux(args) {
|
|
34
|
-
try {
|
|
35
|
-
return execFileSync('tmux', args, {
|
|
36
|
-
encoding: 'utf-8',
|
|
37
|
-
timeout: 10_000,
|
|
38
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
catch (err) {
|
|
42
|
-
const stderr = err.stderr ?? '';
|
|
43
|
-
if (stderr.includes('server exited unexpectedly') || stderr.includes('no server running')) {
|
|
44
|
-
try {
|
|
45
|
-
const uid = process.getuid?.() ?? 1000;
|
|
46
|
-
const { rmSync } = require('node:fs');
|
|
47
|
-
rmSync(`/tmp/tmux-${uid}`, { recursive: true, force: true });
|
|
48
|
-
process.stderr.write('[codex-worker] Cleaned stale tmux socket, retrying\n');
|
|
49
|
-
return execFileSync('tmux', args, {
|
|
50
|
-
encoding: 'utf-8',
|
|
51
|
-
timeout: 10_000,
|
|
52
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
function sessionExists(session) {
|
|
63
|
-
return tmux(['has-session', '-t', session]) !== null;
|
|
64
|
-
}
|
|
65
|
-
function capturePaneText(session, lines) {
|
|
66
|
-
return tmux(['capture-pane', '-t', session, '-p', '-S', `-${lines}`]) ?? '';
|
|
67
|
-
}
|
|
68
|
-
export class CodexWorkerProvider {
|
|
69
|
-
config;
|
|
70
|
-
constructor(config) {
|
|
71
|
-
this.config = config;
|
|
72
|
-
}
|
|
73
|
-
async prepareEnv(worktree, _seq) {
|
|
74
|
-
if (!existsSync(worktree)) {
|
|
75
|
-
throw new Error(`Worktree directory does not exist: ${worktree}`);
|
|
76
|
-
}
|
|
77
|
-
try {
|
|
78
|
-
execFileSync('git', ['-C', worktree, 'rev-parse', '--is-inside-work-tree'], {
|
|
79
|
-
encoding: 'utf-8', timeout: 5_000, stdio: ['ignore', 'pipe', 'pipe'],
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
catch {
|
|
83
|
-
throw new Error(`Directory is not a git worktree: ${worktree}`);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Launch Codex in a tmux session (interactive mode).
|
|
88
|
-
* Handles session reuse and update prompt auto-skip.
|
|
89
|
-
*/
|
|
90
|
-
async launch(session, worktree) {
|
|
91
|
-
const codexCmd = 'codex --sandbox danger-full-access -a never --no-alt-screen';
|
|
92
|
-
if (sessionExists(session)) {
|
|
93
|
-
const pane = capturePaneText(session, 10);
|
|
94
|
-
const codexAlive = CODEX_READY.test(pane) && CODEX_MODEL_LINE.test(pane);
|
|
95
|
-
if (codexAlive) {
|
|
96
|
-
// Codex running — clear conversation + switch worktree
|
|
97
|
-
process.stderr.write(`[codex-worker] Reusing live Codex session ${session}\n`);
|
|
98
|
-
tmux(['send-keys', '-t', session, '/clear', 'Enter']);
|
|
99
|
-
await this.sleep(1_000);
|
|
100
|
-
// Codex doesn't support cd mid-session, but we can try
|
|
101
|
-
tmux(['send-keys', '-t', session, `/cd ${worktree}`, 'Enter']);
|
|
102
|
-
await this.sleep(500);
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
// Session exists but Codex not running
|
|
106
|
-
process.stderr.write(`[codex-worker] Reusing tmux session ${session}\n`);
|
|
107
|
-
tmux(['send-keys', '-t', session, `cd ${worktree}`, 'Enter']);
|
|
108
|
-
await this.sleep(500);
|
|
109
|
-
tmux(['send-keys', '-t', session, codexCmd, 'Enter']);
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
// New session
|
|
113
|
-
const result = tmux(['new-session', '-d', '-s', session, '-c', worktree]);
|
|
114
|
-
if (result === null && !sessionExists(session)) {
|
|
115
|
-
throw new Error(`Failed to create tmux session: ${session}`);
|
|
116
|
-
}
|
|
117
|
-
tmux(['send-keys', '-t', session, codexCmd, 'Enter']);
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Wait for Codex to be ready.
|
|
121
|
-
* Must handle the update prompt blocker (auto-skip).
|
|
122
|
-
*/
|
|
123
|
-
async waitReady(session, timeoutMs = 90_000) {
|
|
124
|
-
const pollInterval = 3_000;
|
|
125
|
-
const deadline = Date.now() + timeoutMs;
|
|
126
|
-
let updateSkipped = false;
|
|
127
|
-
// Codex is slower to start than Claude — wait longer before first check
|
|
128
|
-
await this.sleep(5_000);
|
|
129
|
-
while (Date.now() < deadline) {
|
|
130
|
-
const text = capturePaneText(session, 30);
|
|
131
|
-
// Handle update prompt — send Enter to dismiss, then skip
|
|
132
|
-
if (!updateSkipped && CODEX_UPDATE_PROMPT.test(text)) {
|
|
133
|
-
process.stderr.write('[codex-worker] Detected update prompt, skipping\n');
|
|
134
|
-
// If showing numbered options (1/2/3), select 3 "Skip until next version"
|
|
135
|
-
if (/1\. Update now/i.test(text)) {
|
|
136
|
-
tmux(['send-keys', '-t', session, 'Down']);
|
|
137
|
-
await this.sleep(300);
|
|
138
|
-
tmux(['send-keys', '-t', session, 'Down']);
|
|
139
|
-
await this.sleep(300);
|
|
140
|
-
}
|
|
141
|
-
tmux(['send-keys', '-t', session, 'Enter']);
|
|
142
|
-
updateSkipped = true;
|
|
143
|
-
await this.sleep(5_000);
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
// Handle "Press enter to continue" after update banner
|
|
147
|
-
if (/Press enter to continue/i.test(text)) {
|
|
148
|
-
tmux(['send-keys', '-t', session, 'Enter']);
|
|
149
|
-
await this.sleep(3_000);
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
// Handle rate-limit model switch prompt — select "Keep current model"
|
|
153
|
-
if (CODEX_RATE_LIMIT_PROMPT.test(text)) {
|
|
154
|
-
process.stderr.write('[codex-worker] Detected rate-limit model switch prompt, keeping current model\n');
|
|
155
|
-
this.dismissRateLimitPrompt(session);
|
|
156
|
-
await this.sleep(3_000);
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
// Check if Codex is ready: › prompt + model info line
|
|
160
|
-
if (CODEX_MODEL_LINE.test(text) && CODEX_READY.test(text)) {
|
|
161
|
-
process.stderr.write('[codex-worker] Codex ready\n');
|
|
162
|
-
return true;
|
|
163
|
-
}
|
|
164
|
-
// Also match "OpenAI Codex" banner + › prompt
|
|
165
|
-
if (/OpenAI Codex/i.test(text) && /›/m.test(text)) {
|
|
166
|
-
process.stderr.write('[codex-worker] Codex ready (banner match)\n');
|
|
167
|
-
return true;
|
|
168
|
-
}
|
|
169
|
-
await this.sleep(pollInterval);
|
|
170
|
-
}
|
|
171
|
-
process.stderr.write(`[codex-worker] waitReady timed out after ${timeoutMs}ms\n`);
|
|
172
|
-
return false;
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Send task prompt to Codex via tmux paste-buffer.
|
|
176
|
-
*/
|
|
177
|
-
async sendTask(session, promptFile) {
|
|
178
|
-
if (!existsSync(promptFile)) {
|
|
179
|
-
throw new Error(`Prompt file does not exist: ${promptFile}`);
|
|
180
|
-
}
|
|
181
|
-
const content = readFileSync(promptFile, 'utf-8').trim();
|
|
182
|
-
const bufferFile = `/tmp/sps-task-${Date.now()}.txt`;
|
|
183
|
-
const { writeFileSync: writeTmp, unlinkSync } = await import('node:fs');
|
|
184
|
-
writeTmp(bufferFile, content);
|
|
185
|
-
tmux(['load-buffer', bufferFile]);
|
|
186
|
-
tmux(['paste-buffer', '-t', session]);
|
|
187
|
-
try {
|
|
188
|
-
unlinkSync(bufferFile);
|
|
189
|
-
}
|
|
190
|
-
catch { /* cleanup */ }
|
|
191
|
-
await this.sleep(500);
|
|
192
|
-
tmux(['send-keys', '-t', session, 'Enter']);
|
|
193
|
-
}
|
|
194
|
-
async inspect(session) {
|
|
195
|
-
const alive = sessionExists(session);
|
|
196
|
-
const paneText = alive ? capturePaneText(session, 50) : '';
|
|
197
|
-
return { alive, paneText };
|
|
198
|
-
}
|
|
199
|
-
/**
|
|
200
|
-
* Codex with --dangerously-bypass-approvals-and-sandbox doesn't need confirmation.
|
|
201
|
-
* But if run without that flag, it may show approval prompts.
|
|
202
|
-
* For now, always return not waiting since we use bypass mode.
|
|
203
|
-
*/
|
|
204
|
-
async detectWaiting(session) {
|
|
205
|
-
// Codex in bypass mode doesn't have confirmation prompts
|
|
206
|
-
// But check for any "Press enter" or similar blockers
|
|
207
|
-
const pane = capturePaneText(session, 15);
|
|
208
|
-
// Update prompt blocker
|
|
209
|
-
if (CODEX_UPDATE_PROMPT.test(pane)) {
|
|
210
|
-
return { waiting: true, destructive: false, prompt: 'Codex update prompt' };
|
|
211
|
-
}
|
|
212
|
-
// Rate-limit model switch prompt — auto-dismiss
|
|
213
|
-
if (CODEX_RATE_LIMIT_PROMPT.test(pane)) {
|
|
214
|
-
process.stderr.write('[codex-worker] Auto-dismissing rate-limit model switch prompt\n');
|
|
215
|
-
this.dismissRateLimitPrompt(session);
|
|
216
|
-
return { waiting: false, destructive: false, prompt: '' }; // handled, not waiting
|
|
217
|
-
}
|
|
218
|
-
return { waiting: false, destructive: false, prompt: '' };
|
|
219
|
-
}
|
|
220
|
-
/**
|
|
221
|
-
* Detect completion by checking if Codex returned to › prompt after working.
|
|
222
|
-
*/
|
|
223
|
-
async detectCompleted(session, logDir, _branch) {
|
|
224
|
-
// Priority 1: task_completed marker file (same as Claude)
|
|
225
|
-
const markerPath = `${logDir}/task_completed`;
|
|
226
|
-
if (existsSync(markerPath)) {
|
|
227
|
-
return 'COMPLETED';
|
|
228
|
-
}
|
|
229
|
-
// Priority 2: check for interactive prompts (needs auto-skip, not completion)
|
|
230
|
-
const pane = capturePaneText(session, 20);
|
|
231
|
-
if (CODEX_UPDATE_PROMPT.test(pane)) {
|
|
232
|
-
return 'NEEDS_INPUT'; // will trigger auto-confirm to skip update
|
|
233
|
-
}
|
|
234
|
-
if (CODEX_RATE_LIMIT_PROMPT.test(pane)) {
|
|
235
|
-
// Auto-dismiss and report ALIVE (not completed, not blocked)
|
|
236
|
-
process.stderr.write('[codex-worker] Rate-limit prompt detected during completion check, dismissing\n');
|
|
237
|
-
this.dismissRateLimitPrompt(session);
|
|
238
|
-
return 'ALIVE';
|
|
239
|
-
}
|
|
240
|
-
// Priority 3: completion keywords + back at › prompt
|
|
241
|
-
if (COMPLETION_KEYWORDS.test(pane) && CODEX_READY.test(pane) && CODEX_MODEL_LINE.test(pane)) {
|
|
242
|
-
return 'COMPLETED';
|
|
243
|
-
}
|
|
244
|
-
// Priority 4: session alive
|
|
245
|
-
if (sessionExists(session)) {
|
|
246
|
-
return 'ALIVE';
|
|
247
|
-
}
|
|
248
|
-
return 'DEAD';
|
|
249
|
-
}
|
|
250
|
-
async detectBlocked(session) {
|
|
251
|
-
const pane = capturePaneText(session, 30);
|
|
252
|
-
return /(error|fatal|rate.?limit|quota exceeded)/i.test(pane);
|
|
253
|
-
}
|
|
254
|
-
async sendFix(session, fixPrompt) {
|
|
255
|
-
const escaped = fixPrompt.replace(/'/g, "'\\''");
|
|
256
|
-
tmux(['send-keys', '-t', session, escaped, 'Enter']);
|
|
257
|
-
}
|
|
258
|
-
async resolveConflict(session, worktree, branch) {
|
|
259
|
-
const instruction = [
|
|
260
|
-
`There is a merge conflict on branch ${branch}.`,
|
|
261
|
-
`Working directory: ${worktree}`,
|
|
262
|
-
'Please resolve the conflict:',
|
|
263
|
-
`1. Run: git fetch origin && git rebase origin/${this.config.GITLAB_MERGE_BRANCH}`,
|
|
264
|
-
'2. Resolve any conflicts in the affected files',
|
|
265
|
-
'3. Run: git add . && git rebase --continue',
|
|
266
|
-
'4. Run: git push --force-with-lease',
|
|
267
|
-
].join('\n');
|
|
268
|
-
tmux(['send-keys', '-t', session, instruction, 'Enter']);
|
|
269
|
-
}
|
|
270
|
-
/**
|
|
271
|
-
* Release a worker session after task completion.
|
|
272
|
-
*
|
|
273
|
-
* WORKER_SESSION_REUSE=true: do nothing — keep Codex running so the
|
|
274
|
-
* next task can hot-reuse the session via /clear + cd.
|
|
275
|
-
*
|
|
276
|
-
* WORKER_SESSION_REUSE=false: quit Codex but keep tmux session alive.
|
|
277
|
-
*/
|
|
278
|
-
async release(session) {
|
|
279
|
-
if (!sessionExists(session))
|
|
280
|
-
return;
|
|
281
|
-
if (this.config.WORKER_SESSION_REUSE) {
|
|
282
|
-
// Keep everything alive — next launch() will /clear + /cd + send prompt
|
|
283
|
-
process.stderr.write(`[codex-worker] Session ${session} kept alive for reuse\n`);
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
// Quit Codex but keep tmux session
|
|
287
|
-
tmux(['send-keys', '-t', session, '/quit', 'Enter']);
|
|
288
|
-
}
|
|
289
|
-
/**
|
|
290
|
-
* Force-stop a worker session (error recovery, cleanup).
|
|
291
|
-
* Always quits Codex and kills the tmux session.
|
|
292
|
-
*/
|
|
293
|
-
async stop(session) {
|
|
294
|
-
if (!sessionExists(session))
|
|
295
|
-
return;
|
|
296
|
-
tmux(['send-keys', '-t', session, '/quit', 'Enter']);
|
|
297
|
-
for (let i = 0; i < 5; i++) {
|
|
298
|
-
await this.sleep(1_000);
|
|
299
|
-
if (!sessionExists(session))
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
tmux(['kill-session', '-t', session]);
|
|
303
|
-
}
|
|
304
|
-
async collectSummary(session) {
|
|
305
|
-
return capturePaneText(session, 100);
|
|
306
|
-
}
|
|
307
|
-
/**
|
|
308
|
-
* Dismiss the rate-limit model switch prompt by selecting "Keep current model".
|
|
309
|
-
*
|
|
310
|
-
* The prompt shows:
|
|
311
|
-
* › 1. Switch to gpt-5.1-codex-mini
|
|
312
|
-
* 2. Keep current model
|
|
313
|
-
* 3. Keep current model (never show again)
|
|
314
|
-
*
|
|
315
|
-
* Strategy: press Down once to select option 2, then Enter.
|
|
316
|
-
*/
|
|
317
|
-
dismissRateLimitPrompt(session) {
|
|
318
|
-
// Navigate to "Keep current model" (option 2)
|
|
319
|
-
tmux(['send-keys', '-t', session, 'Down']);
|
|
320
|
-
// Small delay to let the UI register the selection
|
|
321
|
-
tmux(['send-keys', '-t', session, '']);
|
|
322
|
-
// Confirm selection
|
|
323
|
-
setTimeout(() => {
|
|
324
|
-
tmux(['send-keys', '-t', session, 'Enter']);
|
|
325
|
-
}, 500);
|
|
326
|
-
}
|
|
327
|
-
sleep(ms) {
|
|
328
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
329
|
-
}
|
|
330
|
-
}
|
|
5
|
+
export { CodexTmuxProvider as CodexWorkerProvider } from './CodexTmuxProvider.js';
|
|
331
6
|
//# sourceMappingURL=CodexWorkerProvider.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CodexWorkerProvider.js","sourceRoot":"","sources":["../../src/providers/CodexWorkerProvider.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"CodexWorkerProvider.js","sourceRoot":"","sources":["../../src/providers/CodexWorkerProvider.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,iBAAiB,IAAI,mBAAmB,EAAE,MAAM,wBAAwB,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read the last N lines from a file efficiently.
|
|
3
|
+
* Returns empty string if file doesn't exist.
|
|
4
|
+
*/
|
|
5
|
+
export declare function tailFile(filePath: string, lines: number): string;
|
|
6
|
+
/**
|
|
7
|
+
* Get file size in bytes (for checking if output is being written).
|
|
8
|
+
*/
|
|
9
|
+
export declare function fileSize(filePath: string): number;
|
|
10
|
+
/**
|
|
11
|
+
* Parse session ID from a Claude stream-json output file.
|
|
12
|
+
*
|
|
13
|
+
* Claude --output-format stream-json emits lines like:
|
|
14
|
+
* {"type":"result",...,"session_id":"uuid",...}
|
|
15
|
+
*
|
|
16
|
+
* We also check the very first system message.
|
|
17
|
+
*/
|
|
18
|
+
export declare function parseClaudeSessionId(filePath: string): string | null;
|
|
19
|
+
/**
|
|
20
|
+
* Parse session ID from a Codex exec --json JSONL output file.
|
|
21
|
+
*
|
|
22
|
+
* Codex emits JSONL events. Look for session/conversation ID.
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseCodexSessionId(filePath: string): string | null;
|
|
25
|
+
/**
|
|
26
|
+
* Check if a process is alive by sending signal 0.
|
|
27
|
+
*/
|
|
28
|
+
export declare function isProcessAlive(pid: number): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Kill a process group. First SIGTERM, then SIGKILL after timeout.
|
|
31
|
+
* Uses negative PID to signal the entire process group.
|
|
32
|
+
*/
|
|
33
|
+
export declare function killProcessGroup(pid: number, timeoutMs?: number): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Extract the last assistant message text from Claude stream-json output.
|
|
36
|
+
* Useful for verifying task completion.
|
|
37
|
+
*/
|
|
38
|
+
export declare function extractLastAssistantText(filePath: string): string;
|
|
39
|
+
//# sourceMappingURL=outputParser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outputParser.d.ts","sourceRoot":"","sources":["../../src/providers/outputParser.ts"],"names":[],"mappings":"AAMA;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAShE;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMjD;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiBpE;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAoBnE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAQnD;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,SAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBpF;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA2BjE"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for print-mode worker providers.
|
|
3
|
+
* Handles output file tailing, session ID parsing, and process inspection.
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
6
|
+
/**
|
|
7
|
+
* Read the last N lines from a file efficiently.
|
|
8
|
+
* Returns empty string if file doesn't exist.
|
|
9
|
+
*/
|
|
10
|
+
export function tailFile(filePath, lines) {
|
|
11
|
+
if (!existsSync(filePath))
|
|
12
|
+
return '';
|
|
13
|
+
try {
|
|
14
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
15
|
+
const allLines = content.split('\n');
|
|
16
|
+
return allLines.slice(-lines).join('\n');
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return '';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Get file size in bytes (for checking if output is being written).
|
|
24
|
+
*/
|
|
25
|
+
export function fileSize(filePath) {
|
|
26
|
+
try {
|
|
27
|
+
return statSync(filePath).size;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Parse session ID from a Claude stream-json output file.
|
|
35
|
+
*
|
|
36
|
+
* Claude --output-format stream-json emits lines like:
|
|
37
|
+
* {"type":"result",...,"session_id":"uuid",...}
|
|
38
|
+
*
|
|
39
|
+
* We also check the very first system message.
|
|
40
|
+
*/
|
|
41
|
+
export function parseClaudeSessionId(filePath) {
|
|
42
|
+
if (!existsSync(filePath))
|
|
43
|
+
return null;
|
|
44
|
+
try {
|
|
45
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
46
|
+
for (const line of content.split('\n')) {
|
|
47
|
+
if (!line.trim())
|
|
48
|
+
continue;
|
|
49
|
+
try {
|
|
50
|
+
const obj = JSON.parse(line);
|
|
51
|
+
if (obj.session_id)
|
|
52
|
+
return obj.session_id;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// not valid JSON, skip
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// file read error
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Parse session ID from a Codex exec --json JSONL output file.
|
|
66
|
+
*
|
|
67
|
+
* Codex emits JSONL events. Look for session/conversation ID.
|
|
68
|
+
*/
|
|
69
|
+
export function parseCodexSessionId(filePath) {
|
|
70
|
+
if (!existsSync(filePath))
|
|
71
|
+
return null;
|
|
72
|
+
try {
|
|
73
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
74
|
+
for (const line of content.split('\n')) {
|
|
75
|
+
if (!line.trim())
|
|
76
|
+
continue;
|
|
77
|
+
try {
|
|
78
|
+
const obj = JSON.parse(line);
|
|
79
|
+
// Codex uses conversation_id or session_id
|
|
80
|
+
if (obj.conversation_id)
|
|
81
|
+
return obj.conversation_id;
|
|
82
|
+
if (obj.session_id)
|
|
83
|
+
return obj.session_id;
|
|
84
|
+
if (obj.id && typeof obj.id === 'string' && obj.type === 'session_start')
|
|
85
|
+
return obj.id;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// not valid JSON
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// file read error
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Check if a process is alive by sending signal 0.
|
|
99
|
+
*/
|
|
100
|
+
export function isProcessAlive(pid) {
|
|
101
|
+
if (!pid || pid <= 0)
|
|
102
|
+
return false;
|
|
103
|
+
try {
|
|
104
|
+
process.kill(pid, 0);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Kill a process group. First SIGTERM, then SIGKILL after timeout.
|
|
113
|
+
* Uses negative PID to signal the entire process group.
|
|
114
|
+
*/
|
|
115
|
+
export async function killProcessGroup(pid, timeoutMs = 5_000) {
|
|
116
|
+
if (!isProcessAlive(pid))
|
|
117
|
+
return;
|
|
118
|
+
try {
|
|
119
|
+
// Signal the process group
|
|
120
|
+
process.kill(-pid, 'SIGTERM');
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Process group may not exist, try direct kill
|
|
124
|
+
try {
|
|
125
|
+
process.kill(pid, 'SIGTERM');
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Wait for graceful shutdown
|
|
132
|
+
const deadline = Date.now() + timeoutMs;
|
|
133
|
+
while (Date.now() < deadline) {
|
|
134
|
+
if (!isProcessAlive(pid))
|
|
135
|
+
return;
|
|
136
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
137
|
+
}
|
|
138
|
+
// Force kill
|
|
139
|
+
try {
|
|
140
|
+
process.kill(-pid, 'SIGKILL');
|
|
141
|
+
}
|
|
142
|
+
catch { /* ignore */ }
|
|
143
|
+
try {
|
|
144
|
+
process.kill(pid, 'SIGKILL');
|
|
145
|
+
}
|
|
146
|
+
catch { /* ignore */ }
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Extract the last assistant message text from Claude stream-json output.
|
|
150
|
+
* Useful for verifying task completion.
|
|
151
|
+
*/
|
|
152
|
+
export function extractLastAssistantText(filePath) {
|
|
153
|
+
if (!existsSync(filePath))
|
|
154
|
+
return '';
|
|
155
|
+
try {
|
|
156
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
157
|
+
const lines = content.split('\n').filter((l) => l.trim());
|
|
158
|
+
let lastText = '';
|
|
159
|
+
for (const line of lines) {
|
|
160
|
+
try {
|
|
161
|
+
const obj = JSON.parse(line);
|
|
162
|
+
// Claude stream-json assistant content messages
|
|
163
|
+
if (obj.type === 'assistant' && typeof obj.message?.content === 'string') {
|
|
164
|
+
lastText = obj.message.content;
|
|
165
|
+
}
|
|
166
|
+
// Also check result type
|
|
167
|
+
if (obj.type === 'result' && typeof obj.result === 'string') {
|
|
168
|
+
lastText = obj.result;
|
|
169
|
+
}
|
|
170
|
+
// Content block text
|
|
171
|
+
if (obj.type === 'content_block_delta' && obj.delta?.text) {
|
|
172
|
+
lastText += obj.delta.text;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch { /* skip */ }
|
|
176
|
+
}
|
|
177
|
+
return lastText;
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return '';
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=outputParser.js.map
|