@coralai/sps-cli 0.10.2 → 0.11.1
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/README.md +63 -23
- 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 +80 -16
- 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/models/types.d.ts +3 -1
- package/dist/models/types.d.ts.map +1 -1
- package/dist/providers/ClaudePrintProvider.d.ts +54 -0
- package/dist/providers/ClaudePrintProvider.d.ts.map +1 -0
- package/dist/providers/ClaudePrintProvider.js +279 -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 +36 -0
- package/dist/providers/CodexExecProvider.d.ts.map +1 -0
- package/dist/providers/CodexExecProvider.js +238 -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 +50 -0
- package/dist/providers/outputParser.d.ts.map +1 -0
- package/dist/providers/outputParser.js +219 -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,306 +1,6 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
-
/** Completion keywords detected in pane text (priority 3). */
|
|
4
|
-
const COMPLETION_KEYWORDS = /\b(done|完成|全部完成|MR created|merge request|已提交|已推送)\b|🎉/i;
|
|
5
|
-
/** Confirmation prompt patterns (priority 2 / detectWaiting). */
|
|
6
|
-
const CONFIRMATION_PROMPT = /(Do you want to proceed|y\/n|press enter|confirm|approve)/i;
|
|
7
|
-
/** Destructive operation indicators. */
|
|
8
|
-
const DESTRUCTIVE_PATTERN = /(delete|remove|drop|rm -rf|truncate|destroy)/i;
|
|
9
|
-
/** Blocked indicators in pane text. */
|
|
10
|
-
const BLOCKED_PATTERN = /(error|fatal|panic|BLOCKED|stuck|cannot proceed|timed out|rate.?limit)/i;
|
|
11
1
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* On "server exited unexpectedly", auto-cleans stale socket and retries once.
|
|
2
|
+
* @deprecated Use ClaudeTmuxProvider or ClaudePrintProvider directly.
|
|
3
|
+
* This re-export exists for backward compatibility with existing imports.
|
|
15
4
|
*/
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
return execFileSync('tmux', args, {
|
|
19
|
-
encoding: 'utf-8',
|
|
20
|
-
timeout: 10_000,
|
|
21
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
catch (err) {
|
|
25
|
-
const stderr = err.stderr ?? '';
|
|
26
|
-
if (stderr.includes('server exited unexpectedly') || stderr.includes('no server running')) {
|
|
27
|
-
// Stale tmux socket — clean up and retry
|
|
28
|
-
try {
|
|
29
|
-
const uid = process.getuid?.() ?? 1000;
|
|
30
|
-
const { rmSync } = require('node:fs');
|
|
31
|
-
rmSync(`/tmp/tmux-${uid}`, { recursive: true, force: true });
|
|
32
|
-
process.stderr.write('[worker] Cleaned stale tmux socket, retrying\n');
|
|
33
|
-
return execFileSync('tmux', args, {
|
|
34
|
-
encoding: 'utf-8',
|
|
35
|
-
timeout: 10_000,
|
|
36
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
/** Check whether a tmux session exists. */
|
|
47
|
-
function sessionExists(session) {
|
|
48
|
-
return tmux(['has-session', '-t', session]) !== null;
|
|
49
|
-
}
|
|
50
|
-
/** Capture recent pane text from a tmux session. */
|
|
51
|
-
function capturePaneText(session, lines) {
|
|
52
|
-
return tmux(['capture-pane', '-t', session, '-p', '-S', `-${lines}`]) ?? '';
|
|
53
|
-
}
|
|
54
|
-
export class ClaudeWorkerProvider {
|
|
55
|
-
config;
|
|
56
|
-
constructor(config) {
|
|
57
|
-
this.config = config;
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Ensure worktree directory exists and is clean.
|
|
61
|
-
* This is largely a no-op when the worktree is already prepared.
|
|
62
|
-
*/
|
|
63
|
-
async prepareEnv(worktree, _seq) {
|
|
64
|
-
if (!existsSync(worktree)) {
|
|
65
|
-
throw new Error(`Worktree directory does not exist: ${worktree}`);
|
|
66
|
-
}
|
|
67
|
-
// Verify the directory is a git worktree / repo
|
|
68
|
-
try {
|
|
69
|
-
execFileSync('git', ['-C', worktree, 'rev-parse', '--is-inside-work-tree'], {
|
|
70
|
-
encoding: 'utf-8',
|
|
71
|
-
timeout: 5_000,
|
|
72
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
throw new Error(`Directory is not a git worktree: ${worktree}`);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Launch a Claude Code worker inside a tmux session.
|
|
81
|
-
*
|
|
82
|
-
* Session reuse strategy (WORKER_SESSION_REUSE=true):
|
|
83
|
-
* 1. Session exists + Claude running → reuse: /clear + cd worktree (keep context hot)
|
|
84
|
-
* 2. Session exists + Claude not running → reuse session: cd + start claude
|
|
85
|
-
* 3. No session → create new session + start claude
|
|
86
|
-
*/
|
|
87
|
-
async launch(session, worktree) {
|
|
88
|
-
const claudeCmd = 'claude --dangerously-skip-permissions';
|
|
89
|
-
if (sessionExists(session)) {
|
|
90
|
-
const pane = capturePaneText(session, 10);
|
|
91
|
-
const claudeAlive = /❯\s*$/m.test(pane) || /bypass permissions/i.test(pane) || /shortcuts/i.test(pane);
|
|
92
|
-
if (claudeAlive) {
|
|
93
|
-
// Claude is running — reuse instance: clear conversation + switch worktree
|
|
94
|
-
this.log.info(`Reusing live Claude session ${session}`);
|
|
95
|
-
tmux(['send-keys', '-t', session, '/clear', 'Enter']);
|
|
96
|
-
await this.sleep(1_000);
|
|
97
|
-
tmux(['send-keys', '-t', session, `cd ${worktree}`, 'Enter']);
|
|
98
|
-
await this.sleep(500);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
// Session exists but Claude not running — cd and start claude
|
|
102
|
-
this.log.info(`Reusing tmux session ${session} (Claude not running)`);
|
|
103
|
-
tmux(['send-keys', '-t', session, `cd ${worktree}`, 'Enter']);
|
|
104
|
-
await this.sleep(500);
|
|
105
|
-
tmux(['send-keys', '-t', session, claudeCmd, 'Enter']);
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
// No session — create new
|
|
109
|
-
const result = tmux(['new-session', '-d', '-s', session, '-c', worktree]);
|
|
110
|
-
if (result === null && !sessionExists(session)) {
|
|
111
|
-
throw new Error(`Failed to create tmux session: ${session}`);
|
|
112
|
-
}
|
|
113
|
-
tmux(['send-keys', '-t', session, claudeCmd, 'Enter']);
|
|
114
|
-
}
|
|
115
|
-
log = { info: (msg) => process.stderr.write(`[worker] ${msg}\n`) };
|
|
116
|
-
/**
|
|
117
|
-
* Poll tmux pane text until Claude's ready prompt appears.
|
|
118
|
-
* Default timeout: 30 seconds, poll interval: 2 seconds.
|
|
119
|
-
*/
|
|
120
|
-
async waitReady(session, timeoutMs = 30_000) {
|
|
121
|
-
const pollInterval = 2_000;
|
|
122
|
-
const deadline = Date.now() + timeoutMs;
|
|
123
|
-
// Wait at least 3s for Claude to start loading before polling
|
|
124
|
-
await this.sleep(3_000);
|
|
125
|
-
while (Date.now() < deadline) {
|
|
126
|
-
const text = capturePaneText(session, 15);
|
|
127
|
-
// Match Claude Code's actual ready state:
|
|
128
|
-
// - The ❯ prompt on its own line (Claude's input prompt)
|
|
129
|
-
// - "bypass permissions" indicator (only appears after Claude is fully loaded)
|
|
130
|
-
// - "? for shortcuts" (appears at bottom when ready)
|
|
131
|
-
if (/bypass permissions/i.test(text) ||
|
|
132
|
-
/\? for shortcuts/i.test(text) ||
|
|
133
|
-
/tips for shortcuts/i.test(text)) {
|
|
134
|
-
return true;
|
|
135
|
-
}
|
|
136
|
-
// Also match the ❯ prompt but only if Claude banner has appeared
|
|
137
|
-
if (/Claude Code/i.test(text) && /❯\s*$/m.test(text)) {
|
|
138
|
-
return true;
|
|
139
|
-
}
|
|
140
|
-
await this.sleep(pollInterval);
|
|
141
|
-
}
|
|
142
|
-
return false;
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* Send a task prompt file to the Claude session.
|
|
146
|
-
*/
|
|
147
|
-
async sendTask(session, promptFile) {
|
|
148
|
-
if (!existsSync(promptFile)) {
|
|
149
|
-
throw new Error(`Prompt file does not exist: ${promptFile}`);
|
|
150
|
-
}
|
|
151
|
-
// Write prompt content to a temp file, load into tmux buffer, paste, then Enter.
|
|
152
|
-
const content = readFileSync(promptFile, 'utf-8').trim();
|
|
153
|
-
const bufferFile = `/tmp/sps-task-${Date.now()}.txt`;
|
|
154
|
-
const { writeFileSync: writeTmp, unlinkSync } = await import('node:fs');
|
|
155
|
-
writeTmp(bufferFile, content);
|
|
156
|
-
tmux(['load-buffer', bufferFile]);
|
|
157
|
-
tmux(['paste-buffer', '-t', session]);
|
|
158
|
-
try {
|
|
159
|
-
unlinkSync(bufferFile);
|
|
160
|
-
}
|
|
161
|
-
catch { /* cleanup */ }
|
|
162
|
-
// Small delay to let paste complete before sending Enter
|
|
163
|
-
await this.sleep(500);
|
|
164
|
-
tmux(['send-keys', '-t', session, 'Enter']);
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* Inspect a tmux session: check if alive and capture pane text.
|
|
168
|
-
*/
|
|
169
|
-
async inspect(session) {
|
|
170
|
-
const alive = sessionExists(session);
|
|
171
|
-
const paneText = alive ? capturePaneText(session, 50) : '';
|
|
172
|
-
return { alive, paneText };
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Multi-layer completion detection chain.
|
|
176
|
-
*
|
|
177
|
-
* Priority order:
|
|
178
|
-
* 1. task_completed marker file in logDir
|
|
179
|
-
* 2. Waiting for confirmation prompts (delegates to detectWaiting)
|
|
180
|
-
* 3. Completion keywords in pane text
|
|
181
|
-
* 4. MR exists on GitLab (skipped — returns ALIVE)
|
|
182
|
-
* 5. tmux session alive → ALIVE
|
|
183
|
-
* 6. Session dead + restart limit exceeded → DEAD_EXCEEDED
|
|
184
|
-
*/
|
|
185
|
-
async detectCompleted(session, logDir, _branch) {
|
|
186
|
-
// Priority 1: task_completed marker file
|
|
187
|
-
const markerPath = `${logDir}/task_completed`;
|
|
188
|
-
if (existsSync(markerPath)) {
|
|
189
|
-
return 'COMPLETED';
|
|
190
|
-
}
|
|
191
|
-
// Priority 2: waiting for confirmation
|
|
192
|
-
const waitState = await this.detectWaiting(session);
|
|
193
|
-
if (waitState.waiting) {
|
|
194
|
-
return waitState.destructive ? 'NEEDS_INPUT' : 'AUTO_CONFIRM';
|
|
195
|
-
}
|
|
196
|
-
// Priority 3: completion keywords in pane text
|
|
197
|
-
const paneText = capturePaneText(session, 50);
|
|
198
|
-
if (COMPLETION_KEYWORDS.test(paneText)) {
|
|
199
|
-
return 'COMPLETED';
|
|
200
|
-
}
|
|
201
|
-
// Priority 4: MR exists (skipped for now)
|
|
202
|
-
// Priority 5: session alive
|
|
203
|
-
if (sessionExists(session)) {
|
|
204
|
-
return 'ALIVE';
|
|
205
|
-
}
|
|
206
|
-
// Priority 6: session dead — check restart limit
|
|
207
|
-
// The restart count is tracked externally by the engine via state.json.
|
|
208
|
-
// Here we simply report DEAD_EXCEEDED vs DEAD so the engine can decide.
|
|
209
|
-
// Without access to the restart counter, report DEAD and let the caller
|
|
210
|
-
// escalate to DEAD_EXCEEDED if the limit is reached.
|
|
211
|
-
return 'DEAD';
|
|
212
|
-
}
|
|
213
|
-
/**
|
|
214
|
-
* Detect whether the worker is waiting for user confirmation.
|
|
215
|
-
* Returns whether the prompt is destructive (delete/remove/drop etc.).
|
|
216
|
-
*/
|
|
217
|
-
async detectWaiting(session) {
|
|
218
|
-
const paneText = capturePaneText(session, 30);
|
|
219
|
-
const match = paneText.match(CONFIRMATION_PROMPT);
|
|
220
|
-
if (!match) {
|
|
221
|
-
return { waiting: false, destructive: false, prompt: '' };
|
|
222
|
-
}
|
|
223
|
-
// Extract the line containing the prompt for context
|
|
224
|
-
const lines = paneText.split('\n');
|
|
225
|
-
const promptLine = lines.find((l) => CONFIRMATION_PROMPT.test(l))?.trim() ?? match[0];
|
|
226
|
-
const destructive = DESTRUCTIVE_PATTERN.test(paneText);
|
|
227
|
-
return { waiting: true, destructive, prompt: promptLine };
|
|
228
|
-
}
|
|
229
|
-
/**
|
|
230
|
-
* Check pane text for blocked indicators (errors, stuck states).
|
|
231
|
-
*/
|
|
232
|
-
async detectBlocked(session) {
|
|
233
|
-
const paneText = capturePaneText(session, 30);
|
|
234
|
-
return BLOCKED_PATTERN.test(paneText);
|
|
235
|
-
}
|
|
236
|
-
/**
|
|
237
|
-
* Send a fix prompt to the Claude session (e.g. after CI failure).
|
|
238
|
-
*/
|
|
239
|
-
async sendFix(session, fixPrompt) {
|
|
240
|
-
// Escape any single quotes in the prompt for safe tmux transmission
|
|
241
|
-
const escaped = fixPrompt.replace(/'/g, "'\\''");
|
|
242
|
-
tmux(['send-keys', '-t', session, escaped, 'Enter']);
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* Send conflict resolution instructions to the Claude session.
|
|
246
|
-
*/
|
|
247
|
-
async resolveConflict(session, worktree, branch) {
|
|
248
|
-
const instruction = [
|
|
249
|
-
`There is a merge conflict on branch ${branch}.`,
|
|
250
|
-
`Working directory: ${worktree}`,
|
|
251
|
-
'Please resolve the conflict:',
|
|
252
|
-
`1. Run: git fetch origin && git rebase origin/${this.config.GITLAB_MERGE_BRANCH}`,
|
|
253
|
-
'2. Resolve any conflicts in the affected files',
|
|
254
|
-
'3. Run: git add . && git rebase --continue',
|
|
255
|
-
'4. Run: git push --force-with-lease',
|
|
256
|
-
].join('\n');
|
|
257
|
-
tmux(['send-keys', '-t', session, instruction, 'Enter']);
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* Release a worker session after task completion.
|
|
261
|
-
*
|
|
262
|
-
* WORKER_SESSION_REUSE=true: do nothing — keep Claude running so the
|
|
263
|
-
* next task can hot-reuse the session via /clear + cd (preserves
|
|
264
|
-
* session state, env vars, loaded MCP servers, etc.).
|
|
265
|
-
*
|
|
266
|
-
* WORKER_SESSION_REUSE=false: exit Claude but keep tmux session alive
|
|
267
|
-
* (next launch will restart Claude in the existing session).
|
|
268
|
-
*/
|
|
269
|
-
async release(session) {
|
|
270
|
-
if (!sessionExists(session))
|
|
271
|
-
return;
|
|
272
|
-
if (this.config.WORKER_SESSION_REUSE) {
|
|
273
|
-
// Keep everything alive — next launch() will /clear + cd + send prompt
|
|
274
|
-
this.log.info(`Session ${session} kept alive for reuse`);
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
// Exit Claude but keep tmux session
|
|
278
|
-
tmux(['send-keys', '-t', session, '/exit', 'Enter']);
|
|
279
|
-
}
|
|
280
|
-
/**
|
|
281
|
-
* Force-stop a worker session (error recovery, cleanup).
|
|
282
|
-
* Always exits Claude and kills the tmux session.
|
|
283
|
-
*/
|
|
284
|
-
async stop(session) {
|
|
285
|
-
if (!sessionExists(session))
|
|
286
|
-
return;
|
|
287
|
-
tmux(['send-keys', '-t', session, '/exit', 'Enter']);
|
|
288
|
-
for (let i = 0; i < 5; i++) {
|
|
289
|
-
await this.sleep(1_000);
|
|
290
|
-
if (!sessionExists(session))
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
tmux(['kill-session', '-t', session]);
|
|
294
|
-
}
|
|
295
|
-
/**
|
|
296
|
-
* Capture the last 100 lines of pane text as a summary.
|
|
297
|
-
*/
|
|
298
|
-
async collectSummary(session) {
|
|
299
|
-
return capturePaneText(session, 100);
|
|
300
|
-
}
|
|
301
|
-
/** Helper: sleep for the given milliseconds. */
|
|
302
|
-
sleep(ms) {
|
|
303
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
304
|
-
}
|
|
305
|
-
}
|
|
5
|
+
export { ClaudeTmuxProvider as ClaudeWorkerProvider } from './ClaudeTmuxProvider.js';
|
|
306
6
|
//# sourceMappingURL=ClaudeWorkerProvider.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ClaudeWorkerProvider.js","sourceRoot":"","sources":["../../src/providers/ClaudeWorkerProvider.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"ClaudeWorkerProvider.js","sourceRoot":"","sources":["../../src/providers/ClaudeWorkerProvider.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,kBAAkB,IAAI,oBAAoB,EAAE,MAAM,yBAAyB,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ProjectConfig } from '../core/config.js';
|
|
2
|
+
import type { WorkerProvider, LaunchResult } from '../interfaces/WorkerProvider.js';
|
|
3
|
+
import type { WorkerStatus } from '../models/types.js';
|
|
4
|
+
export declare class CodexExecProvider implements WorkerProvider {
|
|
5
|
+
private readonly config;
|
|
6
|
+
constructor(config: ProjectConfig);
|
|
7
|
+
prepareEnv(worktree: string, _seq: string): Promise<void>;
|
|
8
|
+
launch(session: string, worktree: string, promptFile: string): Promise<LaunchResult>;
|
|
9
|
+
inspect(session: string): Promise<{
|
|
10
|
+
alive: boolean;
|
|
11
|
+
paneText: string;
|
|
12
|
+
pid?: number;
|
|
13
|
+
exitCode?: number;
|
|
14
|
+
}>;
|
|
15
|
+
detectCompleted(session: string, logDir: string, branch: string): Promise<WorkerStatus>;
|
|
16
|
+
detectWaiting(_session: string): Promise<{
|
|
17
|
+
waiting: boolean;
|
|
18
|
+
destructive: boolean;
|
|
19
|
+
prompt: string;
|
|
20
|
+
}>;
|
|
21
|
+
detectBlocked(_session: string): Promise<boolean>;
|
|
22
|
+
sendFix(session: string, fixPrompt: string, resumeSessionId?: string): Promise<LaunchResult>;
|
|
23
|
+
resolveConflict(session: string, worktree: string, branch: string, resumeSessionId?: string): Promise<LaunchResult>;
|
|
24
|
+
release(_session: string): Promise<void>;
|
|
25
|
+
stop(session: string): Promise<void>;
|
|
26
|
+
collectSummary(session: string): Promise<string>;
|
|
27
|
+
private spawnCodex;
|
|
28
|
+
extractSessionIdAsync(session: string): Promise<string | null>;
|
|
29
|
+
/**
|
|
30
|
+
* Look up worker slot info from state.json by tmuxSession name.
|
|
31
|
+
* Fallback when activeProcesses map is empty (SPS restarted).
|
|
32
|
+
*/
|
|
33
|
+
private findSlotBySession;
|
|
34
|
+
private log;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=CodexExecProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CodexExecProvider.d.ts","sourceRoot":"","sources":["../../src/providers/CodexExecProvider.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AACpF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAwBvD,qBAAa,iBAAkB,YAAW,cAAc;IACtD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;gBAE3B,MAAM,EAAE,aAAa;IAI3B,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAazD,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAcpF,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;QACtC,KAAK,EAAE,OAAO,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IA6BI,eAAe,CACnB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,YAAY,CAAC;IA2ClB,aAAa,CACjB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,WAAW,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAKhE,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAIjD,OAAO,CACX,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,eAAe,CAAC,EAAE,MAAM,GACvB,OAAO,CAAC,YAAY,CAAC;IAUlB,eAAe,CACnB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,eAAe,CAAC,EAAE,MAAM,GACvB,OAAO,CAAC,YAAY,CAAC;IAmBlB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIxC,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWpC,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAQtD,OAAO,CAAC,UAAU;IAkEZ,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAOpE;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAgCzB,OAAO,CAAC,GAAG;CAGZ"}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodexExecProvider — one-shot print-mode worker using `codex exec`.
|
|
3
|
+
*
|
|
4
|
+
* Eliminates all tmux interaction. Process lifecycle = task lifecycle.
|
|
5
|
+
* Uses `codex exec resume <sessionId>` for context continuity.
|
|
6
|
+
*/
|
|
7
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
8
|
+
import { existsSync, readFileSync, createWriteStream } from 'node:fs';
|
|
9
|
+
import { resolve } from 'node:path';
|
|
10
|
+
import { tailFile, parseCodexSessionId, isProcessAlive, killProcessGroup, branchCommitsAhead, branchPushed, } from './outputParser.js';
|
|
11
|
+
import { readState } from '../core/state.js';
|
|
12
|
+
/** Completion indicators. */
|
|
13
|
+
const COMPLETION_KEYWORDS = /\b(done|completed|finished|committed|pushed|MR created|merge request)\b/i;
|
|
14
|
+
/**
|
|
15
|
+
* Track spawned child processes by session name.
|
|
16
|
+
*/
|
|
17
|
+
const activeProcesses = new Map();
|
|
18
|
+
export class CodexExecProvider {
|
|
19
|
+
config;
|
|
20
|
+
constructor(config) {
|
|
21
|
+
this.config = config;
|
|
22
|
+
}
|
|
23
|
+
async prepareEnv(worktree, _seq) {
|
|
24
|
+
if (!existsSync(worktree)) {
|
|
25
|
+
throw new Error(`Worktree directory does not exist: ${worktree}`);
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
execFileSync('git', ['-C', worktree, 'rev-parse', '--is-inside-work-tree'], {
|
|
29
|
+
encoding: 'utf-8', timeout: 5_000, stdio: ['ignore', 'pipe', 'pipe'],
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw new Error(`Directory is not a git worktree: ${worktree}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async launch(session, worktree, promptFile) {
|
|
37
|
+
if (!existsSync(promptFile)) {
|
|
38
|
+
throw new Error(`Prompt file does not exist: ${promptFile}`);
|
|
39
|
+
}
|
|
40
|
+
const prompt = readFileSync(promptFile, 'utf-8').trim();
|
|
41
|
+
const outputFile = resolve(this.config.raw.LOGS_DIR || `/tmp/sps-${this.config.PROJECT_NAME}`, `${session}-${Date.now()}.jsonl`);
|
|
42
|
+
return this.spawnCodex(session, worktree, prompt, outputFile);
|
|
43
|
+
}
|
|
44
|
+
async inspect(session) {
|
|
45
|
+
const proc = activeProcesses.get(session);
|
|
46
|
+
if (proc) {
|
|
47
|
+
const pid = proc.child.pid ?? 0;
|
|
48
|
+
const alive = pid > 0 && isProcessAlive(pid);
|
|
49
|
+
return {
|
|
50
|
+
alive,
|
|
51
|
+
paneText: tailFile(proc.outputFile, 50),
|
|
52
|
+
pid,
|
|
53
|
+
exitCode: proc.exitCode ?? undefined,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// Fallback: recover from state.json
|
|
57
|
+
const slotInfo = this.findSlotBySession(session);
|
|
58
|
+
if (slotInfo?.pid) {
|
|
59
|
+
const alive = isProcessAlive(slotInfo.pid);
|
|
60
|
+
const paneText = slotInfo.outputFile ? tailFile(slotInfo.outputFile, 50) : '';
|
|
61
|
+
return {
|
|
62
|
+
alive,
|
|
63
|
+
paneText,
|
|
64
|
+
pid: slotInfo.pid,
|
|
65
|
+
exitCode: alive ? undefined : (slotInfo.exitCode ?? undefined),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return { alive: false, paneText: '', pid: undefined, exitCode: undefined };
|
|
69
|
+
}
|
|
70
|
+
async detectCompleted(session, logDir, branch) {
|
|
71
|
+
const markerPath = `${logDir}/task_completed`;
|
|
72
|
+
if (existsSync(markerPath)) {
|
|
73
|
+
return 'COMPLETED';
|
|
74
|
+
}
|
|
75
|
+
// Resolve process info — in-memory or state.json fallback
|
|
76
|
+
const proc = activeProcesses.get(session);
|
|
77
|
+
const slotInfo = !proc ? this.findSlotBySession(session) : null;
|
|
78
|
+
const pid = proc?.child.pid ?? slotInfo?.pid ?? 0;
|
|
79
|
+
const exitCode = proc?.exitCode ?? slotInfo?.exitCode ?? null;
|
|
80
|
+
if (!pid && !proc) {
|
|
81
|
+
return 'DEAD';
|
|
82
|
+
}
|
|
83
|
+
// Process still running
|
|
84
|
+
if (pid > 0 && isProcessAlive(pid)) {
|
|
85
|
+
return 'ALIVE';
|
|
86
|
+
}
|
|
87
|
+
// Process exited — verify with git artifacts
|
|
88
|
+
const worktree = slotInfo?.worktree ?? null;
|
|
89
|
+
const baseBranch = this.config.GITLAB_MERGE_BRANCH;
|
|
90
|
+
if (worktree && branch) {
|
|
91
|
+
const pushed = branchPushed(worktree, branch);
|
|
92
|
+
const commitsAhead = pushed
|
|
93
|
+
? branchCommitsAhead(worktree, branch, baseBranch)
|
|
94
|
+
: 0;
|
|
95
|
+
if (pushed && commitsAhead > 0) {
|
|
96
|
+
return 'COMPLETED';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (exitCode === 0) {
|
|
100
|
+
return 'EXITED_INCOMPLETE';
|
|
101
|
+
}
|
|
102
|
+
return 'DEAD';
|
|
103
|
+
}
|
|
104
|
+
async detectWaiting(_session) {
|
|
105
|
+
// codex exec with --dangerously-bypass-approvals-and-sandbox never waits
|
|
106
|
+
return { waiting: false, destructive: false, prompt: '' };
|
|
107
|
+
}
|
|
108
|
+
async detectBlocked(_session) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
async sendFix(session, fixPrompt, resumeSessionId) {
|
|
112
|
+
const worktree = '.';
|
|
113
|
+
const outputFile = resolve(this.config.raw.LOGS_DIR || `/tmp/sps-${this.config.PROJECT_NAME}`, `${session}-fix-${Date.now()}.jsonl`);
|
|
114
|
+
return this.spawnCodex(session, worktree, fixPrompt, outputFile, resumeSessionId);
|
|
115
|
+
}
|
|
116
|
+
async resolveConflict(session, worktree, branch, resumeSessionId) {
|
|
117
|
+
const instruction = [
|
|
118
|
+
`There is a merge conflict on branch ${branch}.`,
|
|
119
|
+
`Working directory: ${worktree}`,
|
|
120
|
+
'Please resolve the conflict:',
|
|
121
|
+
`1. Run: git fetch origin && git rebase origin/${this.config.GITLAB_MERGE_BRANCH}`,
|
|
122
|
+
'2. Resolve any conflicts in the affected files',
|
|
123
|
+
'3. Run: git add . && git rebase --continue',
|
|
124
|
+
'4. Run: git push --force-with-lease',
|
|
125
|
+
].join('\n');
|
|
126
|
+
const outputFile = resolve(this.config.raw.LOGS_DIR || `/tmp/sps-${this.config.PROJECT_NAME}`, `${session}-conflict-${Date.now()}.jsonl`);
|
|
127
|
+
return this.spawnCodex(session, worktree, instruction, outputFile, resumeSessionId);
|
|
128
|
+
}
|
|
129
|
+
async release(_session) {
|
|
130
|
+
activeProcesses.delete(_session);
|
|
131
|
+
}
|
|
132
|
+
async stop(session) {
|
|
133
|
+
const proc = activeProcesses.get(session);
|
|
134
|
+
if (!proc)
|
|
135
|
+
return;
|
|
136
|
+
const pid = proc.child.pid;
|
|
137
|
+
if (pid && isProcessAlive(pid)) {
|
|
138
|
+
await killProcessGroup(pid);
|
|
139
|
+
}
|
|
140
|
+
activeProcesses.delete(session);
|
|
141
|
+
}
|
|
142
|
+
async collectSummary(session) {
|
|
143
|
+
const proc = activeProcesses.get(session);
|
|
144
|
+
if (!proc)
|
|
145
|
+
return '';
|
|
146
|
+
return tailFile(proc.outputFile, 100);
|
|
147
|
+
}
|
|
148
|
+
// ─── Internal ────────────────────────────────────────────────────
|
|
149
|
+
spawnCodex(session, worktree, prompt, outputFile, resumeSessionId) {
|
|
150
|
+
// Ensure output directory exists
|
|
151
|
+
const { mkdirSync } = require('node:fs');
|
|
152
|
+
const { dirname } = require('node:path');
|
|
153
|
+
try {
|
|
154
|
+
mkdirSync(dirname(outputFile), { recursive: true });
|
|
155
|
+
}
|
|
156
|
+
catch { /* exists */ }
|
|
157
|
+
const outStream = createWriteStream(outputFile, { flags: 'a' });
|
|
158
|
+
let args;
|
|
159
|
+
if (resumeSessionId) {
|
|
160
|
+
// codex exec resume <session_id> "prompt" --json ...
|
|
161
|
+
args = [
|
|
162
|
+
'exec', 'resume', resumeSessionId, '-',
|
|
163
|
+
'--json',
|
|
164
|
+
'--dangerously-bypass-approvals-and-sandbox',
|
|
165
|
+
];
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
// codex exec "prompt" --json ...
|
|
169
|
+
args = [
|
|
170
|
+
'exec', '-',
|
|
171
|
+
'--json',
|
|
172
|
+
'--dangerously-bypass-approvals-and-sandbox',
|
|
173
|
+
];
|
|
174
|
+
}
|
|
175
|
+
const child = spawn('codex', args, {
|
|
176
|
+
cwd: worktree,
|
|
177
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
178
|
+
detached: true,
|
|
179
|
+
env: { ...process.env },
|
|
180
|
+
});
|
|
181
|
+
child.stdout?.pipe(outStream);
|
|
182
|
+
child.stderr?.on('data', (chunk) => {
|
|
183
|
+
outStream.write(chunk);
|
|
184
|
+
});
|
|
185
|
+
// Write prompt to stdin and close
|
|
186
|
+
child.stdin?.write(prompt);
|
|
187
|
+
child.stdin?.end();
|
|
188
|
+
const entry = { child, outputFile, exitCode: null };
|
|
189
|
+
activeProcesses.set(session, entry);
|
|
190
|
+
child.on('exit', (code) => {
|
|
191
|
+
entry.exitCode = code ?? 1;
|
|
192
|
+
outStream.end();
|
|
193
|
+
});
|
|
194
|
+
child.unref();
|
|
195
|
+
this.log(`Spawned codex exec for ${session} (pid=${child.pid}), output=${outputFile}`);
|
|
196
|
+
return {
|
|
197
|
+
pid: child.pid ?? 0,
|
|
198
|
+
outputFile,
|
|
199
|
+
sessionId: resumeSessionId,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
async extractSessionIdAsync(session) {
|
|
203
|
+
await new Promise((r) => setTimeout(r, 5_000));
|
|
204
|
+
const proc = activeProcesses.get(session);
|
|
205
|
+
if (!proc)
|
|
206
|
+
return null;
|
|
207
|
+
return parseCodexSessionId(proc.outputFile);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Look up worker slot info from state.json by tmuxSession name.
|
|
211
|
+
* Fallback when activeProcesses map is empty (SPS restarted).
|
|
212
|
+
*/
|
|
213
|
+
findSlotBySession(session) {
|
|
214
|
+
try {
|
|
215
|
+
const stateFile = resolve(process.env.HOME || '~', '.projects', this.config.PROJECT_NAME, 'runtime', 'state.json');
|
|
216
|
+
if (!existsSync(stateFile))
|
|
217
|
+
return null;
|
|
218
|
+
const state = readState(stateFile, this.config.MAX_CONCURRENT_WORKERS);
|
|
219
|
+
for (const slot of Object.values(state.workers)) {
|
|
220
|
+
if (slot.tmuxSession === session && slot.mode === 'print') {
|
|
221
|
+
return {
|
|
222
|
+
pid: slot.pid ?? null,
|
|
223
|
+
outputFile: slot.outputFile ?? null,
|
|
224
|
+
exitCode: slot.exitCode ?? null,
|
|
225
|
+
sessionId: slot.sessionId ?? null,
|
|
226
|
+
worktree: slot.worktree ?? null,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch { /* state read error */ }
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
log(msg) {
|
|
235
|
+
process.stderr.write(`[codex-exec] ${msg}\n`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
//# sourceMappingURL=CodexExecProvider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CodexExecProvider.js","sourceRoot":"","sources":["../../src/providers/CodexExecProvider.ts"],"names":[],"mappings":"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"}
|