triflux 10.3.0 → 10.3.2

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 (56) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +22 -22
  3. package/LICENSE +21 -21
  4. package/hooks/hook-registry.json +256 -256
  5. package/hub/adaptive-inject.mjs +1 -1
  6. package/hub/assign-callbacks.mjs +120 -120
  7. package/hub/delegator/index.mjs +14 -14
  8. package/hub/delegator/tool-definitions.mjs +35 -35
  9. package/hub/hitl.mjs +143 -143
  10. package/hub/router.mjs +791 -791
  11. package/hub/session-fingerprint.mjs +1 -1
  12. package/hub/team/ansi.mjs +44 -28
  13. package/hub/team/cli/commands/attach.mjs +37 -37
  14. package/hub/team/cli/commands/debug.mjs +74 -74
  15. package/hub/team/cli/commands/focus.mjs +53 -53
  16. package/hub/team/cli/commands/list.mjs +24 -24
  17. package/hub/team/cli/commands/start/start-in-process.mjs +40 -40
  18. package/hub/team/cli/commands/start/start-mux.mjs +73 -73
  19. package/hub/team/cli/commands/start/start-wt.mjs +69 -69
  20. package/hub/team/cli/commands/tasks.mjs +13 -13
  21. package/hub/team/cli/render.mjs +30 -30
  22. package/hub/team/cli/services/attach-fallback.mjs +54 -54
  23. package/hub/team/cli/services/member-selector.mjs +30 -30
  24. package/hub/team/cli/services/native-control.mjs +116 -116
  25. package/hub/team/cli/services/task-model.mjs +30 -30
  26. package/hub/team/conductor.mjs +2 -2
  27. package/hub/team/notify.mjs +1 -1
  28. package/hub/team/orchestrator.mjs +161 -161
  29. package/hub/team/session.mjs +611 -611
  30. package/hub/team/shared.mjs +13 -13
  31. package/hub/team/tui-lite.mjs +4 -4
  32. package/hub/team/tui.mjs +16 -12
  33. package/hub/tray.mjs +368 -368
  34. package/hub/workers/codex-mcp.mjs +507 -507
  35. package/hub/workers/factory.mjs +21 -21
  36. package/hud/constants.mjs +8 -2
  37. package/hud/providers/codex.mjs +11 -0
  38. package/hud/providers/gemini.mjs +21 -0
  39. package/package.json +1 -1
  40. package/scripts/claudemd-sync.mjs +11 -13
  41. package/scripts/completions/tfx.bash +47 -47
  42. package/scripts/completions/tfx.fish +44 -44
  43. package/scripts/completions/tfx.zsh +83 -83
  44. package/scripts/hub-ensure.mjs +120 -120
  45. package/scripts/keyword-detector.mjs +272 -272
  46. package/scripts/keyword-rules-expander.mjs +521 -521
  47. package/scripts/lib/mcp-server-catalog.mjs +118 -118
  48. package/scripts/notion-read.mjs +553 -553
  49. package/scripts/setup.mjs +23 -0
  50. package/scripts/test-tfx-route-no-claude-native.mjs +57 -57
  51. package/scripts/tfx-batch-stats.mjs +96 -96
  52. package/skills/.omc/state/agent-replay-8f0e10a9-9693-4410-96f5-a6b07e8ed995.jsonl +1 -0
  53. package/skills/.omc/state/idle-notif-cooldown.json +3 -0
  54. package/skills/.omc/state/last-tool-error.json +7 -0
  55. package/skills/.omc/state/subagent-tracking.json +7 -0
  56. package/skills/tfx-remote-spawn/references/hosts.json +16 -0
