triflux 3.3.0-dev.7 → 3.3.0-dev.8

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/hub/bridge.mjs CHANGED
@@ -295,6 +295,17 @@ async function requestHub(operation, body, timeoutMs = 3000, fallback = null) {
295
295
  return { transport: 'fallback', result: viaFallback };
296
296
  }
297
297
 
298
+ function unavailableResult() {
299
+ return { ok: false, reason: 'hub_unavailable' };
300
+ }
301
+
302
+ function emitJson(payload) {
303
+ if (payload !== undefined) {
304
+ console.log(JSON.stringify(payload));
305
+ }
306
+ return payload?.ok !== false;
307
+ }
308
+
298
309
  async function cmdRegister(args) {
299
310
  const agentId = args.agent;
300
311
  const timeoutSec = parseInt(args.timeout || '600', 10);
@@ -313,15 +324,15 @@ async function cmdRegister(args) {
313
324
  const result = outcome?.result;
314
325
 
315
326
  if (result?.ok) {
316
- console.log(JSON.stringify({
327
+ return emitJson({
317
328
  ok: true,
318
329
  agent_id: agentId,
319
330
  lease_expires_ms: result.data?.lease_expires_ms,
320
331
  pipe_path: result.data?.pipe_path || getHubPipePath(),
321
- }));
322
- } else {
323
- console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
332
+ });
324
333
  }
334
+
335
+ return emitJson(result || unavailableResult());
325
336
  }
326
337
 
327
338
  async function cmdResult(args) {
@@ -349,10 +360,10 @@ async function cmdResult(args) {
349
360
  const result = outcome?.result;
350
361
 
351
362
  if (result?.ok) {
352
- console.log(JSON.stringify({ ok: true, message_id: result.data?.message_id }));
353
- } else {
354
- console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
363
+ return emitJson({ ok: true, message_id: result.data?.message_id });
355
364
  }
365
+
366
+ return emitJson(result || unavailableResult());
356
367
  }
357
368
 
358
369
  async function cmdControl(args) {
@@ -367,11 +378,7 @@ async function cmdControl(args) {
367
378
  ttl_ms: args['ttl-ms'] != null ? Number(args['ttl-ms']) : undefined,
368
379
  });
369
380
  const result = outcome?.result;
370
- if (result) {
371
- console.log(JSON.stringify(result));
372
- } else {
373
- console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
374
- }
381
+ return emitJson(result || unavailableResult());
375
382
  }
376
383
 
377
384
  async function cmdContext(args) {
@@ -394,14 +401,21 @@ async function cmdContext(args) {
394
401
 
395
402
  if (args.out) {
396
403
  writeFileSync(args.out, combined, 'utf8');
397
- console.log(JSON.stringify({ ok: true, count: result.data.messages.length, file: args.out }));
404
+ return emitJson({ ok: true, count: result.data.messages.length, file: args.out });
398
405
  } else {
399
406
  console.log(combined);
407
+ return true;
400
408
  }
401
- return;
402
409
  }
403
410
 
404
- if (args.out) console.log(JSON.stringify({ ok: true, count: 0 }));
411
+ if (result?.ok) {
412
+ if (args.out) {
413
+ return emitJson({ ok: true, count: 0 });
414
+ }
415
+ return true;
416
+ }
417
+
418
+ return emitJson(result || unavailableResult());
405
419
  }
406
420
 
407
421
  async function cmdDeregister(args) {
@@ -411,10 +425,10 @@ async function cmdDeregister(args) {
411
425
  const result = outcome?.result;
412
426
 
413
427
  if (result?.ok) {
414
- console.log(JSON.stringify({ ok: true, agent_id: args.agent, status: 'offline' }));
415
- } else {
416
- console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
428
+ return emitJson({ ok: true, agent_id: args.agent, status: 'offline' });
417
429
  }
430
+
431
+ return emitJson(result || unavailableResult());
418
432
  }
419
433
 
420
434
  async function cmdAssignAsync(args) {
@@ -432,11 +446,7 @@ async function cmdAssignAsync(args) {
432
446
  correlation_id: args.correlation || undefined,
433
447
  });
434
448
  const result = outcome?.result;
435
- if (result) {
436
- console.log(JSON.stringify(result));
437
- } else {
438
- console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
439
- }
449
+ return emitJson(result || unavailableResult());
440
450
  }
441
451
 
442
452
  async function cmdAssignResult(args) {
@@ -451,11 +461,7 @@ async function cmdAssignResult(args) {
451
461
  metadata: args.metadata ? parseJsonSafe(args.metadata, {}) : {},
452
462
  });
453
463
  const result = outcome?.result;
454
- if (result) {
455
- console.log(JSON.stringify(result));
456
- } else {
457
- console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
458
- }
464
+ return emitJson(result || unavailableResult());
459
465
  }
