triflux 3.2.0-dev.1 → 3.2.0-dev.11

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 (53) hide show
  1. package/README.ko.md +26 -18
  2. package/README.md +26 -18
  3. package/bin/triflux.mjs +1614 -1084
  4. package/hooks/hooks.json +12 -0
  5. package/hooks/keyword-rules.json +354 -0
  6. package/hub/bridge.mjs +371 -193
  7. package/hub/hitl.mjs +45 -31
  8. package/hub/pipe.mjs +457 -0
  9. package/hub/router.mjs +422 -161
  10. package/hub/server.mjs +429 -344
  11. package/hub/store.mjs +388 -314
  12. package/hub/team/cli-team-common.mjs +348 -0
  13. package/hub/team/cli-team-control.mjs +393 -0
  14. package/hub/team/cli-team-start.mjs +516 -0
  15. package/hub/team/cli-team-status.mjs +269 -0
  16. package/hub/team/cli.mjs +99 -368
  17. package/hub/team/dashboard.mjs +165 -64
  18. package/hub/team/native-supervisor.mjs +300 -0
  19. package/hub/team/native.mjs +62 -0
  20. package/hub/team/nativeProxy.mjs +534 -0
  21. package/hub/team/orchestrator.mjs +90 -31
  22. package/hub/team/pane.mjs +149 -101
  23. package/hub/team/psmux.mjs +297 -0
  24. package/hub/team/session.mjs +608 -186
  25. package/hub/team/shared.mjs +13 -0
  26. package/hub/team/staleState.mjs +299 -0
  27. package/hub/tools.mjs +140 -53
  28. package/hub/workers/claude-worker.mjs +446 -0
  29. package/hub/workers/codex-mcp.mjs +414 -0
  30. package/hub/workers/factory.mjs +18 -0
  31. package/hub/workers/gemini-worker.mjs +349 -0
  32. package/hub/workers/interface.mjs +41 -0
  33. package/hud/hud-qos-status.mjs +1789 -1732
  34. package/package.json +6 -2
  35. package/scripts/__tests__/keyword-detector.test.mjs +234 -0
  36. package/scripts/hub-ensure.mjs +83 -0
  37. package/scripts/keyword-detector.mjs +272 -0
  38. package/scripts/keyword-rules-expander.mjs +521 -0
  39. package/scripts/lib/keyword-rules.mjs +168 -0
  40. package/scripts/psmux-steering-prototype.sh +368 -0
  41. package/scripts/run.cjs +62 -0
  42. package/scripts/setup.mjs +189 -7
  43. package/scripts/test-tfx-route-no-claude-native.mjs +49 -0
  44. package/scripts/tfx-route-worker.mjs +161 -0
  45. package/scripts/tfx-route.sh +943 -508
  46. package/skills/tfx-auto/SKILL.md +90 -564
  47. package/skills/tfx-auto-codex/SKILL.md +77 -0
  48. package/skills/tfx-codex/SKILL.md +1 -4
  49. package/skills/tfx-doctor/SKILL.md +1 -0
  50. package/skills/tfx-gemini/SKILL.md +1 -4
  51. package/skills/tfx-multi/SKILL.md +296 -0
  52. package/skills/tfx-setup/SKILL.md +1 -4
  53. package/skills/tfx-team/SKILL.md +0 -172
@@ -0,0 +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
+ }