triflux 3.3.0-dev.3 → 3.3.0-dev.5
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/assign-callbacks.mjs +136 -0
- package/hub/bridge.mjs +283 -97
- package/hub/pipe.mjs +81 -0
- package/hub/server.mjs +56 -53
- package/hub/store.mjs +36 -2
- package/hub/team/cli-team-status.mjs +17 -3
- package/hub/team/native-supervisor.mjs +62 -22
- package/hub/team/native.mjs +266 -200
- package/hub/workers/delegator-mcp.mjs +285 -140
- package/package.json +60 -60
- package/scripts/lib/mcp-filter.mjs +637 -0
- package/scripts/lib/mcp-server-catalog.mjs +118 -0
- package/scripts/mcp-check.mjs +126 -88
- package/scripts/test-tfx-route-no-claude-native.mjs +10 -2
- package/scripts/tfx-route.sh +434 -179
package/hub/pipe.mjs
CHANGED
|
@@ -5,6 +5,19 @@ import net from 'node:net';
|
|
|
5
5
|
import { existsSync, unlinkSync } from 'node:fs';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { randomUUID } from 'node:crypto';
|
|
8
|
+
import {
|
|
9
|
+
teamInfo,
|
|
10
|
+
teamTaskList,
|
|
11
|
+
teamTaskUpdate,
|
|
12
|
+
teamSendMessage,
|
|
13
|
+
} from './team/nativeProxy.mjs';
|
|
14
|
+
import { createPipeline } from './pipeline/index.mjs';
|
|
15
|
+
import {
|
|
16
|
+
ensurePipelineTable,
|
|
17
|
+
initPipelineState,
|
|
18
|
+
listPipelineStates,
|
|
19
|
+
readPipelineState,
|
|
20
|
+
} from './pipeline/state.mjs';
|
|
8
21
|
|
|
9
22
|
const DEFAULT_HEARTBEAT_TTL_MS = 60000;
|
|
10
23
|
|
|
@@ -236,6 +249,41 @@ export function createPipeServer({
|
|
|
236
249
|
};
|
|
237
250
|
}
|
|
238
251
|
|
|
252
|
+
case 'team_task_update': {
|
|
253
|
+
const result = await teamTaskUpdate(payload);
|
|
254
|
+
if (client) touchClient(client);
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
case 'team_send_message': {
|
|
259
|
+
const result = await teamSendMessage(payload);
|
|
260
|
+
if (client) touchClient(client);
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
case 'pipeline_advance': {
|
|
265
|
+
if (client) touchClient(client);
|
|
266
|
+
if (!store?.db) {
|
|
267
|
+
return { ok: false, error: 'hub_db_not_found' };
|
|
268
|
+
}
|
|
269
|
+
ensurePipelineTable(store.db);
|
|
270
|
+
const pipeline = createPipeline(store.db, payload.team_name);
|
|
271
|
+
return pipeline.advance(payload.phase);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
case 'pipeline_init': {
|
|
275
|
+
if (client) touchClient(client);
|
|
276
|
+
if (!store?.db) {
|
|
277
|
+
return { ok: false, error: 'hub_db_not_found' };
|
|
278
|
+
}
|
|
279
|
+
ensurePipelineTable(store.db);
|
|
280
|
+
const state = initPipelineState(store.db, payload.team_name, {
|
|
281
|
+
fix_max: payload.fix_max,
|
|
282
|
+
ralph_max: payload.ralph_max,
|
|
283
|
+
});
|
|
284
|
+
return { ok: true, data: state };
|
|
285
|
+
}
|
|
286
|
+
|
|
239
287
|
default:
|
|
240
288
|
return {
|
|
241
289
|
ok: false,
|
|
@@ -305,6 +353,39 @@ export function createPipeServer({
|
|
|
305
353
|
return router.getAssignStatus(payload);
|
|
306
354
|
}
|
|
307
355
|
|
|
356
|
+
case 'team_info': {
|
|
357
|
+
const result = await teamInfo(payload);
|
|
358
|
+
if (client) touchClient(client);
|
|
359
|
+
return result;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
case 'team_task_list': {
|
|
363
|
+
const result = await teamTaskList(payload);
|
|
364
|
+
if (client) touchClient(client);
|
|
365
|
+
return result;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
case 'pipeline_state': {
|
|
369
|
+
if (client) touchClient(client);
|
|
370
|
+
if (!store?.db) {
|
|
371
|
+
return { ok: false, error: 'hub_db_not_found' };
|
|
372
|
+
}
|
|
373
|
+
ensurePipelineTable(store.db);
|
|
374
|
+
const state = readPipelineState(store.db, payload.team_name);
|
|
375
|
+
return state
|
|
376
|
+
? { ok: true, data: state }
|
|
377
|
+
: { ok: false, error: 'pipeline_not_found' };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
case 'pipeline_list': {
|
|
381
|
+
if (client) touchClient(client);
|
|
382
|
+
if (!store?.db) {
|
|
383
|
+
return { ok: false, error: 'hub_db_not_found' };
|
|
384
|
+
}
|
|
385
|
+
ensurePipelineTable(store.db);
|
|
386
|
+
return { ok: true, data: listPipelineStates(store.db) };
|
|
387
|
+
}
|
|
388
|
+
|
|
308
389
|
default:
|
|
309
390
|
return {
|
|
310
391
|
ok: false,
|
package/hub/server.mjs
CHANGED
|
@@ -13,22 +13,8 @@ import { createStore } from './store.mjs';
|
|
|
13
13
|
import { createRouter } from './router.mjs';
|
|
14
14
|
import { createHitlManager } from './hitl.mjs';
|
|
15
15
|
import { createPipeServer } from './pipe.mjs';
|
|
16
|
+
import { createAssignCallbackServer } from './assign-callbacks.mjs';
|
|
16
17
|
import { createTools } from './tools.mjs';
|
|
17
|
-
import {
|
|
18
|
-
ensurePipelineTable,
|
|
19
|
-
createPipeline,
|
|
20
|
-
} from './pipeline/index.mjs';
|
|
21
|
-
import {
|
|
22
|
-
readPipelineState,
|
|
23
|
-
initPipelineState,
|
|
24
|
-
listPipelineStates,
|
|
25
|
-
} from './pipeline/state.mjs';
|
|
26
|
-
import {
|
|
27
|
-
teamInfo,
|
|
28
|
-
teamTaskList,
|
|
29
|
-
teamTaskUpdate,
|
|
30
|
-
teamSendMessage,
|
|
31
|
-
} from './team/nativeProxy.mjs';
|
|
32
18
|
|
|
33
19
|
function isInitializeRequest(body) {
|
|
34
20
|
if (body?.method === 'initialize') return true;
|
|
@@ -61,6 +47,22 @@ function isAllowedOrigin(origin) {
|
|
|
61
47
|
return origin && ALLOWED_ORIGIN_RE.test(origin);
|
|
62
48
|
}
|
|
63
49
|
|
|
50
|
+
function resolveTeamStatusCode(result) {
|
|
51
|
+
if (result?.ok) return 200;
|
|
52
|
+
const code = result?.error?.code;
|
|
53
|
+
if (code === 'TEAM_NOT_FOUND' || code === 'TASK_NOT_FOUND' || code === 'TASKS_DIR_NOT_FOUND') return 404;
|
|
54
|
+
if (code === 'CLAIM_CONFLICT' || code === 'MTIME_CONFLICT') return 409;
|
|
55
|
+
if (code === 'INVALID_TEAM_NAME' || code === 'INVALID_TASK_ID' || code === 'INVALID_TEXT' || code === 'INVALID_FROM' || code === 'INVALID_STATUS') return 400;
|
|
56
|
+
return 500;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resolvePipelineStatusCode(result) {
|
|
60
|
+
if (result?.ok) return 200;
|
|
61
|
+
if (result?.error === 'pipeline_not_found') return 404;
|
|
62
|
+
if (result?.error === 'hub_db_not_found') return 503;
|
|
63
|
+
return 400;
|
|
64
|
+
}
|
|
65
|
+
|
|
64
66
|
/**
|
|
65
67
|
* tfx-hub 시작
|
|
66
68
|
* @param {object} opts
|
|
@@ -82,6 +84,7 @@ export async function startHub({ port = 27888, dbPath, host = '127.0.0.1', sessi
|
|
|
82
84
|
const store = createStore(dbPath);
|
|
83
85
|
const router = createRouter(store);
|
|
84
86
|
const pipe = createPipeServer({ router, store, sessionId });
|
|
87
|
+
const assignCallbacks = createAssignCallbackServer({ store, sessionId });
|
|
85
88
|
const hitl = createHitlManager(store, router);
|
|
86
89
|
const tools = createTools(store, router, hitl, pipe);
|
|
87
90
|
const transports = new Map();
|
|
@@ -145,6 +148,8 @@ export async function startHub({ port = 27888, dbPath, host = '127.0.0.1', sessi
|
|
|
145
148
|
port,
|
|
146
149
|
pipe_path: pipe.path,
|
|
147
150
|
pipe: pipe.getStatus(),
|
|
151
|
+
assign_callback_pipe_path: assignCallbacks.path,
|
|
152
|
+
assign_callback_pipe: assignCallbacks.getStatus(),
|
|
148
153
|
}));
|
|
149
154
|
}
|
|
150
155
|
|
|
@@ -343,25 +348,17 @@ export async function startHub({ port = 27888, dbPath, host = '127.0.0.1', sessi
|
|
|
343
348
|
if (req.method === 'POST') {
|
|
344
349
|
let teamResult = null;
|
|
345
350
|
if (path === '/bridge/team/info' || path === '/bridge/team-info') {
|
|
346
|
-
teamResult = await
|
|
351
|
+
teamResult = await pipe.executeQuery('team_info', body);
|
|
347
352
|
} else if (path === '/bridge/team/task-list' || path === '/bridge/team-task-list') {
|
|
348
|
-
teamResult = await
|
|
353
|
+
teamResult = await pipe.executeQuery('team_task_list', body);
|
|
349
354
|
} else if (path === '/bridge/team/task-update' || path === '/bridge/team-task-update') {
|
|
350
|
-
teamResult = await
|
|
355
|
+
teamResult = await pipe.executeCommand('team_task_update', body);
|
|
351
356
|
} else if (path === '/bridge/team/send-message' || path === '/bridge/team-send-message') {
|
|
352
|
-
teamResult = await
|
|
357
|
+
teamResult = await pipe.executeCommand('team_send_message', body);
|
|
353
358
|
}
|
|
354
359
|
|
|
355
360
|
if (teamResult) {
|
|
356
|
-
|
|
357
|
-
const code = teamResult?.error?.code;
|
|
358
|
-
if (!teamResult.ok) {
|
|
359
|
-
if (code === 'TEAM_NOT_FOUND' || code === 'TASK_NOT_FOUND' || code === 'TASKS_DIR_NOT_FOUND') status = 404;
|
|
360
|
-
else if (code === 'CLAIM_CONFLICT' || code === 'MTIME_CONFLICT') status = 409;
|
|
361
|
-
else if (code === 'INVALID_TEAM_NAME' || code === 'INVALID_TASK_ID' || code === 'INVALID_TEXT' || code === 'INVALID_FROM' || code === 'INVALID_STATUS') status = 400;
|
|
362
|
-
else status = 500;
|
|
363
|
-
}
|
|
364
|
-
res.writeHead(status);
|
|
361
|
+
res.writeHead(resolveTeamStatusCode(teamResult));
|
|
365
362
|
return res.end(JSON.stringify(teamResult));
|
|
366
363
|
}
|
|
367
364
|
|
|
@@ -372,51 +369,42 @@ export async function startHub({ port = 27888, dbPath, host = '127.0.0.1', sessi
|
|
|
372
369
|
|
|
373
370
|
// ── 파이프라인 엔드포인트 ──
|
|
374
371
|
if (path === '/bridge/pipeline/state' && req.method === 'POST') {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
res.writeHead(state ? 200 : 404);
|
|
379
|
-
return res.end(JSON.stringify(state
|
|
380
|
-
? { ok: true, data: state }
|
|
381
|
-
: { ok: false, error: 'pipeline_not_found' }));
|
|
372
|
+
const result = await pipe.executeQuery('pipeline_state', body);
|
|
373
|
+
res.writeHead(resolvePipelineStatusCode(result));
|
|
374
|
+
return res.end(JSON.stringify(result));
|
|
382
375
|
}
|
|
383
376
|
|
|
384
377
|
if (path === '/bridge/pipeline/advance' && req.method === 'POST') {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
const pipeline = createPipeline(store.db, team_name);
|
|
388
|
-
const result = pipeline.advance(phase);
|
|
389
|
-
res.writeHead(result.ok ? 200 : 400);
|
|
378
|
+
const result = await pipe.executeCommand('pipeline_advance', body);
|
|
379
|
+
res.writeHead(resolvePipelineStatusCode(result));
|
|
390
380
|
return res.end(JSON.stringify(result));
|
|
391
381
|
}
|
|
392
382
|
|
|
393
383
|
if (path === '/bridge/pipeline/init' && req.method === 'POST') {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
res.writeHead(200);
|
|
398
|
-
return res.end(JSON.stringify({ ok: true, data: state }));
|
|
384
|
+
const result = await pipe.executeCommand('pipeline_init', body);
|
|
385
|
+
res.writeHead(resolvePipelineStatusCode(result));
|
|
386
|
+
return res.end(JSON.stringify(result));
|
|
399
387
|
}
|
|
400
388
|
|
|
401
389
|
if (path === '/bridge/pipeline/list' && req.method === 'POST') {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
res.
|
|
405
|
-
return res.end(JSON.stringify({ ok: true, data: states }));
|
|
390
|
+
const result = await pipe.executeQuery('pipeline_list', body);
|
|
391
|
+
res.writeHead(resolvePipelineStatusCode(result));
|
|
392
|
+
return res.end(JSON.stringify(result));
|
|
406
393
|
}
|
|
407
394
|
}
|
|
408
395
|
|
|
409
396
|
if (path === '/bridge/context' && req.method === 'POST') {
|
|
410
|
-
const { agent_id, topics, max_messages = 10 } = body;
|
|
397
|
+
const { agent_id, topics, max_messages = 10, auto_ack = true } = body;
|
|
411
398
|
if (!agent_id) {
|
|
412
399
|
res.writeHead(400);
|
|
413
400
|
return res.end(JSON.stringify({ ok: false, error: 'agent_id 필수' }));
|
|
414
401
|
}
|
|
415
402
|
|
|
416
|
-
const result = await pipe.executeQuery('
|
|
403
|
+
const result = await pipe.executeQuery('drain', {
|
|
417
404
|
agent_id,
|
|
418
405
|
topics,
|
|
419
406
|
max_messages,
|
|
407
|
+
auto_ack,
|
|
420
408
|
});
|
|
421
409
|
res.writeHead(200);
|
|
422
410
|
return res.end(JSON.stringify(result));
|
|
@@ -536,6 +524,7 @@ export async function startHub({ port = 27888, dbPath, host = '127.0.0.1', sessi
|
|
|
536
524
|
|
|
537
525
|
mkdirSync(PID_DIR, { recursive: true });
|
|
538
526
|
await pipe.start();
|
|
527
|
+
await assignCallbacks.start();
|
|
539
528
|
|
|
540
529
|
return new Promise((resolve, reject) => {
|
|
541
530
|
httpServer.listen(port, host, () => {
|
|
@@ -544,9 +533,12 @@ export async function startHub({ port = 27888, dbPath, host = '127.0.0.1', sessi
|
|
|
544
533
|
host,
|
|
545
534
|
dbPath,
|
|
546
535
|
pid: process.pid,
|
|
536
|
+
hubToken: HUB_TOKEN,
|
|
547
537
|
url: `http://${host}:${port}/mcp`,
|
|
548
538
|
pipe_path: pipe.path,
|
|
549
539
|
pipePath: pipe.path,
|
|
540
|
+
assign_callback_pipe_path: assignCallbacks.path,
|
|
541
|
+
assignCallbackPipePath: assignCallbacks.path,
|
|
550
542
|
};
|
|
551
543
|
|
|
552
544
|
writeFileSync(PID_FILE, JSON.stringify({
|
|
@@ -556,10 +548,11 @@ export async function startHub({ port = 27888, dbPath, host = '127.0.0.1', sessi
|
|
|
556
548
|
url: info.url,
|
|
557
549
|
pipe_path: pipe.path,
|
|
558
550
|
pipePath: pipe.path,
|
|
551
|
+
assign_callback_pipe_path: assignCallbacks.path,
|
|
559
552
|
started: Date.now(),
|
|
560
553
|
}));
|
|
561
554
|
|
|
562
|
-
console.log(`[tfx-hub] MCP 서버 시작: ${info.url} / pipe ${pipe.path} (PID ${process.pid})`);
|
|
555
|
+
console.log(`[tfx-hub] MCP 서버 시작: ${info.url} / pipe ${pipe.path} / assign-callback ${assignCallbacks.path} (PID ${process.pid})`);
|
|
563
556
|
|
|
564
557
|
const stopFn = async () => {
|
|
565
558
|
router.stopSweeper();
|
|
@@ -570,13 +563,23 @@ export async function startHub({ port = 27888, dbPath, host = '127.0.0.1', sessi
|
|
|
570
563
|
}
|
|
571
564
|
transports.clear();
|
|
572
565
|
await pipe.stop();
|
|
566
|
+
await assignCallbacks.stop();
|
|
573
567
|
store.close();
|
|
574
568
|
try { unlinkSync(PID_FILE); } catch {}
|
|
575
569
|
try { unlinkSync(TOKEN_FILE); } catch {}
|
|
576
570
|
await new Promise((resolveClose) => httpServer.close(resolveClose));
|
|
577
571
|
};
|
|
578
572
|
|
|
579
|
-
resolve({
|
|
573
|
+
resolve({
|
|
574
|
+
...info,
|
|
575
|
+
httpServer,
|
|
576
|
+
store,
|
|
577
|
+
router,
|
|
578
|
+
hitl,
|
|
579
|
+
pipe,
|
|
580
|
+
assignCallbacks,
|
|
581
|
+
stop: stopFn,
|
|
582
|
+
});
|
|
580
583
|
});
|
|
581
584
|
httpServer.on('error', reject);
|
|
582
585
|
});
|
package/hub/store.mjs
CHANGED
|
@@ -223,6 +223,24 @@ export function createStore(dbPath) {
|
|
|
223
223
|
activeAssignCount: db.prepare("SELECT COUNT(*) as cnt FROM assign_jobs WHERE status IN ('queued','running')"),
|
|
224
224
|
};
|
|
225
225
|
|
|
226
|
+
const assignStatusListeners = new Set();
|
|
227
|
+
|
|
228
|
+
function buildAssignCallbackEvent(row) {
|
|
229
|
+
return {
|
|
230
|
+
job_id: row.job_id,
|
|
231
|
+
status: row.status,
|
|
232
|
+
result: row.result ?? row.error ?? null,
|
|
233
|
+
timestamp: new Date(row.updated_at_ms || Date.now()).toISOString(),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function notifyAssignStatusListeners(row) {
|
|
238
|
+
const event = buildAssignCallbackEvent(row);
|
|
239
|
+
for (const listener of Array.from(assignStatusListeners)) {
|
|
240
|
+
try { listener(event, row); } catch {}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
226
244
|
function clampMaxMessages(value, fallback = 20) {
|
|
227
245
|
const num = Number(value);
|
|
228
246
|
if (!Number.isFinite(num)) return fallback;
|
|
@@ -481,7 +499,9 @@ export function createStore(dbPath) {
|
|
|
481
499
|
last_retry_at_ms: retry_count > 0 ? now : null,
|
|
482
500
|
};
|
|
483
501
|
S.insertAssign.run(row);
|
|
484
|
-
|
|
502
|
+
const inserted = store.getAssign(row.job_id);
|
|
503
|
+
notifyAssignStatusListeners(inserted);
|
|
504
|
+
return inserted;
|
|
485
505
|
},
|
|
486
506
|
|
|
487
507
|
getAssign(jobId) {
|
|
@@ -541,7 +561,11 @@ export function createStore(dbPath) {
|
|
|
541
561
|
: current.last_retry_at_ms,
|
|
542
562
|
};
|
|
543
563
|
S.updateAssign.run(nextRow);
|
|
544
|
-
|
|
564
|
+
const updated = store.getAssign(jobId);
|
|
565
|
+
if (updated && current.status !== updated.status) {
|
|
566
|
+
notifyAssignStatusListeners(updated);
|
|
567
|
+
}
|
|
568
|
+
return updated;
|
|
545
569
|
},
|
|
546
570
|
|
|
547
571
|
listAssigns({
|
|
@@ -639,6 +663,16 @@ export function createStore(dbPath) {
|
|
|
639
663
|
};
|
|
640
664
|
},
|
|
641
665
|
|
|
666
|
+
onAssignStatusChange(listener) {
|
|
667
|
+
if (typeof listener !== 'function') {
|
|
668
|
+
return () => {};
|
|
669
|
+
}
|
|
670
|
+
assignStatusListeners.add(listener);
|
|
671
|
+
return () => {
|
|
672
|
+
assignStatusListeners.delete(listener);
|
|
673
|
+
};
|
|
674
|
+
},
|
|
675
|
+
|
|
642
676
|
getDeliveryStats() {
|
|
643
677
|
return {
|
|
644
678
|
total_deliveries: S.ackedRecent.get(Date.now()).cnt,
|
|
@@ -54,6 +54,21 @@ function renderTasks(tasks = []) {
|
|
|
54
54
|
console.log("");
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
function formatCompletionSuffix(member) {
|
|
58
|
+
if (!member?.completionStatus) return "";
|
|
59
|
+
if (member.completionStatus === "abnormal") {
|
|
60
|
+
const reason = member.completionReason || "unknown";
|
|
61
|
+
return ` ${RED}[abnormal:${reason}]${RESET}`;
|
|
62
|
+
}
|
|
63
|
+
if (member.completionStatus === "normal") {
|
|
64
|
+
return ` ${GREEN}[route-ok]${RESET}`;
|
|
65
|
+
}
|
|
66
|
+
if (member.completionStatus === "unchecked") {
|
|
67
|
+
return ` ${GRAY}[route-unchecked]${RESET}`;
|
|
68
|
+
}
|
|
69
|
+
return "";
|
|
70
|
+
}
|
|
71
|
+
|
|
57
72
|
export async function teamStatus() {
|
|
58
73
|
const state = loadTeamState();
|
|
59
74
|
if (!state) {
|
|
@@ -91,7 +106,7 @@ export async function teamStatus() {
|
|
|
91
106
|
if (nativeMembers.length) {
|
|
92
107
|
console.log("");
|
|
93
108
|
for (const m of nativeMembers) {
|
|
94
|
-
console.log(` • ${m.name}: ${m.status}${m.lastPreview ? ` ${DIM}${m.lastPreview}${RESET}` : ""}`);
|
|
109
|
+
console.log(` • ${m.name}: ${m.status}${formatCompletionSuffix(m)}${m.lastPreview ? ` ${DIM}${m.lastPreview}${RESET}` : ""}`);
|
|
95
110
|
}
|
|
96
111
|
}
|
|
97
112
|
}
|
|
@@ -216,7 +231,7 @@ export async function teamDebug() {
|
|
|
216
231
|
console.log(` ${DIM}(no data)${RESET}`);
|
|
217
232
|
} else {
|
|
218
233
|
for (const m of members) {
|
|
219
|
-
console.log(` - ${m.name}: ${m.status}${m.lastPreview ? ` ${DIM}${m.lastPreview}${RESET}` : ""}`);
|
|
234
|
+
console.log(` - ${m.name}: ${m.status}${formatCompletionSuffix(m)}${m.lastPreview ? ` ${DIM}${m.lastPreview}${RESET}` : ""}`);
|
|
220
235
|
}
|
|
221
236
|
}
|
|
222
237
|
console.log("");
|
|
@@ -266,4 +281,3 @@ export function teamList() {
|
|
|
266
281
|
}
|
|
267
282
|
console.log("");
|
|
268
283
|
}
|
|
269
|
-
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// hub/team/native-supervisor.mjs — tmux 없이 멀티 CLI를 직접 띄우는 네이티브 팀 런타임
|
|
2
|
-
import { createServer } from "node:http";
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
|
-
import { mkdirSync, readFileSync, writeFileSync, createWriteStream } from "node:fs";
|
|
5
|
-
import { dirname, join } from "node:path";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { mkdirSync, readFileSync, writeFileSync, createWriteStream } from "node:fs";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { verifySlimWrapperRouteExecution } from "./native.mjs";
|
|
7
|
+
|
|
8
|
+
const ROUTE_LOG_TAIL_BYTES = 65536;
|
|
6
9
|
|
|
7
10
|
function parseArgs(argv) {
|
|
8
11
|
const out = {};
|
|
@@ -19,10 +22,43 @@ async function readJson(path) {
|
|
|
19
22
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
function safeText(v, fallback = "") {
|
|
23
|
-
if (v == null) return fallback;
|
|
24
|
-
return String(v);
|
|
25
|
-
}
|
|
25
|
+
function safeText(v, fallback = "") {
|
|
26
|
+
if (v == null) return fallback;
|
|
27
|
+
return String(v);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readTailText(path, maxBytes = ROUTE_LOG_TAIL_BYTES) {
|
|
31
|
+
try {
|
|
32
|
+
const raw = readFileSync(path, "utf8");
|
|
33
|
+
if (raw.length <= maxBytes) return raw;
|
|
34
|
+
return raw.slice(-maxBytes);
|
|
35
|
+
} catch {
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function finalizeRouteVerification(state) {
|
|
41
|
+
if (state?.member?.role !== "worker") return;
|
|
42
|
+
|
|
43
|
+
const verification = verifySlimWrapperRouteExecution({
|
|
44
|
+
promptText: safeText(state.member?.prompt),
|
|
45
|
+
stdoutText: readTailText(state.logFile),
|
|
46
|
+
stderrText: readTailText(state.errFile),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
state.routeVerification = verification;
|
|
50
|
+
if (!verification.expectedRouteInvocation) {
|
|
51
|
+
state.completionStatus = "unchecked";
|
|
52
|
+
state.completionReason = null;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
state.completionStatus = verification.abnormal ? "abnormal" : "normal";
|
|
57
|
+
state.completionReason = verification.reason;
|
|
58
|
+
if (verification.abnormal) {
|
|
59
|
+
state.lastPreview = "[abnormal] tfx-route.sh evidence missing";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
26
62
|
|
|
27
63
|
function nowMs() {
|
|
28
64
|
return Date.now();
|
|
@@ -60,13 +96,16 @@ function memberStateSnapshot() {
|
|
|
60
96
|
agentId: m.agentId,
|
|
61
97
|
command: m.command,
|
|
62
98
|
pid: state?.child?.pid || null,
|
|
63
|
-
status: state?.status || "unknown",
|
|
64
|
-
exitCode: state?.exitCode ?? null,
|
|
65
|
-
lastPreview: state?.lastPreview || "",
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
99
|
+
status: state?.status || "unknown",
|
|
100
|
+
exitCode: state?.exitCode ?? null,
|
|
101
|
+
lastPreview: state?.lastPreview || "",
|
|
102
|
+
completionStatus: state?.completionStatus || null,
|
|
103
|
+
completionReason: state?.completionReason || null,
|
|
104
|
+
routeVerification: state?.routeVerification || null,
|
|
105
|
+
logFile: state?.logFile || null,
|
|
106
|
+
errFile: state?.errFile || null,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
70
109
|
return states;
|
|
71
110
|
}
|
|
72
111
|
|
|
@@ -128,13 +167,14 @@ function spawnMember(member) {
|
|
|
128
167
|
}
|
|
129
168
|
});
|
|
130
169
|
|
|
131
|
-
child.on("exit", (code) => {
|
|
132
|
-
state.status = "exited";
|
|
133
|
-
state.exitCode = code;
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
|
|
137
|
-
|
|
170
|
+
child.on("exit", (code) => {
|
|
171
|
+
state.status = "exited";
|
|
172
|
+
state.exitCode = code;
|
|
173
|
+
finalizeRouteVerification(state);
|
|
174
|
+
try { outWs.end(); } catch {}
|
|
175
|
+
try { errWs.end(); } catch {}
|
|
176
|
+
maybeAutoShutdown();
|
|
177
|
+
});
|
|
138
178
|
|
|
139
179
|
processMap.set(member.name, state);
|
|
140
180
|
}
|