triflux 3.3.0-dev.8 → 4.0.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.
Files changed (91) hide show
  1. package/README.ko.md +108 -199
  2. package/README.md +108 -199
  3. package/bin/triflux.mjs +2427 -1762
  4. package/hooks/keyword-rules.json +361 -354
  5. package/hooks/pipeline-stop.mjs +5 -2
  6. package/hub/assign-callbacks.mjs +136 -136
  7. package/hub/bridge.mjs +734 -684
  8. package/hub/delegator/contracts.mjs +38 -38
  9. package/hub/delegator/index.mjs +14 -14
  10. package/hub/delegator/schema/delegator-tools.schema.json +250 -250
  11. package/hub/delegator/service.mjs +302 -118
  12. package/hub/delegator/tool-definitions.mjs +35 -35
  13. package/hub/hitl.mjs +67 -67
  14. package/hub/paths.mjs +28 -0
  15. package/hub/pipe.mjs +589 -561
  16. package/hub/pipeline/state.mjs +23 -0
  17. package/hub/public/dashboard.html +349 -0
  18. package/hub/public/tray-icon.ico +0 -0
  19. package/hub/public/tray-icon.png +0 -0
  20. package/hub/router.mjs +782 -782
  21. package/hub/schema.sql +40 -40
  22. package/hub/server.mjs +810 -637
  23. package/hub/store.mjs +706 -706
  24. package/hub/team/cli/commands/attach.mjs +37 -0
  25. package/hub/team/cli/commands/control.mjs +43 -0
  26. package/hub/team/cli/commands/debug.mjs +74 -0
  27. package/hub/team/cli/commands/focus.mjs +53 -0
  28. package/hub/team/cli/commands/interrupt.mjs +36 -0
  29. package/hub/team/cli/commands/kill.mjs +37 -0
  30. package/hub/team/cli/commands/list.mjs +24 -0
  31. package/hub/team/cli/commands/send.mjs +37 -0
  32. package/hub/team/cli/commands/start/index.mjs +87 -0
  33. package/hub/team/cli/commands/start/parse-args.mjs +32 -0
  34. package/hub/team/cli/commands/start/start-in-process.mjs +40 -0
  35. package/hub/team/cli/commands/start/start-mux.mjs +73 -0
  36. package/hub/team/cli/commands/start/start-wt.mjs +69 -0
  37. package/hub/team/cli/commands/status.mjs +87 -0
  38. package/hub/team/cli/commands/stop.mjs +31 -0
  39. package/hub/team/cli/commands/task.mjs +30 -0
  40. package/hub/team/cli/commands/tasks.mjs +13 -0
  41. package/hub/team/{cli.mjs → cli/help.mjs} +38 -99
  42. package/hub/team/cli/index.mjs +39 -0
  43. package/hub/team/cli/manifest.mjs +28 -0
  44. package/hub/team/cli/render.mjs +30 -0
  45. package/hub/team/cli/services/attach-fallback.mjs +54 -0
  46. package/hub/team/cli/services/hub-client.mjs +171 -0
  47. package/hub/team/cli/services/member-selector.mjs +30 -0
  48. package/hub/team/cli/services/native-control.mjs +115 -0
  49. package/hub/team/cli/services/runtime-mode.mjs +60 -0
  50. package/hub/team/cli/services/state-store.mjs +34 -0
  51. package/hub/team/cli/services/task-model.mjs +30 -0
  52. package/hub/team/native-supervisor.mjs +69 -63
  53. package/hub/team/native.mjs +367 -367
  54. package/hub/team/nativeProxy.mjs +217 -173
  55. package/hub/team/pane.mjs +149 -149
  56. package/hub/team/psmux.mjs +946 -946
  57. package/hub/team/session.mjs +608 -608
  58. package/hub/team/staleState.mjs +369 -299
  59. package/hub/tools.mjs +107 -107
  60. package/hub/tray.mjs +332 -0
  61. package/hub/workers/claude-worker.mjs +446 -446
  62. package/hub/workers/codex-mcp.mjs +414 -414
  63. package/hub/workers/delegator-mcp.mjs +1045 -1045
  64. package/hub/workers/factory.mjs +21 -21
  65. package/hub/workers/gemini-worker.mjs +349 -349
  66. package/hub/workers/interface.mjs +41 -41
  67. package/package.json +3 -2
  68. package/scripts/__tests__/keyword-detector.test.mjs +234 -234
  69. package/scripts/hub-ensure.mjs +102 -101
  70. package/scripts/keyword-detector.mjs +272 -272
  71. package/scripts/keyword-rules-expander.mjs +521 -521
  72. package/scripts/lib/keyword-rules.mjs +168 -168
  73. package/scripts/lib/mcp-filter.mjs +642 -642
  74. package/scripts/lib/mcp-server-catalog.mjs +118 -118
  75. package/scripts/mcp-check.mjs +126 -126
  76. package/scripts/preflight-cache.mjs +19 -0
  77. package/scripts/run.cjs +62 -62
  78. package/scripts/setup.mjs +68 -31
  79. package/scripts/test-tfx-route-no-claude-native.mjs +57 -57
  80. package/scripts/tfx-route-worker.mjs +161 -161
  81. package/scripts/tfx-route.sh +1360 -1326
  82. package/skills/tfx-auto/SKILL.md +196 -196
  83. package/skills/tfx-auto-codex/SKILL.md +77 -77
  84. package/skills/tfx-multi/SKILL.md +378 -378
  85. package/hub/team/cli-team-common.mjs +0 -348
  86. package/hub/team/cli-team-control.mjs +0 -393
  87. package/hub/team/cli-team-start.mjs +0 -516
  88. package/hub/team/cli-team-status.mjs +0 -283
  89. package/skills/auto-verify/SKILL.md +0 -145
  90. package/skills/manage-skills/SKILL.md +0 -192
  91. package/skills/verify-implementation/SKILL.md +0 -138
