triflux 3.2.0-dev.8 → 3.3.0-dev.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.
- package/bin/triflux.mjs +1296 -1055
- package/hooks/hooks.json +17 -0
- package/hooks/keyword-rules.json +20 -4
- package/hooks/pipeline-stop.mjs +54 -0
- package/hub/bridge.mjs +517 -318
- package/hub/hitl.mjs +45 -31
- package/hub/pipe.mjs +457 -0
- package/hub/pipeline/index.mjs +121 -0
- package/hub/pipeline/state.mjs +164 -0
- package/hub/pipeline/transitions.mjs +114 -0
- package/hub/router.mjs +422 -161
- package/hub/schema.sql +14 -0
- package/hub/server.mjs +499 -424
- package/hub/store.mjs +388 -314
- package/hub/team/cli-team-common.mjs +348 -0
- package/hub/team/cli-team-control.mjs +393 -0
- package/hub/team/cli-team-start.mjs +516 -0
- package/hub/team/cli-team-status.mjs +269 -0
- package/hub/team/cli.mjs +75 -1475
- package/hub/team/dashboard.mjs +1 -9
- package/hub/team/native.mjs +190 -130
- package/hub/team/nativeProxy.mjs +165 -78
- package/hub/team/orchestrator.mjs +15 -20
- package/hub/team/pane.mjs +137 -103
- package/hub/team/psmux.mjs +506 -0
- package/hub/team/session.mjs +393 -330
- package/hub/team/shared.mjs +13 -0
- package/hub/team/staleState.mjs +299 -0
- package/hub/tools.mjs +105 -31
- package/hub/workers/claude-worker.mjs +446 -0
- package/hub/workers/codex-mcp.mjs +414 -0
- package/hub/workers/factory.mjs +18 -0
- package/hub/workers/gemini-worker.mjs +349 -0
- package/hub/workers/interface.mjs +41 -0
- package/hud/hud-qos-status.mjs +1790 -1788
- package/package.json +4 -1
- package/scripts/__tests__/keyword-detector.test.mjs +8 -8
- package/scripts/keyword-detector.mjs +15 -0
- package/scripts/lib/keyword-rules.mjs +4 -1
- package/scripts/preflight-cache.mjs +72 -0
- package/scripts/psmux-steering-prototype.sh +368 -0
- package/scripts/setup.mjs +136 -71
- package/scripts/tfx-route-worker.mjs +161 -0
- package/scripts/tfx-route.sh +485 -91
- package/skills/tfx-auto/SKILL.md +90 -564
- package/skills/tfx-auto-codex/SKILL.md +1 -3
- package/skills/tfx-codex/SKILL.md +1 -4
- package/skills/tfx-doctor/SKILL.md +1 -0
- package/skills/tfx-gemini/SKILL.md +1 -4
- package/skills/tfx-multi/SKILL.md +378 -0
- package/skills/tfx-setup/SKILL.md +1 -4
- package/skills/tfx-team/SKILL.md +0 -304
package/hub/team/nativeProxy.mjs
CHANGED
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
import {
|
|
5
5
|
existsSync,
|
|
6
6
|
mkdirSync,
|
|
7
|
-
readdirSync,
|
|
8
|
-
readFileSync,
|
|
9
7
|
renameSync,
|
|
10
8
|
statSync,
|
|
11
9
|
unlinkSync,
|
|
@@ -13,6 +11,7 @@ import {
|
|
|
13
11
|
openSync,
|
|
14
12
|
closeSync,
|
|
15
13
|
} from 'node:fs';
|
|
14
|
+
import { readdir, stat, readFile } from 'node:fs/promises';
|
|
16
15
|
import { basename, dirname, join } from 'node:path';
|
|
17
16
|
import { homedir } from 'node:os';
|
|
18
17
|
import { randomUUID } from 'node:crypto';
|
|
@@ -22,6 +21,15 @@ const CLAUDE_HOME = join(homedir(), '.claude');
|
|
|
22
21
|
const TEAMS_ROOT = join(CLAUDE_HOME, 'teams');
|
|
23
22
|
const TASKS_ROOT = join(CLAUDE_HOME, 'tasks');
|
|
24
23
|
|
|
24
|
+
// ── 인메모리 캐시 (디렉토리 mtime 기반 무효화) ──
|
|
25
|
+
const _dirCache = new Map(); // tasksDir → { mtimeMs, files: string[] }
|
|
26
|
+
const _taskIdIndex = new Map(); // taskId → filePath
|
|
27
|
+
const _taskContentCache = new Map(); // filePath → { mtimeMs, data }
|
|
28
|
+
|
|
29
|
+
function _invalidateCache(tasksDir) {
|
|
30
|
+
_dirCache.delete(tasksDir);
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
function err(code, message, extra = {}) {
|
|
26
34
|
return { ok: false, error: { code, message, ...extra } };
|
|
27
35
|
}
|
|
@@ -32,9 +40,9 @@ function validateTeamName(teamName) {
|
|
|
32
40
|
}
|
|
33
41
|
}
|
|
34
42
|
|
|
35
|
-
function readJsonSafe(path) {
|
|
43
|
+
async function readJsonSafe(path) {
|
|
36
44
|
try {
|
|
37
|
-
return JSON.parse(
|
|
45
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
38
46
|
} catch {
|
|
39
47
|
return null;
|
|
40
48
|
}
|
|
@@ -44,15 +52,25 @@ function atomicWriteJson(path, value) {
|
|
|
44
52
|
mkdirSync(dirname(path), { recursive: true });
|
|
45
53
|
const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
46
54
|
writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
47
|
-
|
|
55
|
+
try {
|
|
56
|
+
renameSync(tmp, path);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
// Windows NTFS: 대상 파일 존재 시 rename 실패 가능 → 삭제 후 재시도
|
|
59
|
+
if (process.platform === 'win32' && (e.code === 'EPERM' || e.code === 'EEXIST')) {
|
|
60
|
+
try { unlinkSync(path); } catch {}
|
|
61
|
+
renameSync(tmp, path);
|
|
62
|
+
} else {
|
|
63
|
+
try { unlinkSync(tmp); } catch {}
|
|
64
|
+
throw e;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
48
67
|
}
|
|
49
68
|
|
|
50
|
-
function sleepMs(ms) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
69
|
+
async function sleepMs(ms) {
|
|
70
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
71
|
+
}
|
|
54
72
|
|
|
55
|
-
function withFileLock(lockPath, fn, retries = 20, delayMs = 25) {
|
|
73
|
+
async function withFileLock(lockPath, fn, retries = 20, delayMs = 25) {
|
|
56
74
|
let fd = null;
|
|
57
75
|
for (let i = 0; i < retries; i += 1) {
|
|
58
76
|
try {
|
|
@@ -60,12 +78,12 @@ function withFileLock(lockPath, fn, retries = 20, delayMs = 25) {
|
|
|
60
78
|
break;
|
|
61
79
|
} catch (e) {
|
|
62
80
|
if (e?.code !== 'EEXIST' || i === retries - 1) throw e;
|
|
63
|
-
sleepMs(delayMs);
|
|
81
|
+
await sleepMs(delayMs);
|
|
64
82
|
}
|
|
65
83
|
}
|
|
66
84
|
|
|
67
85
|
try {
|
|
68
|
-
return fn();
|
|
86
|
+
return await fn();
|
|
69
87
|
} finally {
|
|
70
88
|
try { if (fd != null) closeSync(fd); } catch {}
|
|
71
89
|
try { unlinkSync(lockPath); } catch {}
|
|
@@ -80,13 +98,13 @@ function getLeadSessionId(config) {
|
|
|
80
98
|
|| null;
|
|
81
99
|
}
|
|
82
100
|
|
|
83
|
-
export function resolveTeamPaths(teamName) {
|
|
101
|
+
export async function resolveTeamPaths(teamName) {
|
|
84
102
|
validateTeamName(teamName);
|
|
85
103
|
|
|
86
104
|
const teamDir = join(TEAMS_ROOT, teamName);
|
|
87
105
|
const configPath = join(teamDir, 'config.json');
|
|
88
106
|
const inboxesDir = join(teamDir, 'inboxes');
|
|
89
|
-
const config = readJsonSafe(configPath);
|
|
107
|
+
const config = await readJsonSafe(configPath);
|
|
90
108
|
const leadSessionId = getLeadSessionId(config);
|
|
91
109
|
|
|
92
110
|
const byTeam = join(TASKS_ROOT, teamName);
|
|
@@ -112,24 +130,49 @@ export function resolveTeamPaths(teamName) {
|
|
|
112
130
|
};
|
|
113
131
|
}
|
|
114
132
|
|
|
115
|
-
function collectTaskFiles(tasksDir) {
|
|
133
|
+
async function collectTaskFiles(tasksDir) {
|
|
116
134
|
if (!existsSync(tasksDir)) return [];
|
|
117
|
-
|
|
135
|
+
|
|
136
|
+
// 디렉토리 mtime 기반 캐시 — O(N) I/O를 반복 호출 시 O(1)로 축소
|
|
137
|
+
let dirMtime;
|
|
138
|
+
try { dirMtime = (await stat(tasksDir)).mtimeMs; } catch { return []; }
|
|
139
|
+
|
|
140
|
+
const cached = _dirCache.get(tasksDir);
|
|
141
|
+
if (cached && cached.mtimeMs === dirMtime) {
|
|
142
|
+
return cached.files;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const entries = await readdir(tasksDir);
|
|
146
|
+
const files = entries
|
|
118
147
|
.filter((name) => name.endsWith('.json'))
|
|
119
148
|
.filter((name) => !name.endsWith('.lock'))
|
|
120
149
|
.filter((name) => name !== '.highwatermark')
|
|
121
150
|
.map((name) => join(tasksDir, name));
|
|
151
|
+
|
|
152
|
+
_dirCache.set(tasksDir, { mtimeMs: dirMtime, files });
|
|
153
|
+
return files;
|
|
122
154
|
}
|
|
123
155
|
|
|
124
|
-
function locateTaskFile(tasksDir, taskId) {
|
|
156
|
+
async function locateTaskFile(tasksDir, taskId) {
|
|
125
157
|
const direct = join(tasksDir, `${taskId}.json`);
|
|
126
158
|
if (existsSync(direct)) return direct;
|
|
127
159
|
|
|
128
|
-
|
|
160
|
+
// ID→파일 인덱스 캐시
|
|
161
|
+
const indexed = _taskIdIndex.get(taskId);
|
|
162
|
+
if (indexed && existsSync(indexed)) return indexed;
|
|
163
|
+
|
|
164
|
+
// 캐시된 collectTaskFiles로 풀 스캔
|
|
165
|
+
const files = await collectTaskFiles(tasksDir);
|
|
129
166
|
for (const file of files) {
|
|
130
|
-
if (basename(file, '.json') === taskId)
|
|
131
|
-
|
|
132
|
-
|
|
167
|
+
if (basename(file, '.json') === taskId) {
|
|
168
|
+
_taskIdIndex.set(taskId, file);
|
|
169
|
+
return file;
|
|
170
|
+
}
|
|
171
|
+
const json = await readJsonSafe(file);
|
|
172
|
+
if (json && String(json.id || '') === taskId) {
|
|
173
|
+
_taskIdIndex.set(taskId, file);
|
|
174
|
+
return file;
|
|
175
|
+
}
|
|
133
176
|
}
|
|
134
177
|
return null;
|
|
135
178
|
}
|
|
@@ -138,7 +181,7 @@ function isObject(v) {
|
|
|
138
181
|
return !!v && typeof v === 'object' && !Array.isArray(v);
|
|
139
182
|
}
|
|
140
183
|
|
|
141
|
-
export function teamInfo(args = {}) {
|
|
184
|
+
export async function teamInfo(args = {}) {
|
|
142
185
|
const { team_name, include_members = true, include_paths = true } = args;
|
|
143
186
|
try {
|
|
144
187
|
validateTeamName(team_name);
|
|
@@ -146,7 +189,7 @@ export function teamInfo(args = {}) {
|
|
|
146
189
|
return err('INVALID_TEAM_NAME', 'team_name 형식이 올바르지 않습니다');
|
|
147
190
|
}
|
|
148
191
|
|
|
149
|
-
const paths = resolveTeamPaths(team_name);
|
|
192
|
+
const paths = await resolveTeamPaths(team_name);
|
|
150
193
|
if (!existsSync(paths.team_dir)) {
|
|
151
194
|
return err('TEAM_NOT_FOUND', `팀 디렉토리가 없습니다: ${paths.team_dir}`);
|
|
152
195
|
}
|
|
@@ -181,7 +224,7 @@ export function teamInfo(args = {}) {
|
|
|
181
224
|
};
|
|
182
225
|
}
|
|
183
226
|
|
|
184
|
-
export function teamTaskList(args = {}) {
|
|
227
|
+
export async function teamTaskList(args = {}) {
|
|
185
228
|
const {
|
|
186
229
|
team_name,
|
|
187
230
|
owner,
|
|
@@ -196,7 +239,7 @@ export function teamTaskList(args = {}) {
|
|
|
196
239
|
return err('INVALID_TEAM_NAME', 'team_name 형식이 올바르지 않습니다');
|
|
197
240
|
}
|
|
198
241
|
|
|
199
|
-
const paths = resolveTeamPaths(team_name);
|
|
242
|
+
const paths = await resolveTeamPaths(team_name);
|
|
200
243
|
if (paths.tasks_dir_resolution === 'not_found') {
|
|
201
244
|
return err('TASKS_DIR_NOT_FOUND', `task 디렉토리를 찾지 못했습니다: ${team_name}`);
|
|
202
245
|
}
|
|
@@ -206,8 +249,22 @@ export function teamTaskList(args = {}) {
|
|
|
206
249
|
let parseWarnings = 0;
|
|
207
250
|
|
|
208
251
|
const tasks = [];
|
|
209
|
-
for (const file of collectTaskFiles(paths.tasks_dir)) {
|
|
210
|
-
|
|
252
|
+
for (const file of await collectTaskFiles(paths.tasks_dir)) {
|
|
253
|
+
// mtime 기반 task 콘텐츠 캐시 — 변경 없으면 파일 읽기 생략
|
|
254
|
+
let fileMtime = Date.now();
|
|
255
|
+
try { fileMtime = (await stat(file)).mtimeMs; } catch {}
|
|
256
|
+
|
|
257
|
+
const contentCached = _taskContentCache.get(file);
|
|
258
|
+
let json;
|
|
259
|
+
if (contentCached && contentCached.mtimeMs === fileMtime) {
|
|
260
|
+
json = contentCached.data;
|
|
261
|
+
} else {
|
|
262
|
+
json = await readJsonSafe(file);
|
|
263
|
+
if (json && isObject(json)) {
|
|
264
|
+
_taskContentCache.set(file, { mtimeMs: fileMtime, data: json });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
211
268
|
if (!json || !isObject(json)) {
|
|
212
269
|
parseWarnings += 1;
|
|
213
270
|
continue;
|
|
@@ -217,13 +274,10 @@ export function teamTaskList(args = {}) {
|
|
|
217
274
|
if (owner && String(json.owner || '') !== String(owner)) continue;
|
|
218
275
|
if (statusSet.size > 0 && !statusSet.has(String(json.status || ''))) continue;
|
|
219
276
|
|
|
220
|
-
let mtime = Date.now();
|
|
221
|
-
try { mtime = statSync(file).mtimeMs; } catch {}
|
|
222
|
-
|
|
223
277
|
tasks.push({
|
|
224
278
|
...json,
|
|
225
279
|
task_file: file,
|
|
226
|
-
mtime_ms:
|
|
280
|
+
mtime_ms: fileMtime,
|
|
227
281
|
});
|
|
228
282
|
}
|
|
229
283
|
|
|
@@ -242,7 +296,21 @@ export function teamTaskList(args = {}) {
|
|
|
242
296
|
};
|
|
243
297
|
}
|
|
244
298
|
|
|
245
|
-
|
|
299
|
+
// status 화이트리스트 (Claude Code API 호환)
|
|
300
|
+
const VALID_STATUSES = new Set(['pending', 'in_progress', 'completed', 'deleted']);
|
|
301
|
+
|
|
302
|
+
export async function teamTaskUpdate(args = {}) {
|
|
303
|
+
// "failed" → "completed" + metadata.result 자동 매핑
|
|
304
|
+
if (String(args.status || '') === 'failed') {
|
|
305
|
+
args = {
|
|
306
|
+
...args,
|
|
307
|
+
status: 'completed',
|
|
308
|
+
metadata_patch: { ...(args.metadata_patch || {}), result: 'failed' },
|
|
309
|
+
};
|
|
310
|
+
} else if (args.status != null && !VALID_STATUSES.has(String(args.status))) {
|
|
311
|
+
return err('INVALID_STATUS', `유효하지 않은 status: ${args.status}. 허용: ${[...VALID_STATUSES].join(', ')}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
246
314
|
const {
|
|
247
315
|
team_name,
|
|
248
316
|
task_id,
|
|
@@ -269,12 +337,12 @@ export function teamTaskUpdate(args = {}) {
|
|
|
269
337
|
return err('INVALID_TASK_ID', 'task_id가 필요합니다');
|
|
270
338
|
}
|
|
271
339
|
|
|
272
|
-
const paths = resolveTeamPaths(team_name);
|
|
340
|
+
const paths = await resolveTeamPaths(team_name);
|
|
273
341
|
if (paths.tasks_dir_resolution === 'not_found') {
|
|
274
342
|
return err('TASKS_DIR_NOT_FOUND', `task 디렉토리를 찾지 못했습니다: ${team_name}`);
|
|
275
343
|
}
|
|
276
344
|
|
|
277
|
-
const taskFile = locateTaskFile(paths.tasks_dir, String(task_id));
|
|
345
|
+
const taskFile = await locateTaskFile(paths.tasks_dir, String(task_id));
|
|
278
346
|
if (!taskFile) {
|
|
279
347
|
return err('TASK_NOT_FOUND', `task를 찾지 못했습니다: ${task_id}`);
|
|
280
348
|
}
|
|
@@ -282,14 +350,14 @@ export function teamTaskUpdate(args = {}) {
|
|
|
282
350
|
const lockFile = `${taskFile}.lock`;
|
|
283
351
|
|
|
284
352
|
try {
|
|
285
|
-
return withFileLock(lockFile, () => {
|
|
286
|
-
const before = readJsonSafe(taskFile);
|
|
353
|
+
return await withFileLock(lockFile, async () => {
|
|
354
|
+
const before = await readJsonSafe(taskFile);
|
|
287
355
|
if (!before || !isObject(before)) {
|
|
288
356
|
return err('INVALID_TASK_FILE', `task 파일 파싱 실패: ${taskFile}`);
|
|
289
357
|
}
|
|
290
358
|
|
|
291
359
|
let beforeMtime = Date.now();
|
|
292
|
-
try { beforeMtime =
|
|
360
|
+
try { beforeMtime = (await stat(taskFile)).mtimeMs; } catch {}
|
|
293
361
|
|
|
294
362
|
if (if_match_mtime_ms != null && Number(if_match_mtime_ms) !== Number(beforeMtime)) {
|
|
295
363
|
return err('MTIME_CONFLICT', 'if_match_mtime_ms가 일치하지 않습니다', {
|
|
@@ -369,6 +437,9 @@ export function teamTaskUpdate(args = {}) {
|
|
|
369
437
|
|
|
370
438
|
if (updated) {
|
|
371
439
|
atomicWriteJson(taskFile, after);
|
|
440
|
+
_invalidateCache(dirname(taskFile));
|
|
441
|
+
// 콘텐츠 캐시 무효화
|
|
442
|
+
_taskContentCache.delete(taskFile);
|
|
372
443
|
}
|
|
373
444
|
|
|
374
445
|
let afterMtime = beforeMtime;
|
|
@@ -395,7 +466,7 @@ function sanitizeRecipientName(v) {
|
|
|
395
466
|
return String(v || 'team-lead').replace(/[\\/:*?"<>|]/g, '_');
|
|
396
467
|
}
|
|
397
468
|
|
|
398
|
-
export function teamSendMessage(args = {}) {
|
|
469
|
+
export async function teamSendMessage(args = {}) {
|
|
399
470
|
const {
|
|
400
471
|
team_name,
|
|
401
472
|
from,
|
|
@@ -414,47 +485,63 @@ export function teamSendMessage(args = {}) {
|
|
|
414
485
|
if (!String(from || '').trim()) return err('INVALID_FROM', 'from이 필요합니다');
|
|
415
486
|
if (!String(text || '').trim()) return err('INVALID_TEXT', 'text가 필요합니다');
|
|
416
487
|
|
|
417
|
-
const paths = resolveTeamPaths(team_name);
|
|
488
|
+
const paths = await resolveTeamPaths(team_name);
|
|
418
489
|
if (!existsSync(paths.team_dir)) {
|
|
419
490
|
return err('TEAM_NOT_FOUND', `팀 디렉토리가 없습니다: ${paths.team_dir}`);
|
|
420
491
|
}
|
|
421
492
|
|
|
422
|
-
const recipient = sanitizeRecipientName(to);
|
|
423
|
-
const inboxFile = join(paths.inboxes_dir, `${recipient}.json`);
|
|
424
|
-
const lockFile = `${inboxFile}.lock`;
|
|
425
|
-
let message;
|
|
426
|
-
|
|
427
|
-
try {
|
|
428
|
-
const unreadCount = withFileLock(lockFile, () => {
|
|
429
|
-
const queue = readJsonSafe(inboxFile);
|
|
430
|
-
const list = Array.isArray(queue) ? queue : [];
|
|
431
|
-
|
|
432
|
-
message = {
|
|
433
|
-
id: randomUUID(),
|
|
434
|
-
from: String(from),
|
|
435
|
-
text: String(text),
|
|
436
|
-
...(summary ? { summary: String(summary) } : {}),
|
|
437
|
-
timestamp: new Date().toISOString(),
|
|
438
|
-
color: String(color || 'blue'),
|
|
439
|
-
read: false,
|
|
440
|
-
};
|
|
441
|
-
list.push(message);
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
493
|
+
const recipient = sanitizeRecipientName(to);
|
|
494
|
+
const inboxFile = join(paths.inboxes_dir, `${recipient}.json`);
|
|
495
|
+
const lockFile = `${inboxFile}.lock`;
|
|
496
|
+
let message;
|
|
497
|
+
|
|
498
|
+
try {
|
|
499
|
+
const unreadCount = await withFileLock(lockFile, async () => {
|
|
500
|
+
const queue = await readJsonSafe(inboxFile);
|
|
501
|
+
const list = Array.isArray(queue) ? queue : [];
|
|
502
|
+
|
|
503
|
+
message = {
|
|
504
|
+
id: randomUUID(),
|
|
505
|
+
from: String(from),
|
|
506
|
+
text: String(text),
|
|
507
|
+
...(summary ? { summary: String(summary) } : {}),
|
|
508
|
+
timestamp: new Date().toISOString(),
|
|
509
|
+
color: String(color || 'blue'),
|
|
510
|
+
read: false,
|
|
511
|
+
};
|
|
512
|
+
list.push(message);
|
|
513
|
+
|
|
514
|
+
// inbox 정리: 최대 200개 유지, read + 1시간 경과 메시지 제거
|
|
515
|
+
const MAX_INBOX = 200;
|
|
516
|
+
if (list.length > MAX_INBOX) {
|
|
517
|
+
const ONE_HOUR_MS = 3600000;
|
|
518
|
+
const cutoff = Date.now() - ONE_HOUR_MS;
|
|
519
|
+
const pruned = list.filter((m) =>
|
|
520
|
+
m?.read !== true || !m?.timestamp || new Date(m.timestamp).getTime() > cutoff
|
|
521
|
+
);
|
|
522
|
+
list.length = 0;
|
|
523
|
+
list.push(...pruned);
|
|
524
|
+
if (list.length > MAX_INBOX) {
|
|
525
|
+
list.splice(0, list.length - MAX_INBOX);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
atomicWriteJson(inboxFile, list);
|
|
530
|
+
|
|
531
|
+
return list.filter((m) => m?.read !== true).length;
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
return {
|
|
535
|
+
ok: true,
|
|
536
|
+
data: {
|
|
537
|
+
message_id: message.id,
|
|
538
|
+
recipient,
|
|
539
|
+
inbox_file: inboxFile,
|
|
540
|
+
queued_at: message.timestamp,
|
|
541
|
+
unread_count: unreadCount,
|
|
542
|
+
},
|
|
543
|
+
};
|
|
544
|
+
} catch (e) {
|
|
545
|
+
return err('SEND_MESSAGE_FAILED', e.message);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
@@ -58,6 +58,8 @@ export function buildLeadPrompt(taskDescription, config) {
|
|
|
58
58
|
|
|
59
59
|
const workerIds = workers.map((w) => w.agentId).join(", ");
|
|
60
60
|
|
|
61
|
+
const bridgePath = "node hub/bridge.mjs";
|
|
62
|
+
|
|
61
63
|
return `리드 에이전트: ${agentId}
|
|
62
64
|
|
|
63
65
|
목표: ${taskDescription}
|
|
@@ -68,14 +70,13 @@ ${roster}
|
|
|
68
70
|
|
|
69
71
|
규칙:
|
|
70
72
|
- 가능한 짧고 핵심만 지시/요약(토큰 절약)
|
|
71
|
-
- 워커
|
|
72
|
-
|
|
73
|
+
- 워커 제어:
|
|
74
|
+
${bridgePath} result --agent ${agentId} --topic lead.control
|
|
73
75
|
- 워커 결과 수집:
|
|
74
|
-
|
|
76
|
+
${bridgePath} context --agent ${agentId} --max 20
|
|
75
77
|
- 최종 결과는 topic="task.result"를 모아 통합
|
|
76
78
|
|
|
77
|
-
|
|
78
|
-
Hub: ${hubUrl}
|
|
79
|
+
워커 ID: ${workerIds || "(없음)"}
|
|
79
80
|
지금 즉시 워커를 배정하고 병렬 진행을 관리하라.`;
|
|
80
81
|
}
|
|
81
82
|
|
|
@@ -93,26 +94,20 @@ export function buildPrompt(subtask, config) {
|
|
|
93
94
|
|
|
94
95
|
const hubBase = hubUrl.replace("/mcp", "");
|
|
95
96
|
|
|
97
|
+
const bridgePath = "node hub/bridge.mjs";
|
|
98
|
+
|
|
96
99
|
return `워커: ${agentId} (${cli})
|
|
97
100
|
작업: ${subtask}
|
|
98
101
|
|
|
99
102
|
필수 규칙:
|
|
100
103
|
1) 간결하게 작업(불필요한 장문 설명 금지)
|
|
101
|
-
2) 시작 즉시
|
|
102
|
-
register
|
|
104
|
+
2) 시작 즉시 등록:
|
|
105
|
+
${bridgePath} register --agent ${agentId} --cli ${cli} --topics lead.control,task.result
|
|
103
106
|
3) 주기적으로 수신함 확인:
|
|
104
|
-
|
|
105
|
-
4) lead.control 수신 시 즉시 반응
|
|
106
|
-
- interrupt: 즉시 중단, 진행상태 요약 publish
|
|
107
|
-
- stop: 작업 종료, 최종 요약 publish 후 대기
|
|
108
|
-
- pause: 작업 일시정지
|
|
109
|
-
- resume: 작업 재개
|
|
107
|
+
${bridgePath} context --agent ${agentId} --max 10
|
|
108
|
+
4) lead.control 수신 시 즉시 반응 (interrupt/stop/pause/resume)
|
|
110
109
|
5) 완료 시 결과 발행:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
MCP가 없으면 REST 폴백:
|
|
114
|
-
- POST ${hubBase}/bridge/register
|
|
115
|
-
- POST ${hubBase}/bridge/result
|
|
110
|
+
${bridgePath} result --agent ${agentId} --topic task.result --file <출력파일>
|
|
116
111
|
|
|
117
112
|
지금 작업을 시작하라.`;
|
|
118
113
|
}
|
|
@@ -149,7 +144,7 @@ export async function orchestrate(sessionName, assignments, opts = {}) {
|
|
|
149
144
|
teammateMode,
|
|
150
145
|
workers: workers.map((w) => ({ agentId: w.agentId, cli: w.cli, subtask: w.subtask })),
|
|
151
146
|
});
|
|
152
|
-
injectPrompt(lead.target, leadPrompt);
|
|
147
|
+
injectPrompt(lead.target, leadPrompt, { useFileRef: true });
|
|
153
148
|
await new Promise((r) => setTimeout(r, 100));
|
|
154
149
|
}
|
|
155
150
|
|
|
@@ -160,7 +155,7 @@ export async function orchestrate(sessionName, assignments, opts = {}) {
|
|
|
160
155
|
hubUrl,
|
|
161
156
|
sessionName,
|
|
162
157
|
});
|
|
163
|
-
injectPrompt(worker.target, prompt);
|
|
158
|
+
injectPrompt(worker.target, prompt, { useFileRef: true });
|
|
164
159
|
await new Promise((r) => setTimeout(r, 100));
|
|
165
160
|
}
|
|
166
161
|
}
|