tlc-claude-code 2.4.2 → 2.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/.claude/commands/tlc/build.md +75 -5
  2. package/.claude/commands/tlc/discuss.md +174 -123
  3. package/.claude/commands/tlc/e2e-verify.md +1 -1
  4. package/.claude/commands/tlc/plan.md +77 -2
  5. package/.claude/commands/tlc/recall.md +59 -87
  6. package/.claude/commands/tlc/remember.md +76 -71
  7. package/.claude/commands/tlc/review.md +76 -21
  8. package/.claude/commands/tlc/tlc.md +204 -473
  9. package/.claude/hooks/tlc-capture-exchange.sh +50 -21
  10. package/.claude/hooks/tlc-session-init.sh +30 -0
  11. package/CLAUDE.md +6 -5
  12. package/bin/init.js +12 -3
  13. package/package.json +4 -1
  14. package/scripts/dev-link.sh +29 -0
  15. package/scripts/test-package.sh +54 -0
  16. package/scripts/version-sync.js +42 -0
  17. package/scripts/version-sync.test.js +100 -0
  18. package/server/lib/capture/classifier.js +71 -0
  19. package/server/lib/capture/classifier.test.js +71 -0
  20. package/server/lib/capture/claude-capture.js +140 -0
  21. package/server/lib/capture/claude-capture.test.js +152 -0
  22. package/server/lib/capture/codex-capture.js +79 -0
  23. package/server/lib/capture/codex-capture.test.js +161 -0
  24. package/server/lib/capture/codex-event-parser.js +76 -0
  25. package/server/lib/capture/codex-event-parser.test.js +83 -0
  26. package/server/lib/capture/ensure-ready.js +56 -0
  27. package/server/lib/capture/ensure-ready.test.js +135 -0
  28. package/server/lib/capture/envelope.js +77 -0
  29. package/server/lib/capture/envelope.test.js +169 -0
  30. package/server/lib/capture/extractor.js +51 -0
  31. package/server/lib/capture/extractor.test.js +92 -0
  32. package/server/lib/capture/generic-capture.js +96 -0
  33. package/server/lib/capture/generic-capture.test.js +171 -0
  34. package/server/lib/capture/index.js +117 -0
  35. package/server/lib/capture/index.test.js +263 -0
  36. package/server/lib/capture/redactor.js +68 -0
  37. package/server/lib/capture/redactor.test.js +93 -0
  38. package/server/lib/capture/spool-processor.js +155 -0
  39. package/server/lib/capture/spool-processor.test.js +278 -0
  40. package/server/lib/health-check.js +255 -0
  41. package/server/lib/health-check.test.js +243 -0
  42. package/server/lib/model-router.js +11 -2
  43. package/server/lib/model-router.test.js +27 -1
  44. package/server/lib/orchestration/cli-dispatch.js +200 -0
  45. package/server/lib/orchestration/cli-dispatch.test.js +242 -0
  46. package/server/lib/orchestration/codex-orchestrator.js +185 -0
  47. package/server/lib/orchestration/codex-orchestrator.test.js +221 -0
  48. package/server/lib/orchestration/dep-linker.js +61 -0
  49. package/server/lib/orchestration/dep-linker.test.js +174 -0
  50. package/server/lib/orchestration/prompt-builder.js +118 -0
  51. package/server/lib/orchestration/prompt-builder.test.js +200 -0
  52. package/server/lib/orchestration/standalone-compat.js +39 -0
  53. package/server/lib/orchestration/standalone-compat.test.js +144 -0
  54. package/server/lib/orchestration/worktree-manager.js +43 -0
  55. package/server/lib/orchestration/worktree-manager.test.js +50 -0
  56. package/server/lib/router-config.js +18 -3
  57. package/server/lib/router-config.test.js +57 -1
  58. package/server/lib/routing/index.js +34 -0
  59. package/server/lib/routing/index.test.js +33 -0
  60. package/server/lib/routing-command.js +11 -2
  61. package/server/lib/routing-command.test.js +39 -1
  62. package/server/lib/routing-preamble.integration.test.js +319 -0
  63. package/server/lib/routing-preamble.js +34 -11
  64. package/server/lib/routing-preamble.test.js +11 -0
  65. package/server/lib/task-router-config.js +35 -14
  66. package/server/lib/task-router-config.test.js +77 -13