@@ -1,117 +1,117 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { spawn } from "node:child_process";
4
-
5
- import { buildLeadPrompt, buildPrompt } from "../../orchestrator.mjs";
6
- import { HUB_PID_DIR, PKG_ROOT } from "./state-store.mjs";
7
-
8
- import { buildExecArgs } from "../../../codex-adapter.mjs";
9
-
10
- export function buildNativeCliCommand(cli) {
11
- switch (cli) {
12
- case "codex":
13
- return buildExecArgs({});
14
- case "gemini":
15
- return "gemini";
16
- case "claude":
17
- return "claude";
18
- default:
19
- return cli;
20
- }
21
- }
22
-
23
- export async function startNativeSupervisor({ sessionId, task, lead, agents, subtasks, hubUrl }) {
24
- const configPath = join(HUB_PID_DIR, `team-native-${sessionId}.config.json`);
25
- const runtimePath = join(HUB_PID_DIR, `team-native-${sessionId}.runtime.json`);
26
- const logsDir = join(HUB_PID_DIR, "team-logs", sessionId);
27
- mkdirSync(logsDir, { recursive: true });
28
-
29
- const leadMember = {
30
- role: "lead",
31
- name: "lead",
32
- cli: lead,
33
- agentId: `${lead}-lead`,
34
- command: buildNativeCliCommand(lead),
35
- };
36
- const workers = agents.map((cli, index) => ({
37
- role: "worker",
38
- name: `${cli}-${index + 1}`,
39
- cli,
40
- agentId: `${cli}-w${index + 1}`,
41
- command: buildNativeCliCommand(cli),
42
- subtask: subtasks[index],
43
- }));
44
- const members = [
45
- {
46
- ...leadMember,
47
- prompt: buildLeadPrompt(task, {
48
- agentId: leadMember.agentId,
49
- hubUrl,
50
- teammateMode: "in-process",
51
- workers: workers.map((worker) => ({
52
- agentId: worker.agentId,
53
- cli: worker.cli,
54
- subtask: worker.subtask,
55
- })),
56
- }),
57
- },
58
- ...workers.map((worker) => ({
59
- ...worker,
60
- prompt: buildPrompt(worker.subtask, { cli: worker.cli, agentId: worker.agentId, hubUrl }),
61
- })),
62
- ];
63
-
64
- writeFileSync(configPath, JSON.stringify({
65
- sessionName: sessionId,
66
- hubUrl,
67
- startupDelayMs: 3000,
68
- logsDir,
69
- runtimeFile: runtimePath,
70
- members,
71
- }, null, 2) + "\n");
72
-
73
- const child = spawn(process.execPath, [join(PKG_ROOT, "hub", "team", "native-supervisor.mjs"), "--config", configPath], {
74
- detached: true,
75
- stdio: "ignore",
76
- env: { ...process.env },
77
- windowsHide: true,
78
- });
79
- child.unref();
80
-
81
- const deadline = Date.now() + 5000;
82
- while (Date.now() < deadline) {
83
- if (existsSync(runtimePath)) {
84
- try {
85
- const runtime = JSON.parse(readFileSync(runtimePath, "utf8"));
86
- return { runtime, members };
87
- } catch {}
88
- }
89
- await new Promise((resolve) => setTimeout(resolve, 100));
90
- }
91
-
92
- return { runtime: null, members };
93
- }
94
-
95
- export async function nativeRequest(state, path, body = {}) {
96
- if (!state?.native?.controlUrl) return null;
97
- try {
98
- const res = await fetch(`${state.native.controlUrl}${path}`, {
99
- method: "POST",
100
- headers: { "Content-Type": "application/json" },
101
- body: JSON.stringify(body),
102
- });
103
- return await res.json();
104
- } catch {
105
- return null;
106
- }
107
- }
108
-
109
- export async function nativeGetStatus(state) {
110
- if (!state?.native?.controlUrl) return null;
111
- try {
112
- const res = await fetch(`${state.native.controlUrl}/status`);
113
- return await res.json();
114
- } catch {
115
- return null;
116
- }
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { spawn } from "node:child_process";
4
+
5
+ import { buildLeadPrompt, buildPrompt } from "../../orchestrator.mjs";
6
+ import { HUB_PID_DIR, PKG_ROOT } from "./state-store.mjs";
7
+
8
+ import { buildExecArgs } from "../../../codex-adapter.mjs";
9
+
10
+ export function buildNativeCliCommand(cli) {
11
+ switch (cli) {
12
+ case "codex":
13
+ return buildExecArgs({});
14
+ case "gemini":
15
+ return "gemini";
16
+ case "claude":
17
+ return "claude";
18
+ default:
19
+ return cli;
20
+ }
21
+ }
22
+
23
+ export async function startNativeSupervisor({ sessionId, task, lead, agents, subtasks, hubUrl }) {
24
+ const configPath = join(HUB_PID_DIR, `team-native-${sessionId}.config.json`);
25
+ const runtimePath = join(HUB_PID_DIR, `team-native-${sessionId}.runtime.json`);
26
+ const logsDir = join(HUB_PID_DIR, "team-logs", sessionId);
27
+ mkdirSync(logsDir, { recursive: true });
28
+
29
+ const leadMember = {
30
+ role: "lead",
31
+ name: "lead",
32
+ cli: lead,
33
+ agentId: `${lead}-lead`,
34
+ command: buildNativeCliCommand(lead),
35
+ };
36
+ const workers = agents.map((cli, index) => ({
37
+ role: "worker",
38
+ name: `${cli}-${index + 1}`,
39
+ cli,
40
+ agentId: `${cli}-w${index + 1}`,
41
+ command: buildNativeCliCommand(cli),
42
+ subtask: subtasks[index],
43
+ }));
44
+ const members = [
45
+ {
46
+ ...leadMember,
47
+ prompt: buildLeadPrompt(task, {
48
+ agentId: leadMember.agentId,
49
+ hubUrl,
50
+ teammateMode: "in-process",
51
+ workers: workers.map((worker) => ({
52
+ agentId: worker.agentId,
53
+ cli: worker.cli,
54
+ subtask: worker.subtask,
55
+ })),
56
+ }),
57
+ },
58
+ ...workers.map((worker) => ({
59
+ ...worker,
60
+ prompt: buildPrompt(worker.subtask, { cli: worker.cli, agentId: worker.agentId, hubUrl }),
61
+ })),
62
+ ];
63
+
64
+ writeFileSync(configPath, JSON.stringify({
65
+ sessionName: sessionId,
66
+ hubUrl,
67
+ startupDelayMs: 3000,
68
+ logsDir,
69
+ runtimeFile: runtimePath,
70
+ members,
71
+ }, null, 2) + "\n");
72
+
73
+ const child = spawn(process.execPath, [join(PKG_ROOT, "hub", "team", "native-supervisor.mjs"), "--config", configPath], {
74
+ detached: true,
75
+ stdio: "ignore",
76
+ env: { ...process.env },
77
+ windowsHide: true,
78
+ });
79
+ child.unref();
80
+
81
+ const deadline = Date.now() + 5000;
82
+ while (Date.now() < deadline) {
83
+ if (existsSync(runtimePath)) {
84
+ try {
85
+ const runtime = JSON.parse(readFileSync(runtimePath, "utf8"));
86
+ return { runtime, members };
87
+ } catch {}
88
+ }
89
+ await new Promise((resolve) => setTimeout(resolve, 100));
90
+ }
91
+
92
+ return { runtime: null, members };
93
+ }
94
+
95
+ export async function nativeRequest(state, path, body = {}) {
96
+ if (!state?.native?.controlUrl) return null;
97
+ try {
98
+ const res = await fetch(`${state.native.controlUrl}${path}`, {
99
+ method: "POST",
100
+ headers: { "Content-Type": "application/json" },
101
+ body: JSON.stringify(body),
102
+ });
103
+ return await res.json();
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+
109
+ export async function nativeGetStatus(state) {
110
+ if (!state?.native?.controlUrl) return null;
111
+ try {
112
+ const res = await fetch(`${state.native.controlUrl}/status`);
113
+ return await res.json();
114
+ } catch {
115
+ return null;
116
+ }
117
117
  }
@@ -1,30 +1,30 @@
1
- export function buildTasks(subtasks, workers) {
2
- return subtasks.map((subtask, index) => ({
3
- id: `T${index + 1}`,
4
- title: subtask,
5
- owner: workers[index]?.name || null,
6
- status: "pending",
7
- depends_on: index === 0 ? [] : [`T${index}`],
8
- }));
9
- }
10
-
11
- export function normalizeTaskStatus(action) {
12
- const value = String(action || "").toLowerCase();
13
- if (value === "done" || value === "complete" || value === "completed") return "completed";
14
- if (value === "progress" || value === "in-progress" || value === "in_progress") return "in_progress";
15
- if (value === "pending") return "pending";
16
- return null;
17
- }
18
-
19
- export function updateTaskStatus(tasks = [], taskId, nextStatus) {
20
- const normalizedId = String(taskId || "").toUpperCase();
21
- const target = tasks.find((task) => String(task.id).toUpperCase() === normalizedId);
22
- if (!target) return { tasks, target: null };
23
-
24
- return {
25
- target: { ...target, status: nextStatus },
26
- tasks: tasks.map((task) => (
27
- String(task.id).toUpperCase() === normalizedId ? { ...task, status: nextStatus } : task
28
- )),
29
- };
30
- }
1
+ export function buildTasks(subtasks, workers) {
2
+ return subtasks.map((subtask, index) => ({
3
+ id: `T${index + 1}`,
4
+ title: subtask,
5
+ owner: workers[index]?.name || null,
6
+ status: "pending",
7
+ depends_on: index === 0 ? [] : [`T${index}`],
8
+ }));
9
+ }
10
+
11
+ export function normalizeTaskStatus(action) {
12
+ const value = String(action || "").toLowerCase();
13
+ if (value === "done" || value === "complete" || value === "completed") return "completed";
14
+ if (value === "progress" || value === "in-progress" || value === "in_progress") return "in_progress";
15
+ if (value === "pending") return "pending";
16
+ return null;
17
+ }
18
+
19
+ export function updateTaskStatus(tasks = [], taskId, nextStatus) {
20
+ const normalizedId = String(taskId || "").toUpperCase();
21
+ const target = tasks.find((task) => String(task.id).toUpperCase() === normalizedId);
22
+ if (!target) return { tasks, target: null };
23
+
24
+ return {
25
+ target: { ...target, status: nextStatus },
26
+ tasks: tasks.map((task) => (
27
+ String(task.id).toUpperCase() === normalizedId ? { ...task, status: nextStatus } : task
28
+ )),
29
+ };
30
+ }
@@ -22,7 +22,7 @@ import { createRegistry } from "../../mesh/mesh-registry.mjs";
22
22
  import { broker } from "../account-broker.mjs";
23
23
  import { killProcess } from "../platform.mjs";
24
24
  import { createConductorMeshBridge } from "./conductor-mesh-bridge.mjs";
25
- import { getConductorRegistry } from "./conductor-registry.mjs";
25
+ import { ensureConductorRegistry, getConductorRegistry } from "./conductor-registry.mjs";
26
26
  import { createEventLog } from "./event-log.mjs";
27
27
  import { createHealthProbe } from "./health-probe.mjs";
28
28
  import { buildLauncher } from "./launcher-template.mjs";
@@ -797,6 +797,6 @@ export function createConductor(opts = {}) {
797
797
  }
798
798
 
799
799
  const frozenApi = Object.freeze(conductor);
800
- getConductorRegistry().register(frozenApi);
800
+ ensureConductorRegistry();
801
801
  return frozenApi;
802
802
  }
@@ -290,4 +290,4 @@ export function createNotifier(opts = {}) {
290
290
  });
291
291
 
292
292
  return createNotifierInstance(normalizeChannels(opts.channels, env), deps);
293
- }
293
+ }
@@ -1,161 +1,161 @@
1
- // hub/team/orchestrator.mjs — 작업 분배 + 프롬프트 구성
2
- // 의존성: pane.mjs만 사용
3
- import { injectPrompt } from "./pane.mjs";
4
-
5
- /**
6
- * 작업 분해 (LLM 없이 구분자 기반)
7
- * @param {string} taskDescription — 전체 작업 설명
8
- * @param {number} agentCount — 에이전트 수
9
- * @returns {string[]} 각 에이전트의 서브태스크
10
- */
11
- export function decomposeTask(taskDescription, agentCount) {
12
- if (agentCount <= 0) return [];
13
- if (agentCount === 1) return [taskDescription];
14
-
15
- // '+', ',', '\n' 기준으로 분리
16
- const parts = taskDescription
17
- .split(/[+,\n]+/)
18
- .map((s) => s.trim())
19
- .filter(Boolean);
20
-
21
- if (parts.length === 0) return [taskDescription];
22
-
23
- // 에이전트보다 서브태스크가 적으면 마지막 에이전트에 전체 태스크 부여
24
- if (parts.length < agentCount) {
25
- const result = [...parts];
26
- while (result.length < agentCount) {
27
- result.push(taskDescription);
28
- }
29
- return result;
30
- }
31
-
32
- // 에이전트보다 서브태스크가 많으면 앞에서부터 N개, 나머지는 마지막에 합침
33
- if (parts.length > agentCount) {
34
- const result = parts.slice(0, agentCount - 1);
35
- result.push(parts.slice(agentCount - 1).join(" + "));
36
- return result;
37
- }
38
-
39
- return parts;
40
- }
41
-
42
- /**
43
- * 리드(보통 claude) 초기 프롬프트 생성
44
- * @param {string} taskDescription
45
- * @param {object} config
46
- * @param {string} config.agentId
47
- * @param {string} config.hubUrl
48
- * @param {string} config.teammateMode
49
- * @param {Array<{agentId:string, cli:string, subtask:string}>} config.workers
50
- * @returns {string}
51
- */
52
- export function buildLeadPrompt(taskDescription, config) {
53
- const { agentId, teammateMode = "tmux", workers = [] } = config;
54
-
55
- const roster = workers
56
- .map((w, i) => `${i + 1}. ${w.agentId} (${w.cli}) — ${w.subtask}`)
57
- .join("\n") || "- (워커 없음)";
58
-
59
- const workerIds = workers.map((w) => w.agentId).join(", ");
60
-
61
- const bridgePath = "node hub/bridge.mjs";
62
-
63
- return `리드 에이전트: ${agentId}
64
-
65
- 목표: ${taskDescription}
66
- 모드: ${teammateMode}
67
-
68
- 워커:
69
- ${roster}
70
-
71
- 규칙:
72
- - 가능한 짧고 핵심만 지시/요약(토큰 절약)
73
- - 워커 제어:
74
- ${bridgePath} result --agent ${agentId} --topic lead.control
75
- - 워커 결과 수집:
76
- ${bridgePath} context --agent ${agentId} --max 20
77
- - 최종 결과는 topic="task.result"를 모아 통합
78
-
79
- 워커 ID: ${workerIds || "(없음)"}
80
- 지금 즉시 워커를 배정하고 병렬 진행을 관리하라.`;
81
- }
82
-
83
- /**
84
- * 워커 초기 프롬프트 생성
85
- * @param {string} subtask — 이 에이전트의 서브태스크
86
- * @param {object} config
87
- * @param {string} config.cli — codex/gemini/claude
88
- * @param {string} config.agentId — 에이전트 식별자
89
- * @param {string} config.hubUrl — Hub URL
90
- * @returns {string}
91
- */
92
- export function buildPrompt(subtask, config) {
93
- const { cli, agentId, hubUrl } = config;
94
-
95
- const _hubBase = hubUrl.replace("/mcp", "");
96
-
97
- const bridgePath = "node hub/bridge.mjs";
98
-
99
- return `워커: ${agentId} (${cli})
100
- 작업: ${subtask}
101
-
102
- 필수 규칙:
103
- 1) 간결하게 작업(불필요한 장문 설명 금지)
104
- 2) 시작 즉시 등록:
105
- ${bridgePath} register --agent ${agentId} --cli ${cli} --topics lead.control,task.result
106
- 3) 주기적으로 수신함 확인:
107
- ${bridgePath} context --agent ${agentId} --max 10
108
- 4) lead.control 수신 시 즉시 반응 (interrupt/stop/pause/resume)
109
- 5) 완료 시 결과 발행:
110
- ${bridgePath} result --agent ${agentId} --topic task.result --file <출력파일>
111
-
112
- 지금 작업을 시작하라.`;
113
- }
114
-
115
- /**
116
- * 팀 오케스트레이션 실행 — 각 pane에 프롬프트 주입
117
- * @param {string} sessionName — tmux 세션 이름
118
- * @param {Array<{target: string, cli: string, subtask: string}>} assignments
119
- * @param {object} opts
120
- * @param {string} opts.hubUrl — Hub URL
121
- * @param {{target:string, cli:string, task:string}|null} opts.lead
122
- * @param {string} opts.teammateMode
123
- * @returns {Promise<void>}
124
- */
125
- export async function orchestrate(sessionName, assignments, opts = {}) {
126
- const {
127
- hubUrl = "http://127.0.0.1:27888/mcp",
128
- lead = null,
129
- teammateMode = "tmux",
130
- } = opts;
131
-
132
- const workers = assignments.map(({ target, cli, subtask }) => ({
133
- target,
134
- cli,
135
- subtask,
136
- agentId: `${cli}-${target.split(".").pop()}`,
137
- }));
138
-
139
- if (lead?.target) {
140
- const leadAgentId = `${lead.cli || "claude"}-${lead.target.split(".").pop()}`;
141
- const leadPrompt = buildLeadPrompt(lead.task || "팀 작업 조율", {
142
- agentId: leadAgentId,
143
- hubUrl,
144
- teammateMode,
145
- workers: workers.map((w) => ({ agentId: w.agentId, cli: w.cli, subtask: w.subtask })),
146
- });
147
- injectPrompt(lead.target, leadPrompt, { useFileRef: true });
148
- await new Promise((r) => setTimeout(r, 100));
149
- }
150
-
151
- for (const worker of workers) {
152
- const prompt = buildPrompt(worker.subtask, {
153
- cli: worker.cli,
154
- agentId: worker.agentId,
155
- hubUrl,
156
- sessionName,
157
- });
158
- injectPrompt(worker.target, prompt, { useFileRef: true });
159
- await new Promise((r) => setTimeout(r, 100));
160
- }
161
- }
1
+ // hub/team/orchestrator.mjs — 작업 분배 + 프롬프트 구성
2
+ // 의존성: pane.mjs만 사용
3
+ import { injectPrompt } from "./pane.mjs";
4
+
5
+ /**
6
+ * 작업 분해 (LLM 없이 구분자 기반)
7
+ * @param {string} taskDescription — 전체 작업 설명
8
+ * @param {number} agentCount — 에이전트 수
9
+ * @returns {string[]} 각 에이전트의 서브태스크
10
+ */
11
+ export function decomposeTask(taskDescription, agentCount) {
12
+ if (agentCount <= 0) return [];
13
+ if (agentCount === 1) return [taskDescription];
14
+
15
+ // '+', ',', '\n' 기준으로 분리
16
+ const parts = taskDescription
17
+ .split(/[+,\n]+/)
18
+ .map((s) => s.trim())
19
+ .filter(Boolean);
20
+
21
+ if (parts.length === 0) return [taskDescription];
22
+
23
+ // 에이전트보다 서브태스크가 적으면 마지막 에이전트에 전체 태스크 부여
24
+ if (parts.length < agentCount) {
25
+ const result = [...parts];
26
+ while (result.length < agentCount) {
27
+ result.push(taskDescription);
28
+ }
29
+ return result;
30
+ }
31
+
32
+ // 에이전트보다 서브태스크가 많으면 앞에서부터 N개, 나머지는 마지막에 합침
33
+ if (parts.length > agentCount) {
34
+ const result = parts.slice(0, agentCount - 1);
35
+ result.push(parts.slice(agentCount - 1).join(" + "));
36
+ return result;
37
+ }
38
+
39
+ return parts;
40
+ }
41
+
42
+ /**
43
+ * 리드(보통 claude) 초기 프롬프트 생성
44
+ * @param {string} taskDescription
45
+ * @param {object} config
46
+ * @param {string} config.agentId
47
+ * @param {string} config.hubUrl
48
+ * @param {string} config.teammateMode
49
+ * @param {Array<{agentId:string, cli:string, subtask:string}>} config.workers
50
+ * @returns {string}
51
+ */
52
+ export function buildLeadPrompt(taskDescription, config) {
53
+ const { agentId, teammateMode = "tmux", workers = [] } = config;
54
+
55
+ const roster = workers
56
+ .map((w, i) => `${i + 1}. ${w.agentId} (${w.cli}) — ${w.subtask}`)
57
+ .join("\n") || "- (워커 없음)";
58
+
59
+ const workerIds = workers.map((w) => w.agentId).join(", ");
60
+
61
+ const bridgePath = "node hub/bridge.mjs";
62
+
63
+ return `리드 에이전트: ${agentId}
64
+
65
+ 목표: ${taskDescription}
66
+ 모드: ${teammateMode}
67
+
68
+ 워커:
69
+ ${roster}
70
+
71
+ 규칙:
72
+ - 가능한 짧고 핵심만 지시/요약(토큰 절약)
73
+ - 워커 제어:
74
+ ${bridgePath} result --agent ${agentId} --topic lead.control
75
+ - 워커 결과 수집:
76
+ ${bridgePath} context --agent ${agentId} --max 20
77
+ - 최종 결과는 topic="task.result"를 모아 통합
78
+
79
+ 워커 ID: ${workerIds || "(없음)"}
80
+ 지금 즉시 워커를 배정하고 병렬 진행을 관리하라.`;
81
+ }
82
+
83
+ /**
84
+ * 워커 초기 프롬프트 생성
85
+ * @param {string} subtask — 이 에이전트의 서브태스크
86
+ * @param {object} config
87
+ * @param {string} config.cli — codex/gemini/claude
88
+ * @param {string} config.agentId — 에이전트 식별자
89
+ * @param {string} config.hubUrl — Hub URL
90
+ * @returns {string}
91
+ */
92
+ export function buildPrompt(subtask, config) {
93
+ const { cli, agentId, hubUrl } = config;
94
+
95
+ const _hubBase = hubUrl.replace("/mcp", "");
96
+
97
+ const bridgePath = "node hub/bridge.mjs";
98
+
99
+ return `워커: ${agentId} (${cli})
100
+ 작업: ${subtask}
101
+
102
+ 필수 규칙:
103
+ 1) 간결하게 작업(불필요한 장문 설명 금지)
104
+ 2) 시작 즉시 등록:
105
+ ${bridgePath} register --agent ${agentId} --cli ${cli} --topics lead.control,task.result
106
+ 3) 주기적으로 수신함 확인:
107
+ ${bridgePath} context --agent ${agentId} --max 10
108
+ 4) lead.control 수신 시 즉시 반응 (interrupt/stop/pause/resume)
109
+ 5) 완료 시 결과 발행:
110
+ ${bridgePath} result --agent ${agentId} --topic task.result --file <출력파일>
111
+
112
+ 지금 작업을 시작하라.`;
113
+ }
114
+
115
+ /**
116
+ * 팀 오케스트레이션 실행 — 각 pane에 프롬프트 주입
117
+ * @param {string} sessionName — tmux 세션 이름
118
+ * @param {Array<{target: string, cli: string, subtask: string}>} assignments
119
+ * @param {object} opts
120
+ * @param {string} opts.hubUrl — Hub URL
121
+ * @param {{target:string, cli:string, task:string}|null} opts.lead
122
+ * @param {string} opts.teammateMode
123
+ * @returns {Promise<void>}
124
+ */
125
+ export async function orchestrate(sessionName, assignments, opts = {}) {
126
+ const {
127
+ hubUrl = "http://127.0.0.1:27888/mcp",
128
+ lead = null,
129
+ teammateMode = "tmux",
130
+ } = opts;
131
+
132
+ const workers = assignments.map(({ target, cli, subtask }) => ({
133
+ target,
134
+ cli,
135
+ subtask,
136
+ agentId: `${cli}-${target.split(".").pop()}`,
137
+ }));
138
+
139
+ if (lead?.target) {
140
+ const leadAgentId = `${lead.cli || "claude"}-${lead.target.split(".").pop()}`;
141
+ const leadPrompt = buildLeadPrompt(lead.task || "팀 작업 조율", {
142
+ agentId: leadAgentId,
143
+ hubUrl,
144
+ teammateMode,
145
+ workers: workers.map((w) => ({ agentId: w.agentId, cli: w.cli, subtask: w.subtask })),
146
+ });
147
+ injectPrompt(lead.target, leadPrompt, { useFileRef: true });
148
+ await new Promise((r) => setTimeout(r, 100));
149
+ }
150
+
151
+ for (const worker of workers) {
152
+ const prompt = buildPrompt(worker.subtask, {
153
+ cli: worker.cli,
154
+ agentId: worker.agentId,
155
+ hubUrl,
156
+ sessionName,
157
+ });
158
+ injectPrompt(worker.target, prompt, { useFileRef: true });
159
+ await new Promise((r) => setTimeout(r, 100));
160
+ }
161
+ }