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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +22 -22
- package/LICENSE +21 -21
- package/hooks/hook-registry.json +256 -256
- package/hub/adaptive-inject.mjs +1 -1
- package/hub/assign-callbacks.mjs +120 -120
- package/hub/delegator/index.mjs +14 -14
- package/hub/delegator/tool-definitions.mjs +35 -35
- package/hub/hitl.mjs +143 -143
- package/hub/router.mjs +791 -791
- package/hub/session-fingerprint.mjs +1 -1
- package/hub/team/ansi.mjs +44 -28
- package/hub/team/cli/commands/attach.mjs +37 -37
- package/hub/team/cli/commands/debug.mjs +74 -74
- package/hub/team/cli/commands/focus.mjs +53 -53
- package/hub/team/cli/commands/list.mjs +24 -24
- package/hub/team/cli/commands/start/start-in-process.mjs +40 -40
- package/hub/team/cli/commands/start/start-mux.mjs +73 -73
- package/hub/team/cli/commands/start/start-wt.mjs +69 -69
- package/hub/team/cli/commands/tasks.mjs +13 -13
- package/hub/team/cli/render.mjs +30 -30
- package/hub/team/cli/services/attach-fallback.mjs +54 -54
- package/hub/team/cli/services/member-selector.mjs +30 -30
- package/hub/team/cli/services/native-control.mjs +116 -116
- package/hub/team/cli/services/task-model.mjs +30 -30
- package/hub/team/conductor.mjs +2 -2
- package/hub/team/notify.mjs +1 -1
- package/hub/team/orchestrator.mjs +161 -161
- package/hub/team/session.mjs +611 -611
- package/hub/team/shared.mjs +13 -13
- package/hub/team/tui-lite.mjs +4 -4
- package/hub/team/tui.mjs +16 -12
- package/hub/tray.mjs +368 -368
- package/hub/workers/codex-mcp.mjs +507 -507
- package/hub/workers/factory.mjs +21 -21
- package/hud/constants.mjs +8 -2
- package/hud/providers/codex.mjs +11 -0
- package/hud/providers/gemini.mjs +21 -0
- package/package.json +1 -1
- package/scripts/claudemd-sync.mjs +11 -13
- package/scripts/completions/tfx.bash +47 -47
- package/scripts/completions/tfx.fish +44 -44
- package/scripts/completions/tfx.zsh +83 -83
- package/scripts/hub-ensure.mjs +120 -120
- package/scripts/keyword-detector.mjs +272 -272
- package/scripts/keyword-rules-expander.mjs +521 -521
- package/scripts/lib/mcp-server-catalog.mjs +118 -118
- package/scripts/notion-read.mjs +553 -553
- package/scripts/setup.mjs +23 -0
- package/scripts/test-tfx-route-no-claude-native.mjs +57 -57
- package/scripts/tfx-batch-stats.mjs +96 -96
- package/skills/.omc/state/agent-replay-8f0e10a9-9693-4410-96f5-a6b07e8ed995.jsonl +1 -0
- package/skills/.omc/state/idle-notif-cooldown.json +3 -0
- package/skills/.omc/state/last-tool-error.json +7 -0
- package/skills/.omc/state/subagent-tracking.json +7 -0
- 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
|
+
}
|
package/hub/team/conductor.mjs
CHANGED
|
@@ -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
|
-
|
|
800
|
+
ensureConductorRegistry();
|
|
801
801
|
return frozenApi;
|
|
802
802
|
}
|
package/hub/team/notify.mjs
CHANGED
|
@@ -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
|
+
}
|