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.
Files changed (52) hide show
  1. package/bin/triflux.mjs +1296 -1055
  2. package/hooks/hooks.json +17 -0
  3. package/hooks/keyword-rules.json +20 -4
  4. package/hooks/pipeline-stop.mjs +54 -0
  5. package/hub/bridge.mjs +517 -318
  6. package/hub/hitl.mjs +45 -31
  7. package/hub/pipe.mjs +457 -0
  8. package/hub/pipeline/index.mjs +121 -0
  9. package/hub/pipeline/state.mjs +164 -0
  10. package/hub/pipeline/transitions.mjs +114 -0
  11. package/hub/router.mjs +422 -161
  12. package/hub/schema.sql +14 -0
  13. package/hub/server.mjs +499 -424
  14. package/hub/store.mjs +388 -314
  15. package/hub/team/cli-team-common.mjs +348 -0
  16. package/hub/team/cli-team-control.mjs +393 -0
  17. package/hub/team/cli-team-start.mjs +516 -0
  18. package/hub/team/cli-team-status.mjs +269 -0
  19. package/hub/team/cli.mjs +75 -1475
  20. package/hub/team/dashboard.mjs +1 -9
  21. package/hub/team/native.mjs +190 -130
  22. package/hub/team/nativeProxy.mjs +165 -78
  23. package/hub/team/orchestrator.mjs +15 -20
  24. package/hub/team/pane.mjs +137 -103
  25. package/hub/team/psmux.mjs +506 -0
  26. package/hub/team/session.mjs +393 -330
  27. package/hub/team/shared.mjs +13 -0
  28. package/hub/team/staleState.mjs +299 -0
  29. package/hub/tools.mjs +105 -31
  30. package/hub/workers/claude-worker.mjs +446 -0
  31. package/hub/workers/codex-mcp.mjs +414 -0
  32. package/hub/workers/factory.mjs +18 -0
  33. package/hub/workers/gemini-worker.mjs +349 -0
  34. package/hub/workers/interface.mjs +41 -0
  35. package/hud/hud-qos-status.mjs +1790 -1788
  36. package/package.json +4 -1
  37. package/scripts/__tests__/keyword-detector.test.mjs +8 -8
  38. package/scripts/keyword-detector.mjs +15 -0
  39. package/scripts/lib/keyword-rules.mjs +4 -1
  40. package/scripts/preflight-cache.mjs +72 -0
  41. package/scripts/psmux-steering-prototype.sh +368 -0
  42. package/scripts/setup.mjs +136 -71
  43. package/scripts/tfx-route-worker.mjs +161 -0
  44. package/scripts/tfx-route.sh +485 -91
  45. package/skills/tfx-auto/SKILL.md +90 -564
  46. package/skills/tfx-auto-codex/SKILL.md +1 -3
  47. package/skills/tfx-codex/SKILL.md +1 -4
  48. package/skills/tfx-doctor/SKILL.md +1 -0
  49. package/skills/tfx-gemini/SKILL.md +1 -4
  50. package/skills/tfx-multi/SKILL.md +378 -0
  51. package/skills/tfx-setup/SKILL.md +1 -4
  52. package/skills/tfx-team/SKILL.md +0 -304
@@ -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(readFileSync(path, 'utf8'));
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
- renameSync(tmp, path);
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
- // busy-wait를 피하고 Atomics.wait로 동기 대기 (CPU 점유 최소화)
52
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
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
- return readdirSync(tasksDir)
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
- const files = collectTaskFiles(tasksDir);
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) return file;
131
- const json = readJsonSafe(file);
132
- if (json && String(json.id || '') === taskId) return file;
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
- const json = readJsonSafe(file);
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: mtime,
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
- export function teamTaskUpdate(args = {}) {
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 = statSync(taskFile).mtimeMs; } catch {}
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
- atomicWriteJson(inboxFile, list);
443
-
444
- return list.filter((m) => m?.read !== true).length;
445
- });
446
-
447
- return {
448
- ok: true,
449
- data: {
450
- message_id: message.id,
451
- recipient,
452
- inbox_file: inboxFile,
453
- queued_at: message.timestamp,
454
- unread_count: unreadCount,
455
- },
456
- };
457
- } catch (e) {
458
- return err('SEND_MESSAGE_FAILED', e.message);
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
- publish(from="${agentId}", to="<worker-agent-id>", topic="lead.control", payload={command:"interrupt|stop|pause|resume", reason:"..."})
73
+ - 워커 제어:
74
+ ${bridgePath} result --agent ${agentId} --topic lead.control
73
75
  - 워커 결과 수집:
74
- poll_messages(agent_id="${agentId}", wait_ms=1000, max_messages=20)
76
+ ${bridgePath} context --agent ${agentId} --max 20
75
77
  - 최종 결과는 topic="task.result"를 모아 통합
76
78
 
77
- 권장 워커 ID: ${workerIds || "(없음)"}
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) 시작 즉시 register 실행:
102
- register(agent_id="${agentId}", cli="${cli}", capabilities=["code"], topics=["lead.control","task.result"], heartbeat_ttl_ms=600000)
104
+ 2) 시작 즉시 등록:
105
+ ${bridgePath} register --agent ${agentId} --cli ${cli} --topics lead.control,task.result
103
106
  3) 주기적으로 수신함 확인:
104
- poll_messages(agent_id="${agentId}", wait_ms=1000, max_messages=10)
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
- publish(from="${agentId}", to="topic:task.result", topic="task.result", payload={summary:"..."})
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
  }