460
466
 
461
467
  async function cmdAssignStatus(args) {
@@ -463,11 +469,7 @@ async function cmdAssignStatus(args) {
463
469
  job_id: args['job-id'],
464
470
  });
465
471
  const result = outcome?.result;
466
- if (result) {
467
- console.log(JSON.stringify(result));
468
- } else {
469
- console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
470
- }
472
+ return emitJson(result || unavailableResult());
471
473
  }
472
474
 
473
475
  async function cmdAssignRetry(args) {
@@ -477,11 +479,7 @@ async function cmdAssignRetry(args) {
477
479
  requested_by: args['requested-by'],
478
480
  });
479
481
  const result = outcome?.result;
480
- if (result) {
481
- console.log(JSON.stringify(result));
482
- } else {
483
- console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
484
- }
482
+ return emitJson(result || unavailableResult());
485
483
  }
486
484
 
487
485
  async function cmdTeamInfo(args) {
@@ -495,9 +493,7 @@ async function cmdTeamInfo(args) {
495
493
  return await teamInfo(body);
496
494
  });
497
495
  const result = outcome?.result;
498
- if (result) {
499
- console.log(JSON.stringify(result));
500
- }
496
+ return emitJson(result || unavailableResult());
501
497
  }
502
498
 
503
499
  async function cmdTeamTaskList(args) {
@@ -513,9 +509,7 @@ async function cmdTeamTaskList(args) {
513
509
  return await teamTaskList(body);
514
510
  });
515
511
  const result = outcome?.result;
516
- if (result) {
517
- console.log(JSON.stringify(result));
518
- }
512
+ return emitJson(result || unavailableResult());
519
513
  }
520
514
 
521
515
  async function cmdTeamTaskUpdate(args) {
@@ -539,9 +533,7 @@ async function cmdTeamTaskUpdate(args) {
539
533
  return await teamTaskUpdate(body);
540
534
  });
541
535
  const result = outcome?.result;
542
- if (result) {
543
- console.log(JSON.stringify(result));
544
- }
536
+ return emitJson(result || unavailableResult());
545
537
  }
546
538
 
547
539
  async function cmdTeamSendMessage(args) {
@@ -558,9 +550,7 @@ async function cmdTeamSendMessage(args) {
558
550
  return await teamSendMessage(body);
559
551
  });
560
552
  const result = outcome?.result;
561
- if (result) {
562
- console.log(JSON.stringify(result));
563
- }
553
+ return emitJson(result || unavailableResult());
564
554
  }
565
555
 
566
556
  function getHubDbPath() {
@@ -588,9 +578,7 @@ async function cmdPipelineState(args) {
588
578
  }
589
579
  });
590
580
  const result = outcome?.result;
591
- if (result) {
592
- console.log(JSON.stringify(result));
593
- }
581
+ return emitJson(result || unavailableResult());
594
582
  }
595
583
 
596
584
  async function cmdPipelineAdvance(args) {
@@ -616,9 +604,7 @@ async function cmdPipelineAdvance(args) {
616
604
  }
617
605
  });
618
606
  const result = outcome?.result;
619
- if (result) {
620
- console.log(JSON.stringify(result));
621
- }
607
+ return emitJson(result || unavailableResult());
622
608
  }
623
609
 
624
610
  async function cmdPipelineInit(args) {
@@ -628,49 +614,39 @@ async function cmdPipelineInit(args) {
628
614
  ralph_max: args['ralph-max'] != null ? Number(args['ralph-max']) : undefined,
629
615
  });
630
616
  const result = outcome?.result;
631
- if (result) {
632
- console.log(JSON.stringify(result));
633
- } else {
634
- console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
635
- }
617
+ return emitJson(result || unavailableResult());
636
618
  }