@@ -1,446 +1,446 @@
1
- // hub/workers/claude-worker.mjs — Claude stream-json subprocess 래퍼
2
- // ADR-007: --input-format/--output-format stream-json 기반 세션 워커.
3
-
4
- import { spawn } from 'node:child_process';
5
- import readline from 'node:readline';
6
-
7
- const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
8
- const DEFAULT_KILL_GRACE_MS = 1000;
9
-
10
- function toStringList(value) {
11
- if (!Array.isArray(value)) return [];
12
- return value
13
- .map((item) => String(item ?? '').trim())
14
- .filter(Boolean);
15
- }
16
-
17
- function safeJsonParse(line) {
18
- try {
19
- return JSON.parse(line);
20
- } catch {
21
- return null;
22
- }
23
- }
24
-
25
- function appendTextFragments(value, parts) {
26
- if (value == null) return;
27
- if (typeof value === 'string') {
28
- const trimmed = value.trim();
29
- if (trimmed) parts.push(trimmed);
30
- return;
31
- }
32
- if (Array.isArray(value)) {
33
- for (const item of value) appendTextFragments(item, parts);
34
- return;
35
- }
36
- if (typeof value === 'object') {
37
- if (typeof value.text === 'string') appendTextFragments(value.text, parts);
38
- if (typeof value.result === 'string') appendTextFragments(value.result, parts);
39
- if (typeof value.content === 'string' || Array.isArray(value.content) || value.content) {
40
- appendTextFragments(value.content, parts);
41
- }
42
- if (typeof value.message === 'string' || Array.isArray(value.message) || value.message) {
43
- appendTextFragments(value.message, parts);
44
- }
45
- }
46
- }
47
-
48
- function extractText(event) {
49
- const parts = [];
50
- appendTextFragments(event, parts);
51
- return parts.join('\n').trim();
52
- }
53
-
54
- function findSessionId(event) {
55
- return event?.session_id
56
- || event?.sessionId
57
- || event?.message?.session_id
58
- || event?.message?.sessionId
59
- || null;
60
- }
61
-
62
- function createWorkerError(message, details = {}) {
63
- const error = new Error(message);
64
- Object.assign(error, details);
65
- return error;
66
- }
67
-
68
- function buildClaudeArgs(worker, options) {
69
- const args = [...worker.commandArgs];
70
-
71
- args.push('--print');
72
- args.push('--input-format', 'stream-json');
73
- args.push('--output-format', 'stream-json');
74
-
75
- if (options.includePartialMessages) args.push('--include-partial-messages');
76
- if (options.replayUserMessages) args.push('--replay-user-messages');
77
- if (options.model) args.push('--model', options.model);
78
- if (options.allowDangerouslySkipPermissions) args.push('--dangerously-skip-permissions');
79
- if (options.permissionMode) args.push('--permission-mode', options.permissionMode);
80
-
81
- for (const config of toStringList(options.mcpConfig)) {
82
- args.push('--mcp-config', config);
83
- }
84
-
85
- if (worker.resumeSessionId) {
86
- args.push('--resume', worker.resumeSessionId);
87
- }
88
-
89
- args.push(...toStringList(options.extraArgs));
90
-
91
- return args;
92
- }
93
-
94
- /**
95
- * Claude stream-json 세션 워커
96
- */
97
- export class ClaudeWorker {
98
- type = 'claude';
99
-
100
- constructor(options = {}) {
101
- this.command = options.command || 'claude';
102
- this.commandArgs = toStringList(options.commandArgs || options.args);
103
- this.cwd = options.cwd || process.cwd();
104
- this.env = { ...process.env, ...(options.env || {}) };
105
- this.model = options.model || null;
106
- this.permissionMode = options.permissionMode || null;
107
- this.allowDangerouslySkipPermissions = options.allowDangerouslySkipPermissions !== false;
108
- this.includePartialMessages = options.includePartialMessages === true;
109
- this.replayUserMessages = options.replayUserMessages !== false;
110
- this.mcpConfig = toStringList(options.mcpConfig);
111
- this.extraArgs = toStringList(options.extraArgs);
112
- this.timeoutMs = Number(options.timeoutMs) > 0 ? Number(options.timeoutMs) : DEFAULT_TIMEOUT_MS;
113
- this.killGraceMs = Number(options.killGraceMs) > 0 ? Number(options.killGraceMs) : DEFAULT_KILL_GRACE_MS;
114
- this.onEvent = typeof options.onEvent === 'function' ? options.onEvent : null;
115
- this.controlRequestHandler = typeof options.controlRequestHandler === 'function'
116
- ? options.controlRequestHandler
117
- : null;
118
-
119
- this.state = 'idle';
120
- this.child = null;
121
- this.stdoutReader = null;
122
- this.stderrReader = null;
123
- this.pendingTurn = null;
124
- this.history = [];
125
- this.events = [];
126
- this.stderrLines = [];
127
- this.sessionId = null;
128
- this.resumeSessionId = null;
129
- this.lastTurn = null;
130
- this._closePromise = null;
131
- }
132
-
133
- getStatus() {
134
- return {
135
- type: 'claude',
136
- state: this.state,
137
- pid: this.child?.pid || null,
138
- session_id: this.sessionId,
139
- history_length: this.history.length,
140
- last_turn_at_ms: this.lastTurn?.finishedAtMs || null,
141
- };
142
- }
143
-
144
- _writeFrame(frame) {
145
- if (!this.child?.stdin?.writable) {
146
- throw createWorkerError('Claude worker stdin is not writable', { code: 'WORKER_STDIN_CLOSED' });
147
- }
148
- this.child.stdin.write(`${JSON.stringify(frame)}\n`);
149
- }
150
-
151
- _terminateChild(child) {
152
- if (!child || child.exitCode !== null || child.killed) return;
153
- try { child.stdin.end(); } catch {}
154
- try { child.kill(); } catch {}
155
- const timer = setTimeout(() => {
156
- if (child.exitCode === null) {
157
- try { child.kill('SIGKILL'); } catch {}
158
- }
159
- }, this.killGraceMs);
160
- timer.unref?.();
161
- }
162
-
163
- async _handleControlRequest(event) {
164
- let responseFrame = null;
165
-
166
- if (this.controlRequestHandler) {
167
- responseFrame = await this.controlRequestHandler(event, { worker: this });
168
- } else {
169
- const requestId = event.request_id || event.requestId;
170
- if (!requestId) return;
171
-
172
- const successPayload = event.subtype === 'can_use_tool'
173
- ? { decision: 'allow', allowed: true }
174
- : { acknowledged: true, subtype: event.subtype || 'unknown' };
175
-
176
- responseFrame = {
177
- type: 'control_response',
178
- response: {
179
- request_id: requestId,
180
- subtype: 'success',
181
- response: successPayload,
182
- },
183
- };
184
- }
185
-
186
- if (responseFrame) {
187
- this._writeFrame(responseFrame);
188
- }
189
- }
190
-
191
- _finalizePendingTurn(turn, event) {
192
- if (!turn || turn.completed) return;
193
-
194
- turn.completed = true;
195
- clearTimeout(turn.timeout);
196
- this.pendingTurn = null;
197
-
198
- const response = [
199
- ...turn.assistantTexts,
200
- extractText(event),
201
- ]
202
- .filter(Boolean)
203
- .join('\n')
204
- .trim();
205
-
206
- if (response) {
207
- this.history.push({
208
- role: 'assistant',
209
- content: response,
210
- at_ms: Date.now(),
211
- });
212
- }
213
-
214
- const result = {
215
- type: 'claude',
216
- sessionId: this.sessionId,
217
- response,
218
- assistantEvents: turn.assistantEvents,
219
- resultEvent: event,
220
- stderr: this.stderrLines.join('\n').trim(),
221
- history: [...this.history],
222
- startedAtMs: turn.startedAtMs,
223
- finishedAtMs: Date.now(),
224
- durationMs: Date.now() - turn.startedAtMs,
225
- };
226
-
227
- this.lastTurn = result;
228
- turn.resolve(result);
229
- }
230
-
231
- _rejectPendingTurn(error) {
232
- if (!this.pendingTurn || this.pendingTurn.completed) return;
233
- const turn = this.pendingTurn;
234
- turn.completed = true;
235
- clearTimeout(turn.timeout);
236
- this.pendingTurn = null;
237
- turn.reject(error);
238
- }
239
-
240
- _handleStdoutLine(line) {
241
- if (!line) return;
242
- const event = safeJsonParse(line);
243
- if (!event) return;
244
-
245
- this.events.push(event);
246
- const sessionId = findSessionId(event);
247
- if (sessionId) {
248
- this.sessionId = sessionId;
249
- this.resumeSessionId = sessionId;
250
- }
251
-
252
- if (this.onEvent) {
253
- try { this.onEvent(event); } catch {}
254
- }
255
-
256
- if (event.type === 'control_request') {
257
- void this._handleControlRequest(event);
258
- return;
259
- }
260
-
261
- if (event.type === 'assistant' || event.type === 'streamlined_text') {
262
- const text = extractText(event);
263
- if (this.pendingTurn && text) {
264
- this.pendingTurn.assistantTexts.push(text);
265
- this.pendingTurn.assistantEvents.push(event);
266
- }
267
- return;
268
- }
269
-
270
- if (event.type === 'result' && this.pendingTurn) {
271
- this._finalizePendingTurn(this.pendingTurn, event);
272
- }
273
- }
274
-
275
- async start() {
276
- if (this.child && this.child.exitCode === null) {
277
- return this.getStatus();
278
- }
279
-
280
- const args = buildClaudeArgs(this, {
281
- model: this.model,
282
- permissionMode: this.permissionMode,
283
- allowDangerouslySkipPermissions: this.allowDangerouslySkipPermissions,
284
- includePartialMessages: this.includePartialMessages,
285
- replayUserMessages: this.replayUserMessages,
286
- mcpConfig: this.mcpConfig,
287
- extraArgs: this.extraArgs,
288
- });
289
-
290
- const child = spawn(this.command, args, {
291
- cwd: this.cwd,
292
- env: this.env,
293
- stdio: ['pipe', 'pipe', 'pipe'],
294
- windowsHide: true,
295
- });
296
-
297
- this.child = child;
298
- this.state = 'ready';
299
- this.stderrLines = [];
300
- this.stdoutReader = readline.createInterface({
301
- input: child.stdout,
302
- crlfDelay: Infinity,
303
- });
304
- this.stderrReader = readline.createInterface({
305
- input: child.stderr,
306
- crlfDelay: Infinity,
307
- });
308
-
309
- this.stdoutReader.on('line', (line) => this._handleStdoutLine(line));
310
- this.stderrReader.on('line', (line) => {
311
- if (!line) return;
312
- this.stderrLines.push(line);
313
- });
314
-
315
- this._closePromise = new Promise((resolve, reject) => {
316
- child.once('error', reject);
317
- child.once('close', (code, signal) => {
318
- const closeError = code === 0
319
- ? null
320
- : createWorkerError(`Claude worker exited with code ${code}`, {
321
- code: 'WORKER_EXIT',
322
- exitCode: code,
323
- exitSignal: signal,
324
- stderr: this.stderrLines.join('\n').trim(),
325
- });
326
-
327
- this.child = null;
328
- this.state = 'idle';
329
- try { this.stdoutReader?.close(); } catch {}
330
- try { this.stderrReader?.close(); } catch {}
331
- this.stdoutReader = null;
332
- this.stderrReader = null;
333
- if (closeError) this._rejectPendingTurn(closeError);
334
- resolve({ code, signal });
335
- });
336
- });
337
-
338
- return this.getStatus();
339
- }
340
-
341
- async stop() {
342
- if (!this.child) {
343
- this.state = 'stopped';
344
- return this.getStatus();
345
- }
346
-
347
- const child = this.child;
348
- this._terminateChild(child);
349
- await Promise.race([
350
- this._closePromise,
351
- new Promise((resolve) => setTimeout(resolve, this.killGraceMs + 50)),
352
- ]);
353
-
354
- this.child = null;
355
- this.state = 'stopped';
356
- return this.getStatus();
357
- }
358
-
359
- async restart() {
360
- await this.stop();
361
- this.state = 'idle';
362
- return this.start();
363
- }
364
-
365
- async run(prompt, options = {}) {
366
- await this.start();
367
-
368
- if (this.pendingTurn) {
369
- throw createWorkerError('ClaudeWorker is already handling another turn', { code: 'WORKER_BUSY' });
370
- }
371
-
372
- const timeoutMs = Number(options.timeoutMs) > 0 ? Number(options.timeoutMs) : this.timeoutMs;
373
- const userText = String(prompt ?? '');
374
- const startedAtMs = Date.now();
375
-
376
- this.history.push({
377
- role: 'user',
378
- content: userText,
379
- at_ms: startedAtMs,
380
- });
381
-
382
- const turnPromise = new Promise((resolve, reject) => {
383
- const timeout = setTimeout(() => {
384
- const timeoutError = createWorkerError(`Claude worker timed out after ${timeoutMs}ms`, {
385
- code: 'ETIMEDOUT',
386
- stderr: this.stderrLines.join('\n').trim(),
387
- });
388
- this._rejectPendingTurn(timeoutError);
389
- this._terminateChild(this.child);
390
- }, timeoutMs);
391
- timeout.unref?.();
392
-
393
- this.pendingTurn = {
394
- startedAtMs,
395
- assistantTexts: [],
396
- assistantEvents: [],
397
- timeout,
398
- resolve,
399
- reject,
400
- completed: false,
401
- };
402
- });
403
-
404
- this.state = 'running';
405
- this._writeFrame({
406
- type: 'user',
407
- message: {
408
- role: 'user',
409
- content: userText,
410
- },
411
- });
412
-
413
- try {
414
- return await turnPromise;
415
- } finally {
416
- if (this.child) {
417
- this.state = 'ready';
418
- }
419
- }
420
- }
421
-
422
- isReady() {
423
- return this.state === 'ready' || this.state === 'running';
424
- }
425
-
426
- async execute(prompt, options = {}) {
427
- try {
428
- const result = await this.run(prompt, options);
429
- return {
430
- output: result.response,
431
- exitCode: 0,
432
- threadId: null,
433
- sessionKey: options.sessionKey || this.sessionId || null,
434
- raw: result,
435
- };
436
- } catch (error) {
437
- return {
438
- output: error.stderr || error.message || 'Claude worker failed',
439
- exitCode: error.code === 'ETIMEDOUT' ? 124 : 1,
440
- threadId: null,
441
- sessionKey: options.sessionKey || this.sessionId || null,
442
- raw: null,
443
- };
444
- }
445
- }
446
- }
1
+ // hub/workers/claude-worker.mjs — Claude stream-json subprocess 래퍼
2
+ // ADR-007: --input-format/--output-format stream-json 기반 세션 워커.
3
+
4
+ import { spawn } from 'node:child_process';
5
+ import readline from 'node:readline';
6
+
7
+ const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
8
+ const DEFAULT_KILL_GRACE_MS = 1000;
9
+
10
+ function toStringList(value) {
11
+ if (!Array.isArray(value)) return [];
12
+ return value
13
+ .map((item) => String(item ?? '').trim())
14
+ .filter(Boolean);
15
+ }
16
+
17
+ function safeJsonParse(line) {
18
+ try {
19
+ return JSON.parse(line);
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ function appendTextFragments(value, parts) {
26
+ if (value == null) return;
27
+ if (typeof value === 'string') {
28
+ const trimmed = value.trim();
29
+ if (trimmed) parts.push(trimmed);
30
+ return;
31
+ }
32
+ if (Array.isArray(value)) {
33
+ for (const item of value) appendTextFragments(item, parts);
34
+ return;
35
+ }
36
+ if (typeof value === 'object') {
37
+ if (typeof value.text === 'string') appendTextFragments(value.text, parts);
38
+ if (typeof value.result === 'string') appendTextFragments(value.result, parts);
39
+ if (typeof value.content === 'string' || Array.isArray(value.content) || value.content) {
40
+ appendTextFragments(value.content, parts);
41
+ }
42
+ if (typeof value.message === 'string' || Array.isArray(value.message) || value.message) {
43
+ appendTextFragments(value.message, parts);
44
+ }
45
+ }
46
+ }
47
+
48
+ function extractText(event) {
49
+ const parts = [];
50
+ appendTextFragments(event, parts);
51
+ return parts.join('\n').trim();
52
+ }
53
+
54
+ function findSessionId(event) {
55
+ return event?.session_id
56
+ || event?.sessionId
57
+ || event?.message?.session_id
58
+ || event?.message?.sessionId
59
+ || null;
60
+ }
61
+
62
+ function createWorkerError(message, details = {}) {
63
+ const error = new Error(message);
64
+ Object.assign(error, details);
65
+ return error;
66
+ }
67
+
68
+ function buildClaudeArgs(worker, options) {
69
+ const args = [...worker.commandArgs];
70
+
71
+ args.push('--print');
72
+ args.push('--input-format', 'stream-json');
73
+ args.push('--output-format', 'stream-json');
74
+
75
+ if (options.includePartialMessages) args.push('--include-partial-messages');
76
+ if (options.replayUserMessages) args.push('--replay-user-messages');
77
+ if (options.model) args.push('--model', options.model);
78
+ if (options.allowDangerouslySkipPermissions) args.push('--dangerously-skip-permissions');
79
+ if (options.permissionMode) args.push('--permission-mode', options.permissionMode);
80
+
81
+ for (const config of toStringList(options.mcpConfig)) {
82
+ args.push('--mcp-config', config);
83
+ }
84
+
85
+ if (worker.resumeSessionId) {
86
+ args.push('--resume', worker.resumeSessionId);
87
+ }
88
+
89
+ args.push(...toStringList(options.extraArgs));
90
+
91
+ return args;
92
+ }
93
+
94
+ /**
95
+ * Claude stream-json 세션 워커
96
+ */
97
+ export class ClaudeWorker {
98
+ type = 'claude';
99
+
100
+ constructor(options = {}) {
101
+ this.command = options.command || 'claude';
102
+ this.commandArgs = toStringList(options.commandArgs || options.args);
103
+ this.cwd = options.cwd || process.cwd();
104
+ this.env = { ...process.env, ...(options.env || {}) };
105
+ this.model = options.model || null;
106
+ this.permissionMode = options.permissionMode || null;
107
+ this.allowDangerouslySkipPermissions = options.allowDangerouslySkipPermissions !== false;
108
+ this.includePartialMessages = options.includePartialMessages === true;
109
+ this.replayUserMessages = options.replayUserMessages !== false;
110
+ this.mcpConfig = toStringList(options.mcpConfig);
111
+ this.extraArgs = toStringList(options.extraArgs);
112
+ this.timeoutMs = Number(options.timeoutMs) > 0 ? Number(options.timeoutMs) : DEFAULT_TIMEOUT_MS;
113
+ this.killGraceMs = Number(options.killGraceMs) > 0 ? Number(options.killGraceMs) : DEFAULT_KILL_GRACE_MS;
114
+ this.onEvent = typeof options.onEvent === 'function' ? options.onEvent : null;
115
+ this.controlRequestHandler = typeof options.controlRequestHandler === 'function'
116
+ ? options.controlRequestHandler
117
+ : null;
118
+
119
+ this.state = 'idle';
120
+ this.child = null;
121
+ this.stdoutReader = null;
122
+ this.stderrReader = null;
123
+ this.pendingTurn = null;
124
+ this.history = [];
125
+ this.events = [];
126
+ this.stderrLines = [];
127
+ this.sessionId = null;
128
+ this.resumeSessionId = null;
129
+ this.lastTurn = null;
130
+ this._closePromise = null;
131
+ }
132
+
133
+ getStatus() {
134
+ return {
135
+ type: 'claude',
136
+ state: this.state,
137
+ pid: this.child?.pid || null,
138
+ session_id: this.sessionId,
139
+ history_length: this.history.length,
140
+ last_turn_at_ms: this.lastTurn?.finishedAtMs || null,
141
+ };
142
+ }
143
+
144
+ _writeFrame(frame) {
145
+ if (!this.child?.stdin?.writable) {
146
+ throw createWorkerError('Claude worker stdin is not writable', { code: 'WORKER_STDIN_CLOSED' });
147
+ }
148
+ this.child.stdin.write(`${JSON.stringify(frame)}\n`);
149
+ }
150
+
151
+ _terminateChild(child) {
152
+ if (!child || child.exitCode !== null || child.killed) return;
153
+ try { child.stdin.end(); } catch {}
154
+ try { child.kill(); } catch {}
155
+ const timer = setTimeout(() => {
156
+ if (child.exitCode === null) {
157
+ try { child.kill('SIGKILL'); } catch {}
158
+ }
159
+ }, this.killGraceMs);
160
+ timer.unref?.();
161
+ }
162
+
163
+ async _handleControlRequest(event) {
164
+ let responseFrame = null;
165
+
166
+ if (this.controlRequestHandler) {
167
+ responseFrame = await this.controlRequestHandler(event, { worker: this });
168
+ } else {
169
+ const requestId = event.request_id || event.requestId;
170
+ if (!requestId) return;
171
+
172
+ const successPayload = event.subtype === 'can_use_tool'
173
+ ? { decision: 'allow', allowed: true }
174
+ : { acknowledged: true, subtype: event.subtype || 'unknown' };
175
+
176
+ responseFrame = {
177
+ type: 'control_response',
178
+ response: {
179
+ request_id: requestId,
180
+ subtype: 'success',
181
+ response: successPayload,
182
+ },
183
+ };
184
+ }
185
+
186
+ if (responseFrame) {
187
+ this._writeFrame(responseFrame);
188
+ }
189
+ }
190
+
191
+ _finalizePendingTurn(turn, event) {
192
+ if (!turn || turn.completed) return;
193
+
194
+ turn.completed = true;
195
+ clearTimeout(turn.timeout);
196
+ this.pendingTurn = null;
197
+
198
+ const response = [
199
+ ...turn.assistantTexts,
200
+ extractText(event),
201
+ ]
202
+ .filter(Boolean)
203
+ .join('\n')
204
+ .trim();
205
+
206
+ if (response) {
207
+ this.history.push({
208
+ role: 'assistant',
209
+ content: response,
210
+ at_ms: Date.now(),
211
+ });
212
+ }
213
+
214
+ const result = {
215
+ type: 'claude',
216
+ sessionId: this.sessionId,
217
+ response,
218
+ assistantEvents: turn.assistantEvents,
219
+ resultEvent: event,
220
+ stderr: this.stderrLines.join('\n').trim(),
221
+ history: [...this.history],
222
+ startedAtMs: turn.startedAtMs,
223
+ finishedAtMs: Date.now(),
224
+ durationMs: Date.now() - turn.startedAtMs,
225
+ };
226
+
227
+ this.lastTurn = result;
228
+ turn.resolve(result);
229
+ }
230
+
231
+ _rejectPendingTurn(error) {
232
+ if (!this.pendingTurn || this.pendingTurn.completed) return;
233
+ const turn = this.pendingTurn;
234
+ turn.completed = true;
235
+ clearTimeout(turn.timeout);
236
+ this.pendingTurn = null;
237
+ turn.reject(error);
238
+ }
239
+
240
+ _handleStdoutLine(line) {
241
+ if (!line) return;
242
+ const event = safeJsonParse(line);
243
+ if (!event) return;
244
+
245
+ this.events.push(event);
246
+ const sessionId = findSessionId(event);
247
+ if (sessionId) {
248
+ this.sessionId = sessionId;
249
+ this.resumeSessionId = sessionId;
250
+ }
251
+
252
+ if (this.onEvent) {
253
+ try { this.onEvent(event); } catch {}
254
+ }
255
+
256
+ if (event.type === 'control_request') {
257
+ void this._handleControlRequest(event);
258
+ return;
259
+ }
260
+
261
+ if (event.type === 'assistant' || event.type === 'streamlined_text') {
262
+ const text = extractText(event);
263
+ if (this.pendingTurn && text) {
264
+ this.pendingTurn.assistantTexts.push(text);
265
+ this.pendingTurn.assistantEvents.push(event);
266
+ }
267
+ return;
268
+ }
269
+
270
+ if (event.type === 'result' && this.pendingTurn) {
271
+ this._finalizePendingTurn(this.pendingTurn, event);
272
+ }
273
+ }
274
+
275
+ async start() {
276
+ if (this.child && this.child.exitCode === null) {
277
+ return this.getStatus();
278
+ }
279
+
280
+ const args = buildClaudeArgs(this, {
281
+ model: this.model,
282
+ permissionMode: this.permissionMode,
283
+ allowDangerouslySkipPermissions: this.allowDangerouslySkipPermissions,
284
+ includePartialMessages: this.includePartialMessages,
285
+ replayUserMessages: this.replayUserMessages,
286
+ mcpConfig: this.mcpConfig,
287
+ extraArgs: this.extraArgs,
288
+ });
289
+
290
+ const child = spawn(this.command, args, {
291
+ cwd: this.cwd,
292
+ env: this.env,
293
+ stdio: ['pipe', 'pipe', 'pipe'],
294
+ windowsHide: true,
295
+ });
296
+
297
+ this.child = child;
298
+ this.state = 'ready';
299
+ this.stderrLines = [];
300
+ this.stdoutReader = readline.createInterface({
301
+ input: child.stdout,
302
+ crlfDelay: Infinity,
303
+ });
304
+ this.stderrReader = readline.createInterface({
305
+ input: child.stderr,
306
+ crlfDelay: Infinity,
307
+ });
308
+
309
+ this.stdoutReader.on('line', (line) => this._handleStdoutLine(line));
310
+ this.stderrReader.on('line', (line) => {
311
+ if (!line) return;
312
+ this.stderrLines.push(line);
313
+ });
314
+
315
+ this._closePromise = new Promise((resolve, reject) => {
316
+ child.once('error', reject);
317
+ child.once('close', (code, signal) => {
318
+ const closeError = code === 0
319
+ ? null
320
+ : createWorkerError(`Claude worker exited with code ${code}`, {
321
+ code: 'WORKER_EXIT',
322
+ exitCode: code,
323
+ exitSignal: signal,
324
+ stderr: this.stderrLines.join('\n').trim(),
325
+ });
326
+
327
+ this.child = null;
328
+ this.state = 'idle';
329
+ try { this.stdoutReader?.close(); } catch {}
330
+ try { this.stderrReader?.close(); } catch {}
331
+ this.stdoutReader = null;
332
+ this.stderrReader = null;
333
+ if (closeError) this._rejectPendingTurn(closeError);
334
+ resolve({ code, signal });
335
+ });
336
+ });
337
+
338
+ return this.getStatus();
339
+ }
340
+
341
+ async stop() {
342
+ if (!this.child) {
343
+ this.state = 'stopped';
344
+ return this.getStatus();
345
+ }
346
+
347
+ const child = this.child;
348
+ this._terminateChild(child);
349
+ await Promise.race([
350
+ this._closePromise,
351
+ new Promise((resolve) => setTimeout(resolve, this.killGraceMs + 50)),
352
+ ]);
353
+
354
+ this.child = null;
355
+ this.state = 'stopped';
356
+ return this.getStatus();
357
+ }
358
+
359
+ async restart() {
360
+ await this.stop();
361
+ this.state = 'idle';
362
+ return this.start();
363
+ }
364
+
365
+ async run(prompt, options = {}) {
366
+ await this.start();
367
+
368
+ if (this.pendingTurn) {
369
+ throw createWorkerError('ClaudeWorker is already handling another turn', { code: 'WORKER_BUSY' });
370
+ }
371
+
372
+ const timeoutMs = Number(options.timeoutMs) > 0 ? Number(options.timeoutMs) : this.timeoutMs;
373
+ const userText = String(prompt ?? '');
374
+ const startedAtMs = Date.now();
375
+
376
+ this.history.push({
377
+ role: 'user',
378
+ content: userText,
379
+ at_ms: startedAtMs,
380
+ });
381
+
382
+ const turnPromise = new Promise((resolve, reject) => {
383
+ const timeout = setTimeout(() => {
384
+ const timeoutError = createWorkerError(`Claude worker timed out after ${timeoutMs}ms`, {
385
+ code: 'ETIMEDOUT',
386
+ stderr: this.stderrLines.join('\n').trim(),
387
+ });
388
+ this._rejectPendingTurn(timeoutError);
389
+ this._terminateChild(this.child);
390
+ }, timeoutMs);
391
+ timeout.unref?.();
392
+
393
+ this.pendingTurn = {
394
+ startedAtMs,
395
+ assistantTexts: [],
396
+ assistantEvents: [],
397
+ timeout,
398
+ resolve,
399
+ reject,
400
+ completed: false,
401
+ };
402
+ });
403
+
404
+ this.state = 'running';
405
+ this._writeFrame({
406
+ type: 'user',
407
+ message: {
408
+ role: 'user',
409
+ content: userText,
410
+ },
411
+ });
412
+
413
+ try {
414
+ return await turnPromise;
415
+ } finally {
416
+ if (this.child) {
417
+ this.state = 'ready';
418
+ }
419
+ }
420
+ }
421
+
422
+ isReady() {
423
+ return this.state === 'ready' || this.state === 'running';
424
+ }
425
+
426
+ async execute(prompt, options = {}) {
427
+ try {
428
+ const result = await this.run(prompt, options);
429
+ return {
430
+ output: result.response,
431
+ exitCode: 0,
432
+ threadId: null,
433
+ sessionKey: options.sessionKey || this.sessionId || null,
434
+ raw: result,
435
+ };
436
+ } catch (error) {
437
+ return {
438
+ output: error.stderr || error.message || 'Claude worker failed',
439
+ exitCode: error.code === 'ETIMEDOUT' ? 124 : 1,
440
+ threadId: null,
441
+ sessionKey: options.sessionKey || this.sessionId || null,
442
+ raw: null,
443
+ };
444
+ }
445
+ }
446
+ }