@@ -0,0 +1,200 @@
1
+ let parseCodexEvents = null;
2
+
3
+ try {
4
+ ({ parseCodexEvents } = require('../capture/codex-event-parser.js'));
5
+ } catch (_) {
6
+ parseCodexEvents = null;
7
+ }
8
+
9
+ function fallbackParseCodexEvents(jsonStream) {
10
+ const lines = typeof jsonStream === 'string' ? jsonStream.split('\n') : [];
11
+ let threadId = null;
12
+
13
+ for (const line of lines) {
14
+ if (typeof line !== 'string' || line.trim() === '') {
15
+ continue;
16
+ }
17
+
18
+ try {
19
+ const event = JSON.parse(line);
20
+ if (
21
+ event &&
22
+ event.type === 'thread.started' &&
23
+ typeof event.thread_id === 'string' &&
24
+ event.thread_id.trim() !== ''
25
+ ) {
26
+ threadId = event.thread_id;
27
+ }
28
+ } catch (_) {
29
+ // Ignore non-JSON lines in mixed stdout streams.
30
+ }
31
+ }
32
+
33
+ return { threadId };
34
+ }
35
+
36
+ function extractCodexThreadId(stdout) {
37
+ const parsed = parseCodexEvents
38
+ ? parseCodexEvents(stdout)
39
+ : fallbackParseCodexEvents(stdout);
40
+
41
+ return parsed && typeof parsed.threadId === 'string' ? parsed.threadId : null;
42
+ }
43
+
44
+ function buildCommand(provider, prompt, worktreePath, flags) {
45
+ const normalizedProvider = String(provider || '').trim();
46
+ const baseFlags = Array.isArray(flags) ? [...flags] : [];
47
+ const spawnOptions = {};
48
+ let usesStdin = true;
49
+ let args;
50
+
51
+ if (normalizedProvider === 'codex') {
52
+ args = ['exec', '--json', '--full-auto'];
53
+ if (worktreePath) {
54
+ args.push('-C', worktreePath);
55
+ }
56
+ } else if (normalizedProvider === 'gemini') {
57
+ usesStdin = false;
58
+ args = [...baseFlags, '-p', prompt];
59
+ if (worktreePath) {
60
+ spawnOptions.cwd = worktreePath;
61
+ }
62
+ return { command: normalizedProvider, args, spawnOptions, usesStdin };
63
+ } else if (normalizedProvider === 'claude') {
64
+ usesStdin = false;
65
+ args = ['--agent', 'builder'];
66
+ if (worktreePath) {
67
+ args.push('--worktree', worktreePath);
68
+ spawnOptions.cwd = worktreePath;
69
+ }
70
+ args.push(...baseFlags, '-p', prompt);
71
+ return { command: normalizedProvider, args, spawnOptions, usesStdin };
72
+ } else if (normalizedProvider === 'ollama') {
73
+ args = ['run', ...baseFlags];
74
+ if (worktreePath) {
75
+ spawnOptions.cwd = worktreePath;
76
+ }
77
+ return { command: normalizedProvider, args, spawnOptions, usesStdin };
78
+ } else {
79
+ args = baseFlags;
80
+ if (worktreePath) {
81
+ spawnOptions.cwd = worktreePath;
82
+ }
83
+ return { command: normalizedProvider, args, spawnOptions, usesStdin };
84
+ }
85
+
86
+ args.push(...baseFlags);
87
+ return { command: normalizedProvider, args, spawnOptions, usesStdin };
88
+ }
89
+
90
+ function buildSpawnError(provider, err) {
91
+ const message = err && err.message ? err.message : `Failed to spawn ${provider}`;
92
+
93
+ if (err && err.code === 'ENOENT') {
94
+ return {
95
+ code: 'CLI_NOT_FOUND',
96
+ message: `CLI "${provider}" was not found: ${message}`,
97
+ };
98
+ }
99
+
100
+ return {
101
+ code: 'SPAWN_ERROR',
102
+ message,
103
+ };
104
+ }
105
+
106
+ async function dispatch({
107
+ provider,
108
+ prompt = '',
109
+ worktreePath,
110
+ flags = [],
111
+ timeout = 120000,
112
+ spawn = require('child_process').spawn,
113
+ binaryOverride,
114
+ }) {
115
+ const start = Date.now();
116
+ const built = buildCommand(provider, prompt, worktreePath, flags);
117
+ // Allow standalone to preserve custom/absolute binary paths
118
+ const command = binaryOverride || built.command;
119
+ const { args, spawnOptions, usesStdin } = built;
120
+
121
+ return new Promise((resolve) => {
122
+ let proc;
123
+ let stdout = '';
124
+ let stderr = '';
125
+ let settled = false;
126
+ let timer = null;
127
+
128
+ function finish(exitCode, error) {
129
+ if (settled) {
130
+ return;
131
+ }
132
+
133
+ settled = true;
134
+ if (timer) {
135
+ clearTimeout(timer);
136
+ }
137
+
138
+ resolve({
139
+ stdout,
140
+ stderr,
141
+ exitCode,
142
+ duration: Date.now() - start,
143
+ threadId: command === 'codex' ? extractCodexThreadId(stdout) : null,
144
+ error: error || null,
145
+ });
146
+ }
147
+
148
+ try {
149
+ proc = spawn(command, args, spawnOptions);
150
+ } catch (err) {
151
+ finish(-1, buildSpawnError(command, err));
152
+ return;
153
+ }
154
+
155
+ proc.stdout.on('data', (chunk) => {
156
+ stdout += chunk.toString();
157
+ });
158
+
159
+ proc.stderr.on('data', (chunk) => {
160
+ stderr += chunk.toString();
161
+ });
162
+
163
+ proc.on('error', (err) => {
164
+ finish(-1, buildSpawnError(command, err));
165
+ });
166
+
167
+ proc.on('close', (code) => {
168
+ finish(typeof code === 'number' ? code : -1, null);
169
+ });
170
+
171
+ timer = setTimeout(() => {
172
+ if (typeof proc.kill === 'function') {
173
+ proc.kill();
174
+ }
175
+
176
+ finish(-1, {
177
+ code: 'TIMEOUT',
178
+ message: `CLI "${command}" timed out after ${timeout}ms`,
179
+ });
180
+ }, timeout);
181
+
182
+ if (proc.stdin && typeof proc.stdin.on === 'function') {
183
+ proc.stdin.on('error', () => {});
184
+ }
185
+
186
+ if (proc.stdin) {
187
+ if (usesStdin && typeof proc.stdin.write === 'function' && prompt) {
188
+ proc.stdin.write(prompt);
189
+ }
190
+
191
+ if (typeof proc.stdin.end === 'function') {
192
+ proc.stdin.end();
193
+ }
194
+ }
195
+ });
196
+ }
197
+
198
+ module.exports = {
199
+ dispatch,
200
+ };
@@ -0,0 +1,242 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { EventEmitter } from 'events';
3
+
4
+ const { dispatch } = require('./cli-dispatch.js');
5
+
6
+ function createMockProcess({ stdout = '', stderr = '', exitCode = 0, delay = 0 } = {}) {
7
+ const proc = new EventEmitter();
8
+ proc.stdout = new EventEmitter();
9
+ proc.stderr = new EventEmitter();
10
+ proc.stdin = {
11
+ write: vi.fn(),
12
+ end: vi.fn(),
13
+ on: vi.fn(),
14
+ };
15
+ proc.kill = vi.fn();
16
+
17
+ setTimeout(() => {
18
+ if (stdout) {
19
+ proc.stdout.emit('data', Buffer.from(stdout));
20
+ }
21
+
22
+ if (stderr) {
23
+ proc.stderr.emit('data', Buffer.from(stderr));
24
+ }
25
+
26
+ proc.emit('close', exitCode);
27
+ }, delay);
28
+
29
+ return proc;
30
+ }
31
+
32
+ describe('cli-dispatch', () => {
33
+ it('builds codex args with -C, writes prompt to stdin, and extracts threadId', async () => {
34
+ const stdout = [
35
+ '{"type":"thread.started","thread_id":"thread-123"}',
36
+ '{"type":"agent_message","message":{"items":[{"type":"text","text":"done"}]}}',
37
+ ].join('\n');
38
+ const proc = createMockProcess({ stdout });
39
+ const spawn = vi.fn(() => proc);
40
+
41
+ const result = await dispatch({
42
+ provider: 'codex',
43
+ prompt: 'Review this change',
44
+ worktreePath: '/tmp/worktree',
45
+ flags: ['--model', 'gpt-5.4'],
46
+ spawn,
47
+ });
48
+
49
+ expect(spawn).toHaveBeenCalledWith(
50
+ 'codex',
51
+ ['exec', '--json', '--full-auto', '-C', '/tmp/worktree', '--model', 'gpt-5.4'],
52
+ {}
53
+ );
54
+ expect(proc.stdin.write).toHaveBeenCalledWith('Review this change');
55
+ expect(proc.stdin.end).toHaveBeenCalled();
56
+ expect(result.threadId).toBe('thread-123');
57
+ expect(result.exitCode).toBe(0);
58
+ expect(result.error).toBeNull();
59
+ });
60
+
61
+ it('passes gemini prompt as -p argument and sets cwd from worktreePath', async () => {
62
+ const proc = createMockProcess({ stdout: 'gemini output' });
63
+ const spawn = vi.fn(() => proc);
64
+
65
+ const result = await dispatch({
66
+ provider: 'gemini',
67
+ prompt: 'Explain this diff',
68
+ worktreePath: '/tmp/gemini-worktree',
69
+ flags: ['--model', 'gemini-2.5-pro'],
70
+ spawn,
71
+ });
72
+
73
+ expect(spawn).toHaveBeenCalledWith(
74
+ 'gemini',
75
+ ['--model', 'gemini-2.5-pro', '-p', 'Explain this diff'],
76
+ { cwd: '/tmp/gemini-worktree' }
77
+ );
78
+ expect(proc.stdin.write).not.toHaveBeenCalled();
79
+ expect(proc.stdin.end).toHaveBeenCalled();
80
+ expect(result.stdout).toBe('gemini output');
81
+ });
82
+
83
+ it('builds claude args with worktree and prompt flag and sets cwd', async () => {
84
+ const proc = createMockProcess({ stdout: 'claude output' });
85
+ const spawn = vi.fn(() => proc);
86
+
87
+ await dispatch({
88
+ provider: 'claude',
89
+ prompt: 'Implement the feature',
90
+ worktreePath: '/tmp/claude-worktree',
91
+ flags: ['--permission-mode', 'auto'],
92
+ spawn,
93
+ });
94
+
95
+ expect(spawn).toHaveBeenCalledWith(
96
+ 'claude',
97
+ ['--agent', 'builder', '--worktree', '/tmp/claude-worktree', '--permission-mode', 'auto', '-p', 'Implement the feature'],
98
+ { cwd: '/tmp/claude-worktree' }
99
+ );
100
+ });
101
+
102
+ it('uses stdin prompt for ollama and sets cwd when worktreePath is provided', async () => {
103
+ const proc = createMockProcess({ stdout: 'ollama output' });
104
+ const spawn = vi.fn(() => proc);
105
+
106
+ await dispatch({
107
+ provider: 'ollama',
108
+ prompt: 'Summarize the logs',
109
+ worktreePath: '/tmp/ollama-worktree',
110
+ flags: ['llama3.2'],
111
+ spawn,
112
+ });
113
+
114
+ expect(spawn).toHaveBeenCalledWith(
115
+ 'ollama',
116
+ ['run', 'llama3.2'],
117
+ { cwd: '/tmp/ollama-worktree' }
118
+ );
119
+ expect(proc.stdin.write).toHaveBeenCalledWith('Summarize the logs');
120
+ });
121
+
122
+ it('uses generic flags and stdin for unknown providers', async () => {
123
+ const proc = createMockProcess({ stdout: 'custom output' });
124
+ const spawn = vi.fn(() => proc);
125
+
126
+ await dispatch({
127
+ provider: 'custom-cli',
128
+ prompt: 'Do the custom thing',
129
+ worktreePath: '/tmp/custom-worktree',
130
+ flags: ['--mode', 'fast'],
131
+ spawn,
132
+ });
133
+
134
+ expect(spawn).toHaveBeenCalledWith(
135
+ 'custom-cli',
136
+ ['--mode', 'fast'],
137
+ { cwd: '/tmp/custom-worktree' }
138
+ );
139
+ expect(proc.stdin.write).toHaveBeenCalledWith('Do the custom thing');
140
+ });
141
+
142
+ it('returns CLI_NOT_FOUND when spawn emits ENOENT', async () => {
143
+ const proc = new EventEmitter();
144
+ proc.stdout = new EventEmitter();
145
+ proc.stderr = new EventEmitter();
146
+ proc.stdin = {
147
+ write: vi.fn(),
148
+ end: vi.fn(),
149
+ on: vi.fn(),
150
+ };
151
+ proc.kill = vi.fn();
152
+
153
+ const spawn = vi.fn(() => {
154
+ setTimeout(() => {
155
+ proc.emit('error', Object.assign(new Error('spawn codex ENOENT'), { code: 'ENOENT' }));
156
+ }, 0);
157
+ return proc;
158
+ });
159
+
160
+ const result = await dispatch({
161
+ provider: 'codex',
162
+ prompt: 'test',
163
+ spawn,
164
+ });
165
+
166
+ expect(result.exitCode).toBe(-1);
167
+ expect(result.error).toEqual(
168
+ expect.objectContaining({
169
+ code: 'CLI_NOT_FOUND',
170
+ })
171
+ );
172
+ expect(result.error.message).toContain('codex');
173
+ });
174
+
175
+ it('returns TIMEOUT on hung processes', async () => {
176
+ const proc = createMockProcess({ stdout: 'late output', delay: 5000 });
177
+ const spawn = vi.fn(() => proc);
178
+
179
+ const result = await dispatch({
180
+ provider: 'ollama',
181
+ prompt: 'slow task',
182
+ timeout: 20,
183
+ spawn,
184
+ });
185
+
186
+ expect(result.exitCode).toBe(-1);
187
+ expect(result.error).toEqual(
188
+ expect.objectContaining({
189
+ code: 'TIMEOUT',
190
+ })
191
+ );
192
+ expect(proc.kill).toHaveBeenCalled();
193
+ });
194
+
195
+ it('captures non-zero exits without throwing', async () => {
196
+ const proc = createMockProcess({ stdout: 'partial', stderr: 'failed', exitCode: 2 });
197
+ const spawn = vi.fn(() => proc);
198
+
199
+ const result = await dispatch({
200
+ provider: 'custom-cli',
201
+ prompt: 'run',
202
+ spawn,
203
+ });
204
+
205
+ expect(result.stdout).toBe('partial');
206
+ expect(result.stderr).toBe('failed');
207
+ expect(result.exitCode).toBe(2);
208
+ expect(result.error).toBeNull();
209
+ });
210
+
211
+ it('does not set cwd when codex already uses -C', async () => {
212
+ const proc = createMockProcess({ stdout: 'ok' });
213
+ const spawn = vi.fn(() => proc);
214
+
215
+ await dispatch({
216
+ provider: 'codex',
217
+ prompt: 'test',
218
+ worktreePath: '/tmp/codex-worktree',
219
+ spawn,
220
+ });
221
+
222
+ expect(spawn).toHaveBeenCalledWith(
223
+ 'codex',
224
+ ['exec', '--json', '--full-auto', '-C', '/tmp/codex-worktree'],
225
+ {}
226
+ );
227
+ });
228
+
229
+ it('returns null threadId when codex output does not contain one', async () => {
230
+ const proc = createMockProcess({ stdout: '{"type":"agent_message"}' });
231
+ const spawn = vi.fn(() => proc);
232
+
233
+ const result = await dispatch({
234
+ provider: 'codex',
235
+ prompt: 'test',
236
+ spawn,
237
+ });
238
+
239
+ expect(result.threadId).toBeNull();
240
+ expect(result.exitCode).toBe(0);
241
+ });
242
+ });
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Codex Orchestrator
3
+ *
4
+ * Dispatches tasks to Codex CLI and captures thread IDs for resume flows.
5
+ */
6
+
7
+ function createCliNotFoundError() {
8
+ return {
9
+ code: 'CODEX_CLI_NOT_FOUND',
10
+ message: 'Codex CLI is not installed or not available on PATH',
11
+ };
12
+ }
13
+
14
+ function createTimeoutError(timeout) {
15
+ return {
16
+ code: 'PROCESS_TIMEOUT',
17
+ message: `Process timed out after ${timeout}ms`,
18
+ };
19
+ }
20
+
21
+ function runCodex({
22
+ args,
23
+ prompt = '',
24
+ timeout = 120000,
25
+ cwd,
26
+ spawn = require('child_process').spawn,
27
+ onStdoutLine,
28
+ }) {
29
+ return new Promise((resolve) => {
30
+ const start = Date.now();
31
+ const spawnOpts = {};
32
+ if (cwd) spawnOpts.cwd = cwd;
33
+
34
+ let proc;
35
+ try {
36
+ proc = spawn('codex', args, spawnOpts);
37
+ } catch (err) {
38
+ const error = err && err.code === 'ENOENT'
39
+ ? createCliNotFoundError()
40
+ : {
41
+ code: 'SPAWN_FAILED',
42
+ message: err && err.message ? err.message : 'Failed to spawn Codex CLI',
43
+ };
44
+
45
+ resolve({
46
+ stdout: '',
47
+ stderr: err && err.message ? err.message : error.message,
48
+ exitCode: -1,
49
+ duration: Date.now() - start,
50
+ error,
51
+ });
52
+ return;
53
+ }
54
+
55
+ let stdout = '';
56
+ let stderr = '';
57
+ let settled = false;
58
+ let stdoutBuffer = '';
59
+
60
+ const finish = (exitCode, error = null, stderrOverride) => {
61
+ if (settled) return;
62
+ settled = true;
63
+ clearTimeout(timer);
64
+ resolve({
65
+ stdout,
66
+ stderr: stderrOverride !== undefined ? stderrOverride : stderr,
67
+ exitCode,
68
+ duration: Date.now() - start,
69
+ error,
70
+ });
71
+ };
72
+
73
+ const timer = setTimeout(() => {
74
+ proc.kill();
75
+ const error = createTimeoutError(timeout);
76
+ const nextStderr = stderr ? `${stderr}\n${error.message}` : error.message;
77
+ finish(-1, error, nextStderr);
78
+ }, timeout);
79
+
80
+ proc.stdout.on('data', (data) => {
81
+ const chunk = data.toString();
82
+ stdout += chunk;
83
+
84
+ if (!onStdoutLine) {
85
+ return;
86
+ }
87
+
88
+ stdoutBuffer += chunk;
89
+
90
+ while (stdoutBuffer.includes('\n')) {
91
+ const newlineIndex = stdoutBuffer.indexOf('\n');
92
+ const line = stdoutBuffer.slice(0, newlineIndex);
93
+ stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1);
94
+ onStdoutLine(line);
95
+ }
96
+ });
97
+
98
+ proc.stderr.on('data', (data) => {
99
+ stderr += data.toString();
100
+ });
101
+
102
+ proc.on('close', (code) => {
103
+ if (onStdoutLine && stdoutBuffer) {
104
+ onStdoutLine(stdoutBuffer);
105
+ stdoutBuffer = '';
106
+ }
107
+
108
+ finish(code, null);
109
+ });
110
+
111
+ proc.on('error', (err) => {
112
+ const error = err && err.code === 'ENOENT'
113
+ ? createCliNotFoundError()
114
+ : {
115
+ code: 'SPAWN_FAILED',
116
+ message: err && err.message ? err.message : 'Failed to spawn Codex CLI',
117
+ };
118
+
119
+ const nextStderr = stderr || (err && err.message) || error.message;
120
+ finish(-1, error, nextStderr);
121
+ });
122
+
123
+ proc.stdin.on('error', () => {});
124
+
125
+ if (prompt) {
126
+ proc.stdin.write(prompt);
127
+ }
128
+ proc.stdin.end();
129
+ });
130
+ }
131
+
132
+ async function dispatchToCodex({
133
+ worktreePath,
134
+ prompt,
135
+ timeout = 120000,
136
+ spawn = require('child_process').spawn,
137
+ }) {
138
+ let threadId = null;
139
+
140
+ const result = await runCodex({
141
+ args: ['exec', '--json', '--full-auto', '-C', worktreePath],
142
+ prompt,
143
+ timeout,
144
+ spawn,
145
+ onStdoutLine(line) {
146
+ if (!line) return;
147
+
148
+ try {
149
+ const event = JSON.parse(line);
150
+ if (event.type === 'thread.started' && typeof event.thread_id === 'string') {
151
+ threadId = event.thread_id;
152
+ }
153
+ } catch (_) {
154
+ // Ignore non-JSON lines in mixed stdout streams.
155
+ }
156
+ },
157
+ });
158
+
159
+ return {
160
+ threadId,
161
+ stdout: result.stdout,
162
+ stderr: result.stderr,
163
+ exitCode: result.exitCode,
164
+ duration: result.duration,
165
+ error: result.error,
166
+ };
167
+ }
168
+
169
+ function resumeSession({
170
+ threadId,
171
+ prompt,
172
+ timeout = 120000,
173
+ spawn = require('child_process').spawn,
174
+ }) {
175
+ return runCodex({
176
+ args: ['exec', 'resume', threadId, prompt],
177
+ timeout,
178
+ spawn,
179
+ });
180
+ }
181
+
182
+ module.exports = {
183
+ dispatchToCodex,
184
+ resumeSession,
185
+ };