637
619
 
638
620
  async function cmdPipelineList() {
639
621
  const outcome = await requestHub(HUB_OPERATIONS.pipelineList, {});
640
622
  const result = outcome?.result;
641
- if (result) {
642
- console.log(JSON.stringify(result));
643
- } else {
644
- console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
645
- }
623
+ return emitJson(result || unavailableResult());
646
624
  }
647
625
 
648
626
  async function cmdPing() {
649
627
  const outcome = await requestHub(HUB_OPERATIONS.hubStatus, { scope: 'hub' }, 2000);
650
628
 
651
629
  if (outcome?.transport === 'pipe' && outcome.result?.ok) {
652
- console.log(JSON.stringify({
630
+ return emitJson({
653
631
  ok: true,
654
632
  hub: outcome.result.data?.hub?.state || 'healthy',
655
633
  pipe_path: getHubPipePath(),
656
634
  transport: 'pipe',
657
- }));
658
- return;
635
+ });
659
636
  }
660
637
 
661
638
  if (outcome?.transport === 'http' && outcome.result) {
662
639
  const data = outcome.result;
663
- console.log(JSON.stringify({
640
+ return emitJson({
664
641
  ok: true,
665
642
  hub: data.hub?.state,
666
643
  sessions: data.sessions,
667
644
  pipe_path: data.pipe?.path || data.pipe_path || null,
668
645
  transport: 'http',
669
- }));
670
- return;
646
+ });
671
647
  }
672
648
 
673
- console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
649
+ return emitJson(unavailableResult());
674
650
  }
675
651
 
676
652
  export async function main(argv = process.argv.slice(2)) {
@@ -678,24 +654,24 @@ export async function main(argv = process.argv.slice(2)) {
678
654
  const args = parseArgs(argv.slice(1));
679
655
 
680
656
  switch (cmd) {
681
- case 'register': await cmdRegister(args); break;
682
- case 'result': await cmdResult(args); break;
683
- case 'control': await cmdControl(args); break;
684
- case 'context': await cmdContext(args); break;
685
- case 'deregister': await cmdDeregister(args); break;
686
- case 'assign-async': await cmdAssignAsync(args); break;
687
- case 'assign-result': await cmdAssignResult(args); break;
688
- case 'assign-status': await cmdAssignStatus(args); break;
689
- case 'assign-retry': await cmdAssignRetry(args); break;
690
- case 'team-info': await cmdTeamInfo(args); break;
691
- case 'team-task-list': await cmdTeamTaskList(args); break;
692
- case 'team-task-update': await cmdTeamTaskUpdate(args); break;
693
- case 'team-send-message': await cmdTeamSendMessage(args); break;
694
- case 'pipeline-state': await cmdPipelineState(args); break;
695
- case 'pipeline-advance': await cmdPipelineAdvance(args); break;
696
- case 'pipeline-init': await cmdPipelineInit(args); break;
697
- case 'pipeline-list': await cmdPipelineList(args); break;
698
- case 'ping': await cmdPing(args); break;
657
+ case 'register': return await cmdRegister(args);
658
+ case 'result': return await cmdResult(args);
659
+ case 'control': return await cmdControl(args);
660
+ case 'context': return await cmdContext(args);
661
+ case 'deregister': return await cmdDeregister(args);
662
+ case 'assign-async': return await cmdAssignAsync(args);
663
+ case 'assign-result': return await cmdAssignResult(args);
664
+ case 'assign-status': return await cmdAssignStatus(args);
665
+ case 'assign-retry': return await cmdAssignRetry(args);
666
+ case 'team-info': return await cmdTeamInfo(args);
667
+ case 'team-task-list': return await cmdTeamTaskList(args);
668
+ case 'team-task-update': return await cmdTeamTaskUpdate(args);
669
+ case 'team-send-message': return await cmdTeamSendMessage(args);
670
+ case 'pipeline-state': return await cmdPipelineState(args);
671
+ case 'pipeline-advance': return await cmdPipelineAdvance(args);
672
+ case 'pipeline-init': return await cmdPipelineInit(args);
673
+ case 'pipeline-list': return await cmdPipelineList(args);
674
+ case 'ping': return await cmdPing(args);
699
675
  default:
700
676
  console.error('사용법: bridge.mjs <register|result|control|context|deregister|assign-async|assign-result|assign-status|assign-retry|team-info|team-task-list|team-task-update|team-send-message|pipeline-state|pipeline-advance|pipeline-init|pipeline-list|ping> [--옵션]');
701
677
  process.exit(1);
@@ -704,5 +680,5 @@ export async function main(argv = process.argv.slice(2)) {
704
680
 
705
681
  const selfRun = process.argv[1]?.replace(/\\/g, '/').endsWith('hub/bridge.mjs');
706
682
  if (selfRun) {
707
- await main();
683
+ process.exitCode = await main() ? 0 : 1;
708
684
  }
@@ -0,0 +1,38 @@
1
+ export const DELEGATOR_MCP_SERVER_INFO = Object.freeze({
2
+ name: 'triflux-delegator',
3
+ version: '0.1.0',
4
+ });
5
+
6
+ export const DELEGATOR_TOOL_NAMES = Object.freeze({
7
+ delegate: 'delegate',
8
+ delegateReply: 'delegate-reply',
9
+ status: 'status',
10
+ });
11
+
12
+ export const DELEGATOR_PIPE_ACTIONS = Object.freeze({
13
+ delegate: 'delegator_delegate',
14
+ delegateReply: 'delegator_reply',
15
+ status: 'delegator_status',
16
+ });
17
+
18
+ export const DELEGATOR_JOB_STATUSES = Object.freeze([
19
+ 'queued',
20
+ 'running',
21
+ 'waiting_reply',
22
+ 'completed',
23
+ 'failed',
24
+ ]);
25
+
26
+ export const DELEGATOR_MODES = Object.freeze([
27
+ 'sync',
28
+ 'async',
29
+ ]);
30
+
31
+ export const DELEGATOR_PROVIDERS = Object.freeze([
32
+ 'auto',
33
+ 'codex',
34
+ 'gemini',
35
+ 'claude',
36
+ ]);
37
+
38
+ export const DELEGATOR_SCHEMA_URL = new URL('./schema/delegator-tools.schema.json', import.meta.url);
@@ -0,0 +1,14 @@
1
+ export {
2
+ DELEGATOR_JOB_STATUSES,
3
+ DELEGATOR_MCP_SERVER_INFO,
4
+ DELEGATOR_MODES,
5
+ DELEGATOR_PIPE_ACTIONS,
6
+ DELEGATOR_PROVIDERS,
7
+ DELEGATOR_SCHEMA_URL,
8
+ DELEGATOR_TOOL_NAMES,
9
+ } from './contracts.mjs';
10
+ export { DelegatorService } from './service.mjs';
11
+ export {
12
+ getDelegatorMcpToolDefinitions,
13
+ loadDelegatorSchemaBundle,
14
+ } from './tool-definitions.mjs';
@@ -0,0 +1,250 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://triflux.dev/schema/hub/delegator-tools.schema.json",
4
+ "title": "Triflux Delegator MCP Tool Schemas",
5
+ "$defs": {
6
+ "Provider": {
7
+ "type": "string",
8
+ "enum": ["auto", "codex", "gemini", "claude"]
9
+ },
10
+ "Mode": {
11
+ "type": "string",
12
+ "enum": ["sync", "async"]
13
+ },
14
+ "JobStatus": {
15
+ "type": "string",
16
+ "enum": ["queued", "running", "waiting_reply", "completed", "failed"]
17
+ },
18
+ "McpProfile": {
19
+ "type": "string",
20
+ "enum": [
21
+ "auto",
22
+ "default",
23
+ "executor",
24
+ "designer",
25
+ "explore",
26
+ "reviewer",
27
+ "writer",
28
+ "none",
29
+ "implement",
30
+ "analyze",
31
+ "review",
32
+ "docs",
33
+ "minimal"
34
+ ]
35
+ },
36
+ "SearchTool": {
37
+ "type": "string",
38
+ "enum": ["exa", "brave-search", "tavily"]
39
+ },
40
+ "NullableDateTime": {
41
+ "type": ["string", "null"],
42
+ "format": "date-time"
43
+ },
44
+ "NullableString": {
45
+ "type": ["string", "null"]
46
+ },
47
+ "DelegateInput": {
48
+ "type": "object",
49
+ "additionalProperties": false,
50
+ "required": ["prompt"],
51
+ "properties": {
52
+ "prompt": {
53
+ "type": "string",
54
+ "minLength": 1
55
+ },
56
+ "provider": {
57
+ "$ref": "#/$defs/Provider",
58
+ "default": "auto"
59
+ },
60
+ "mode": {
61
+ "$ref": "#/$defs/Mode",
62
+ "default": "sync"
63
+ },
64
+ "agent_type": {
65
+ "type": "string",
66
+ "default": "executor",
67
+ "minLength": 1
68
+ },
69
+ "cwd": {
70
+ "type": "string",
71
+ "minLength": 1
72
+ },
73
+ "timeout_ms": {
74
+ "type": "integer",
75
+ "minimum": 1
76
+ },
77
+ "session_key": {
78
+ "type": "string",
79
+ "minLength": 1
80
+ },
81
+ "thread_id": {
82
+ "type": "string",
83
+ "minLength": 1
84
+ },
85
+ "reset_session": {
86
+ "type": "boolean",
87
+ "default": false
88
+ },
89
+ "mcp_profile": {
90
+ "$ref": "#/$defs/McpProfile",
91
+ "default": "auto"
92
+ },
93
+ "search_tool": {
94
+ "$ref": "#/$defs/SearchTool"
95
+ },
96
+ "context_file": {
97
+ "type": "string",
98
+ "minLength": 1
99
+ },
100
+ "model": {
101
+ "type": "string",
102
+ "minLength": 1
103
+ },
104
+ "developer_instructions": {
105
+ "type": "string",
106
+ "minLength": 1
107
+ },
108
+ "compact_prompt": {
109
+ "type": "string",
110
+ "minLength": 1
111
+ }
112
+ }
113
+ },
114
+ "DelegateReplyInput": {
115
+ "type": "object",
116
+ "additionalProperties": false,
117
+ "required": ["job_id", "reply"],
118
+ "properties": {
119
+ "job_id": {
120
+ "type": "string",
121
+ "minLength": 1
122
+ },
123
+ "reply": {
124
+ "type": "string",
125
+ "minLength": 1
126
+ },
127
+ "done": {
128
+ "type": "boolean",
129
+ "default": false
130
+ }
131
+ }
132
+ },
133
+ "StatusInput": {
134
+ "type": "object",
135
+ "additionalProperties": false,
136
+ "required": ["job_id"],
137
+ "properties": {
138
+ "job_id": {
139
+ "type": "string",
140
+ "minLength": 1
141
+ }
142
+ }
143
+ },
144
+ "DelegateOutput": {
145
+ "type": "object",
146
+ "additionalProperties": false,
147
+ "required": [
148
+ "ok",
149
+ "job_id",
150
+ "status",
151
+ "mode",
152
+ "provider_requested",
153
+ "provider_resolved",
154
+ "agent_type",
155
+ "transport",
156
+ "created_at",
157
+ "started_at",
158
+ "updated_at",
159
+ "completed_at",
160
+ "thread_id",
161
+ "session_key",
162
+ "conversation_open"
163
+ ],
164
+ "properties": {
165
+ "ok": {
166
+ "type": "boolean"
167
+ },
168
+ "job_id": {
169
+ "type": "string",
170
+ "minLength": 1
171
+ },
172
+ "status": {
173
+ "$ref": "#/$defs/JobStatus"
174
+ },
175
+ "mode": {
176
+ "$ref": "#/$defs/Mode"
177
+ },
178
+ "provider_requested": {
179
+ "$ref": "#/$defs/Provider"
180
+ },
181
+ "provider_resolved": {
182
+ "type": ["string", "null"]
183
+ },
184
+ "agent_type": {
185
+ "type": "string",
186
+ "minLength": 1
187
+ },
188
+ "transport": {
189
+ "type": "string",
190
+ "minLength": 1
191
+ },
192
+ "created_at": {
193
+ "type": "string",
194
+ "format": "date-time"
195
+ },
196
+ "started_at": {
197
+ "$ref": "#/$defs/NullableDateTime"
198
+ },
199
+ "updated_at": {
200
+ "type": "string",
201
+ "format": "date-time"
202
+ },
203
+ "completed_at": {
204
+ "$ref": "#/$defs/NullableDateTime"
205
+ },
206
+ "output": {
207
+ "type": "string"
208
+ },
209
+ "stderr": {
210
+ "type": "string"
211
+ },
212
+ "error": {
213
+ "type": "string"
214
+ },
215
+ "thread_id": {
216
+ "$ref": "#/$defs/NullableString"
217
+ },
218
+ "session_key": {
219
+ "$ref": "#/$defs/NullableString"
220
+ },
221
+ "conversation_open": {
222
+ "type": "boolean"
223
+ }
224
+ }
225
+ }
226
+ },
227
+ "x-triflux-mcp-tools": [
228
+ {
229
+ "name": "delegate",
230
+ "description": "Create a new delegator job and optionally wait for the first result.",
231
+ "inputSchemaDef": "DelegateInput",
232
+ "outputSchemaDef": "DelegateOutput",
233
+ "pipeAction": "delegator_delegate"
234
+ },
235
+ {
236
+ "name": "delegate-reply",
237
+ "description": "Send a follow-up reply into an existing delegator conversation job.",
238
+ "inputSchemaDef": "DelegateReplyInput",
239
+ "outputSchemaDef": "DelegateOutput",
240
+ "pipeAction": "delegator_reply"
241
+ },
242
+ {
243
+ "name": "status",
244
+ "description": "Read the latest snapshot for an existing delegator job.",
245
+ "inputSchemaDef": "StatusInput",
246
+ "outputSchemaDef": "DelegateOutput",
247
+ "pipeAction": "delegator_status"
248
+ }
249
+ ]
250
+ }
@@ -0,0 +1,118 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ import {
4
+ DELEGATOR_JOB_STATUSES,
5
+ DELEGATOR_MODES,
6
+ DELEGATOR_PROVIDERS,
7
+ } from './contracts.mjs';
8
+ import { getDelegatorMcpToolDefinitions } from './tool-definitions.mjs';
9
+
10
+ function deepClone(value) {
11
+ if (value == null) return value;
12
+ return JSON.parse(JSON.stringify(value));
13
+ }
14
+
15
+ function assertKnown(enumValues, value, fieldName) {
16
+ if (value == null) return;
17
+ if (!enumValues.includes(value)) {
18
+ throw new Error(`Unsupported ${fieldName}: ${value}`);
19
+ }
20
+ }
21
+
22
+ export class DelegatorService {
23
+ constructor({
24
+ idFactory = randomUUID,
25
+ now = () => new Date(),
26
+ } = {}) {
27
+ this.idFactory = idFactory;
28
+ this.now = now;
29
+ this.jobs = new Map();
30
+ }
31
+
32
+ listToolDefinitions() {
33
+ return getDelegatorMcpToolDefinitions();
34
+ }
35
+
36
+ createJobSnapshot(input = {}) {
37
+ const timestamp = this.now().toISOString();
38
+ const jobId = input.job_id || this.idFactory();
39
+ const mode = input.mode || 'sync';
40
+ const providerRequested = input.provider || 'auto';
41
+
42
+ assertKnown(DELEGATOR_MODES, mode, 'mode');
43
+ assertKnown(DELEGATOR_PROVIDERS, providerRequested, 'provider');
44
+
45
+ return {
46
+ ok: true,
47
+ job_id: jobId,
48
+ status: 'queued',
49
+ mode,
50
+ provider_requested: providerRequested,
51
+ provider_resolved: null,
52
+ agent_type: input.agent_type || 'executor',
53
+ transport: 'resident-pending',
54
+ created_at: timestamp,
55
+ started_at: null,
56
+ updated_at: timestamp,
57
+ completed_at: null,
58
+ output: '',
59
+ stderr: '',
60
+ error: '',
61
+ thread_id: input.thread_id || null,
62
+ session_key: input.session_key || null,
63
+ conversation_open: false,
64
+ };
65
+ }
66
+
67
+ recordJob(snapshot) {
68
+ if (!snapshot?.job_id) {
69
+ throw new Error('job_id is required');
70
+ }
71
+ assertKnown(DELEGATOR_JOB_STATUSES, snapshot.status, 'status');
72
+ this.jobs.set(snapshot.job_id, deepClone(snapshot));
73
+ return this.getStatusSnapshot(snapshot.job_id);
74
+ }
75
+
76
+ getStatusSnapshot(jobId) {
77
+ const snapshot = this.jobs.get(jobId);
78
+ return snapshot ? deepClone(snapshot) : null;
79
+ }
80
+
81
+ async delegate(_input) {
82
+ throw new Error('Not implemented: wire delegate to the resident worker pool and Hub pipe action.');
83
+ }
84
+
85
+ async reply(_input) {
86
+ throw new Error('Not implemented: wire delegate-reply to the resident conversation handler.');
87
+ }
88
+
89
+ async status({ job_id: jobId } = {}) {
90
+ const snapshot = this.getStatusSnapshot(jobId);
91
+ if (snapshot) {
92
+ return snapshot;
93
+ }
94
+
95
+ const timestamp = this.now().toISOString();
96
+
97
+ return {
98
+ ok: false,
99
+ job_id: jobId || 'unknown-job',
100
+ status: 'failed',
101
+ mode: 'async',
102
+ provider_requested: 'auto',
103
+ provider_resolved: null,
104
+ agent_type: 'executor',
105
+ transport: 'resident-pending',
106
+ created_at: timestamp,
107
+ started_at: null,
108
+ updated_at: timestamp,
109
+ completed_at: null,
110
+ output: '',
111
+ stderr: '',
112
+ error: 'job not found',
113
+ thread_id: null,
114
+ session_key: null,
115
+ conversation_open: false,
116
+ };
117
+ }
118
+ }
@@ -0,0 +1,35 @@
1
+ import { readFileSync } from 'node:fs';
2
+
3
+ import { DELEGATOR_SCHEMA_URL } from './contracts.mjs';
4
+
5
+ let schemaBundleCache = null;
6
+
7
+ function deepClone(value) {
8
+ if (value == null) return value;
9
+ return JSON.parse(JSON.stringify(value));
10
+ }
11
+
12
+ export function loadDelegatorSchemaBundle() {
13
+ if (schemaBundleCache) {
14
+ return schemaBundleCache;
15
+ }
16
+
17
+ schemaBundleCache = JSON.parse(readFileSync(DELEGATOR_SCHEMA_URL, 'utf8'));
18
+ return schemaBundleCache;
19
+ }
20
+
21
+ export function getDelegatorMcpToolDefinitions() {
22
+ const bundle = loadDelegatorSchemaBundle();
23
+ const defs = bundle.$defs || {};
24
+ const tools = Array.isArray(bundle['x-triflux-mcp-tools'])
25
+ ? bundle['x-triflux-mcp-tools']
26
+ : [];
27
+
28
+ return tools.map((tool) => ({
29
+ name: tool.name,
30
+ description: tool.description,
31
+ inputSchema: deepClone(defs[tool.inputSchemaDef]),
32
+ outputSchema: deepClone(defs[tool.outputSchemaDef]),
33
+ pipeAction: tool.pipeAction,
34
+ }));
35
+ }
@@ -6,6 +6,10 @@
6
6
  // SKILL.md가 인라인 프롬프트를 사용하므로, 이 모듈은 CLI(tfx multi --native)에서
7
7
  // 팀 설정을 프로그래밍적으로 생성할 때 사용한다.
8
8
 
9
+ import * as fs from "node:fs/promises";
10
+ import os from "node:os";
11
+ import path from "node:path";
12
+
9
13
  const ROUTE_SCRIPT = "~/.claude/scripts/tfx-route.sh";
10
14
  export const SLIM_WRAPPER_SUBAGENT_TYPE = "slim-wrapper";
11
15
  const ROUTE_LOG_RE = /\[tfx-route\]/i;
@@ -255,6 +259,103 @@ gemini/codex를 직접 호출하지 마라. psmux spawn이 tfx-route.sh를 통
255
259
  실패 여부는 metadata.result로 구분. pane 실패 시에도 반드시 TaskUpdate + SendMessage 후 종료.`;
256
260
  }
257
261
 
262
+ /**
263
+ * tfx-route.sh가 남긴 로컬 결과 파일을 폴링해서 완료/대기 태스크를 분리한다.
264
+ * SendMessage 전달 지연이 있더라도 Phase 4에서 파일 기반으로 완료를 빠르게 감지하기 위한 보조 경로다.
265
+ *
266
+ * @param {string} teamName
267
+ * @param {string[]} expectedTaskIds
268
+ * @returns {Promise<{completed:Array<{taskId:string,result:string,summary:string}>, pending:string[]}>}
269
+ */
270
+ export async function pollTeamResults(teamName, expectedTaskIds = []) {
271
+ const normalizedTaskIds = Array.from(
272
+ new Set(
273
+ (Array.isArray(expectedTaskIds) ? expectedTaskIds : [])
274
+ .map((taskId) => String(taskId || "").trim())
275
+ .filter(Boolean),
276
+ ),
277
+ );
278
+
279
+ if (!normalizedTaskIds.length) {
280
+ return { completed: [], pending: [] };
281
+ }
282
+
283
+ const normalizedTeamName = String(teamName || "").trim();
284
+ if (!normalizedTeamName) {
285
+ return { completed: [], pending: normalizedTaskIds };
286
+ }
287
+
288
+ const resultDir = path.join(os.homedir(), ".claude", "tfx-results", normalizedTeamName);
289
+
290
+ let entries;
291
+ try {
292
+ entries = await fs.readdir(resultDir, { withFileTypes: true });
293
+ } catch (error) {
294
+ if (error && error.code === "ENOENT") {
295
+ return { completed: [], pending: normalizedTaskIds };
296
+ }
297
+ throw error;
298
+ }
299
+
300
+ const availableFiles = new Set(
301
+ entries
302
+ .filter((entry) => entry.isFile())
303
+ .map((entry) => entry.name),
304
+ );
305
+
306
+ const completedCandidates = await Promise.all(
307
+ normalizedTaskIds.map(async (taskId) => {
308
+ const fileName = `${taskId}.json`;
309
+ if (!availableFiles.has(fileName)) return null;
310
+
311
+ try {
312
+ const raw = await fs.readFile(path.join(resultDir, fileName), "utf8");
313
+ const parsed = JSON.parse(raw);
314
+ return {
315
+ taskId,
316
+ result: typeof parsed?.result === "string" ? parsed.result : "failed",
317
+ summary: typeof parsed?.summary === "string" ? parsed.summary : "",
318
+ };
319
+ } catch (error) {
320
+ if (error && error.code === "ENOENT") return null;
321
+ return {
322
+ taskId,
323
+ result: "failed",
324
+ summary: "결과 파일 파싱 실패",
325
+ };
326
+ }
327
+ }),
328
+ );
329
+
330
+ const completed = completedCandidates.filter(Boolean);
331
+ const completedTaskIds = new Set(completed.map((item) => item.taskId));
332
+ const pending = normalizedTaskIds.filter((taskId) => !completedTaskIds.has(taskId));
333
+
334
+ return { completed, pending };
335
+ }
336
+
337
+ /**
338
+ * 폴링 결과를 진행률 한 줄 요약으로 바꾼다.
339
+ *
340
+ * @param {{completed?:Array<{taskId:string,result:string}>, pending?:string[]}} pollResult
341
+ * @returns {string}
342
+ */
343
+ export function formatPollReport(pollResult = {}) {
344
+ const completed = Array.isArray(pollResult.completed) ? pollResult.completed : [];
345
+ const pending = Array.isArray(pollResult.pending) ? pollResult.pending : [];
346
+ const total = completed.length + pending.length;
347
+
348
+ if (total === 0) return "0/0 완료";
349
+
350
+ const detail = completed
351
+ .map(({ taskId, result }) => `${taskId} ${result || "unknown"}`)
352
+ .join(", ");
353
+
354
+ return detail
355
+ ? `${completed.length}/${total} 완료 (${detail})`
356
+ : `${completed.length}/${total} 완료`;
357
+ }
358
+
258
359
  /**
259
360
  * 팀 이름 생성 (타임스탬프 기반)
260
361
  * @returns {string}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "3.3.0-dev.7",
3
+ "version": "3.3.0-dev.8",
4
4
  "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
5
  "type": "module",
6
6
  "bin": {