@triflux/remote 10.1.0 → 10.25.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/hub/pipe.mjs +239 -121
- package/hub/public/dashboard.html +61 -0
- package/hub/server.mjs +2165 -708
- package/hub/store-adapter.mjs +59 -26
- package/hub/store.mjs +356 -153
- package/hub/team/agent-map.json +26 -7
- package/hub/team/ansi.mjs +245 -146
- package/hub/team/backend.mjs +84 -17
- package/hub/team/build-worker-prompt.mjs +42 -0
- package/hub/team/check-mcp-hub.mjs +49 -0
- package/hub/team/claude-daemon-control.mjs +219 -0
- package/hub/team/claude-native-bridge.mjs +784 -0
- package/hub/team/claude-session-projection.mjs +65 -0
- package/hub/team/cli/commands/attach.mjs +57 -37
- package/hub/team/cli/commands/control.mjs +29 -8
- package/hub/team/cli/commands/debug.mjs +95 -74
- package/hub/team/cli/commands/focus.mjs +80 -53
- package/hub/team/cli/commands/interrupt.mjs +18 -6
- package/hub/team/cli/commands/kill.mjs +16 -5
- package/hub/team/cli/commands/list.mjs +31 -24
- package/hub/team/cli/commands/send.mjs +19 -6
- package/hub/team/cli/commands/start/index.mjs +154 -33
- package/hub/team/cli/commands/start/parse-args.mjs +89 -12
- package/hub/team/cli/commands/start/start-headless.mjs +125 -37
- package/hub/team/cli/commands/start/start-in-process.mjs +50 -40
- package/hub/team/cli/commands/start/start-mux.mjs +122 -73
- package/hub/team/cli/commands/start/start-wt.mjs +86 -69
- package/hub/team/cli/commands/status.mjs +43 -14
- package/hub/team/cli/commands/stop.mjs +11 -4
- package/hub/team/cli/commands/task.mjs +8 -3
- package/hub/team/cli/commands/tasks.mjs +13 -13
- package/hub/team/cli/index.mjs +2 -2
- package/hub/team/cli/manifest.mjs +38 -8
- package/hub/team/cli/render.mjs +52 -30
- package/hub/team/cli/services/attach-fallback.mjs +74 -54
- package/hub/team/cli/services/hub-client.mjs +44 -16
- package/hub/team/cli/services/member-selector.mjs +37 -30
- package/hub/team/cli/services/native-control.mjs +144 -116
- package/hub/team/cli/services/runtime-mode.mjs +55 -1
- package/hub/team/cli/services/state-store.mjs +66 -9
- package/hub/team/cli/services/task-model.mjs +40 -30
- package/hub/team/codex-review.mjs +521 -0
- package/hub/team/conductor-mesh-bridge.mjs +25 -24
- package/hub/team/conductor-registry.mjs +121 -0
- package/hub/team/conductor.mjs +1379 -671
- package/hub/team/consensus-meta.mjs +176 -0
- package/hub/team/dashboard-anchor.mjs +4 -5
- package/hub/team/dashboard-layout.mjs +3 -1
- package/hub/team/dashboard-open.mjs +20 -118
- package/hub/team/dashboard.mjs +76 -28
- package/hub/team/event-log.mjs +18 -10
- package/hub/team/execution-mode.mjs +241 -0
- package/hub/team/extract-completion-payload.mjs +110 -0
- package/hub/team/git-preflight.mjs +286 -0
- package/hub/team/handoff.mjs +49 -18
- package/hub/team/headless-bridge-session.mjs +17 -0
- package/hub/team/headless.mjs +2065 -1156
- package/hub/team/health-probe.mjs +232 -69
- package/hub/team/launcher-template.mjs +23 -13
- package/hub/team/lead-control.mjs +91 -4
- package/hub/team/mcp-selector.mjs +145 -0
- package/hub/team/native-supervisor.mjs +90 -26
- package/hub/team/native.mjs +74 -35
- package/hub/team/nativeProxy.mjs +184 -113
- package/hub/team/notify.mjs +147 -78
- package/hub/team/orchestrator.mjs +173 -161
- package/hub/team/pane.mjs +43 -15
- package/hub/team/process-cleanup.mjs +69 -25
- package/hub/team/psmux.mjs +1020 -279
- package/hub/team/recovery-store.mjs +149 -0
- package/hub/team/remote-probe.mjs +68 -52
- package/hub/team/remote-session.mjs +131 -59
- package/hub/team/remote-watcher.mjs +61 -33
- package/hub/team/retry-state-machine.mjs +267 -0
- package/hub/team/routing.mjs +51 -25
- package/hub/team/runtime-strategy.mjs +200 -0
- package/hub/team/schemas/worker-completion.json +39 -0
- package/hub/team/sentinel-capture.mjs +127 -0
- package/hub/team/session-sync.mjs +153 -6
- package/hub/team/session.mjs +605 -611
- package/hub/team/shared.mjs +13 -13
- package/hub/team/staleState.mjs +72 -30
- package/hub/team/swarm-cli.mjs +390 -0
- package/hub/team/swarm-hypervisor.mjs +1522 -193
- package/hub/team/swarm-intent.mjs +216 -0
- package/hub/team/swarm-locks.mjs +140 -25
- package/hub/team/swarm-planner.mjs +121 -21
- package/hub/team/swarm-preflight.mjs +317 -0
- package/hub/team/swarm-reconciler.mjs +128 -23
- package/hub/team/synapse-cli.mjs +458 -0
- package/hub/team/synapse-http.mjs +60 -0
- package/hub/team/synapse-registry.mjs +329 -0
- package/hub/team/terminal-opener.mjs +216 -0
- package/hub/team/tui-core.mjs +288 -0
- package/hub/team/tui-lite.mjs +234 -146
- package/hub/team/tui-remote-adapter.mjs +18 -34
- package/hub/team/tui-synapse.mjs +223 -0
- package/hub/team/tui-viewer.mjs +104 -44
- package/hub/team/tui-widgets.mjs +262 -0
- package/hub/team/tui.mjs +927 -437
- package/hub/team/worker-completion-validator.mjs +84 -0
- package/hub/team/worker-sandbox.mjs +93 -0
- package/hub/team/worker-signal.mjs +263 -0
- package/hub/team/worker-signal.types.d.ts +52 -0
- package/hub/team/worktree-lifecycle.mjs +517 -63
- package/hub/team/wt-manager.mjs +297 -26
- package/hub/tools.mjs +474 -252
- package/hub/tray.mjs +411 -368
- package/hub/workers/claude-worker.mjs +162 -118
- package/hub/workers/codex-app-server-worker.mjs +1015 -0
- package/hub/workers/codex-mcp.mjs +724 -507
- package/hub/workers/delegator-mcp.mjs +662 -335
- package/hub/workers/factory.mjs +100 -21
- package/hub/workers/gemini-worker.mjs +179 -86
- package/hub/workers/interface.mjs +23 -2
- package/hub/workers/lib/jsonrpc-stdio.mjs +463 -0
- package/hub/workers/worker-utils.mjs +22 -16
- package/package.json +4 -3
- package/scripts/lib/agent-json.mjs +27 -0
- package/scripts/lib/async.mjs +174 -0
- package/scripts/lib/claudemd-scanner.mjs +223 -0
- package/scripts/lib/cli-agy.mjs +62 -0
- package/scripts/lib/cli-claude.mjs +78 -0
- package/scripts/lib/cli-codex.mjs +199 -0
- package/scripts/lib/cli-gemini.mjs +67 -0
- package/scripts/lib/codex-recovery.sh +50 -0
- package/scripts/lib/context.mjs +67 -0
- package/scripts/lib/cross-review-utils.mjs +59 -0
- package/scripts/lib/doctor-env-checks.mjs +121 -0
- package/scripts/lib/dynamic-route-cli.mjs +107 -0
- package/scripts/lib/env-probe.mjs +269 -0
- package/scripts/lib/env.mjs +14 -0
- package/scripts/lib/gemini-profiles.mjs +119 -0
- package/scripts/lib/handoff.mjs +187 -0
- package/scripts/lib/hook-utils.mjs +16 -0
- package/scripts/lib/hub.mjs +48 -0
- package/scripts/lib/keyword-rules.mjs +195 -0
- package/scripts/lib/logger.mjs +105 -0
- package/scripts/lib/mcp-filter.mjs +883 -0
- package/scripts/lib/mcp-gateway-servers.mjs +70 -0
- package/scripts/lib/mcp-guard-engine.mjs +2120 -0
- package/scripts/lib/mcp-health.mjs +773 -0
- package/scripts/lib/mcp-manifest.mjs +89 -0
- package/scripts/lib/mcp-server-catalog.mjs +355 -0
- package/scripts/lib/pid.mjs +63 -0
- package/scripts/lib/process-utils.mjs +26 -0
- package/scripts/lib/psmux-info.mjs +179 -0
- package/scripts/lib/quota.mjs +47 -0
- package/scripts/lib/remote-spawn-transfer.mjs +226 -0
- package/scripts/lib/skill-state.mjs +220 -0
- package/scripts/lib/skill-template.mjs +375 -0
- package/scripts/lib/team.mjs +75 -0
- package/scripts/lib/timeout.mjs +66 -0
- package/scripts/lib/tmp.mjs +26 -0
- package/scripts/lib/toml.mjs +113 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
const TASK_TYPES = Object.freeze([
|
|
2
|
+
"implement",
|
|
3
|
+
"review",
|
|
4
|
+
"research",
|
|
5
|
+
"qa",
|
|
6
|
+
"ship",
|
|
7
|
+
"multi",
|
|
8
|
+
"swarm",
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
const CLI_TYPES = Object.freeze(["codex", "gemini"]);
|
|
12
|
+
|
|
13
|
+
export const MCP_CATALOG = Object.freeze([
|
|
14
|
+
Object.freeze({
|
|
15
|
+
name: "filesystem",
|
|
16
|
+
purpose: "Read and write workspace files",
|
|
17
|
+
taskTypes: Object.freeze(["implement", "review", "qa", "ship"]),
|
|
18
|
+
cli: Object.freeze(["codex", "gemini"]),
|
|
19
|
+
}),
|
|
20
|
+
Object.freeze({
|
|
21
|
+
name: "github",
|
|
22
|
+
purpose: "Inspect PRs, issues, and release state",
|
|
23
|
+
taskTypes: Object.freeze(["review", "ship"]),
|
|
24
|
+
cli: Object.freeze(["codex"]),
|
|
25
|
+
}),
|
|
26
|
+
Object.freeze({
|
|
27
|
+
name: "browser",
|
|
28
|
+
purpose: "Research web pages and verify browser flows",
|
|
29
|
+
taskTypes: Object.freeze(["research", "qa"]),
|
|
30
|
+
cli: Object.freeze(["codex", "gemini"]),
|
|
31
|
+
}),
|
|
32
|
+
Object.freeze({
|
|
33
|
+
name: "context7",
|
|
34
|
+
purpose: "Fetch current SDK and API documentation",
|
|
35
|
+
taskTypes: Object.freeze(["implement", "review", "research"]),
|
|
36
|
+
cli: Object.freeze(["codex", "gemini"]),
|
|
37
|
+
}),
|
|
38
|
+
Object.freeze({
|
|
39
|
+
name: "exa",
|
|
40
|
+
purpose: "Search code examples and repositories",
|
|
41
|
+
taskTypes: Object.freeze(["research", "review"]),
|
|
42
|
+
cli: Object.freeze(["codex", "gemini"]),
|
|
43
|
+
}),
|
|
44
|
+
Object.freeze({
|
|
45
|
+
name: "tavily",
|
|
46
|
+
purpose: "Verify current external facts and search results",
|
|
47
|
+
taskTypes: Object.freeze(["research", "qa"]),
|
|
48
|
+
cli: Object.freeze(["gemini"]),
|
|
49
|
+
}),
|
|
50
|
+
Object.freeze({
|
|
51
|
+
name: "sequential-thinking",
|
|
52
|
+
purpose: "Structured reasoning for audits and review",
|
|
53
|
+
taskTypes: Object.freeze(["review"]),
|
|
54
|
+
cli: Object.freeze(["codex"]),
|
|
55
|
+
}),
|
|
56
|
+
Object.freeze({
|
|
57
|
+
name: "tfx-hub",
|
|
58
|
+
purpose: "Coordinate triflux hub, multi-agent, and swarm work",
|
|
59
|
+
taskTypes: Object.freeze(["ship", "multi", "swarm"]),
|
|
60
|
+
cli: Object.freeze(["codex", "gemini"]),
|
|
61
|
+
}),
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
function uniqueStrings(values = []) {
|
|
65
|
+
return [
|
|
66
|
+
...new Set(
|
|
67
|
+
(Array.isArray(values) ? values : [])
|
|
68
|
+
.map((value) => String(value ?? "").trim())
|
|
69
|
+
.filter(Boolean),
|
|
70
|
+
),
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function assertOneOf(label, value, allowed) {
|
|
75
|
+
if (!allowed.includes(value)) {
|
|
76
|
+
throw new TypeError(`${label} must be one of: ${allowed.join(", ")}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function selectMcpServers(opts = {}) {
|
|
81
|
+
const taskType = String(opts.taskType ?? "").trim();
|
|
82
|
+
const cli = String(opts.cli ?? "").trim();
|
|
83
|
+
assertOneOf("taskType", taskType, TASK_TYPES);
|
|
84
|
+
assertOneOf("cli", cli, CLI_TYPES);
|
|
85
|
+
|
|
86
|
+
const allCatalogNames = MCP_CATALOG.map((server) => server.name);
|
|
87
|
+
const available = new Set(
|
|
88
|
+
uniqueStrings(opts.available?.length ? opts.available : allCatalogNames),
|
|
89
|
+
);
|
|
90
|
+
const exclude = new Set(uniqueStrings(opts.exclude));
|
|
91
|
+
const force = uniqueStrings(opts.force);
|
|
92
|
+
|
|
93
|
+
const taskMatched = MCP_CATALOG.filter((server) =>
|
|
94
|
+
server.taskTypes.includes(taskType),
|
|
95
|
+
);
|
|
96
|
+
const compatible = taskMatched.filter((server) => server.cli.includes(cli));
|
|
97
|
+
const incompatible = taskMatched.filter(
|
|
98
|
+
(server) => !server.cli.includes(cli),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const selected = uniqueStrings([
|
|
102
|
+
...compatible
|
|
103
|
+
.map((server) => server.name)
|
|
104
|
+
.filter((name) => available.has(name) && !exclude.has(name)),
|
|
105
|
+
...force.filter((name) => available.has(name) && !exclude.has(name)),
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
const reasonParts = [
|
|
109
|
+
`task=${taskType}`,
|
|
110
|
+
`cli=${cli}`,
|
|
111
|
+
`selected=${selected.length ? selected.join(", ") : "none"}`,
|
|
112
|
+
];
|
|
113
|
+
if (incompatible.length) {
|
|
114
|
+
reasonParts.push(
|
|
115
|
+
`cli-filtered=${incompatible.map((server) => server.name).join(", ")}`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
if (force.length) reasonParts.push(`forced=${force.join(", ")}`);
|
|
119
|
+
if (exclude.size) reasonParts.push(`excluded=${[...exclude].join(", ")}`);
|
|
120
|
+
if (opts.available?.length) {
|
|
121
|
+
reasonParts.push(`available=${[...available].join(", ")}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
selected,
|
|
126
|
+
reason: reasonParts.join(" | "),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function buildMcpArgs(servers = [], cli) {
|
|
131
|
+
const resolvedCli = String(cli ?? "").trim();
|
|
132
|
+
assertOneOf("cli", resolvedCli, CLI_TYPES);
|
|
133
|
+
|
|
134
|
+
const selected = uniqueStrings(servers);
|
|
135
|
+
if (!selected.length) return [];
|
|
136
|
+
|
|
137
|
+
if (resolvedCli === "codex") {
|
|
138
|
+
return selected.flatMap((server) => [
|
|
139
|
+
"-c",
|
|
140
|
+
`mcp_servers.${server}.enabled=true`,
|
|
141
|
+
]);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return ["--allowed-mcp-server-names", ...selected];
|
|
145
|
+
}
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
// hub/team/native-supervisor.mjs — tmux 없이 멀티 CLI를 직접 띄우는 네이티브 팀 런타임
|
|
2
|
+
|
|
3
|
+
import { execSync as execSyncSupervisor, spawn } from "node:child_process";
|
|
4
|
+
import {
|
|
5
|
+
createWriteStream,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
2
10
|
import { createServer } from "node:http";
|
|
3
|
-
import { spawn, execSync as execSyncSupervisor } from "node:child_process";
|
|
4
|
-
import { mkdirSync, readFileSync, writeFileSync, createWriteStream } from "node:fs";
|
|
5
11
|
import { dirname, join } from "node:path";
|
|
12
|
+
import {
|
|
13
|
+
ensureBashScriptExecution,
|
|
14
|
+
resolveBashExecutable,
|
|
15
|
+
} from "@triflux/core/hub/lib/bash-path.mjs";
|
|
6
16
|
import { verifySlimWrapperRouteExecution } from "./native.mjs";
|
|
7
17
|
import { forceCleanupTeam } from "./nativeProxy.mjs";
|
|
18
|
+
import { buildWorkerSandboxEnv } from "./worker-sandbox.mjs";
|
|
8
19
|
|
|
9
20
|
const ROUTE_LOG_TAIL_BYTES = 65536;
|
|
10
21
|
|
|
@@ -106,6 +117,7 @@ function memberStateSnapshot() {
|
|
|
106
117
|
routeVerification: state?.routeVerification || null,
|
|
107
118
|
logFile: state?.logFile || null,
|
|
108
119
|
errFile: state?.errFile || null,
|
|
120
|
+
workerSandboxRoot: state?.workerSandboxRoot || null,
|
|
109
121
|
});
|
|
110
122
|
}
|
|
111
123
|
return states;
|
|
@@ -130,18 +142,23 @@ const SAFE_COMMAND_RE = /^[a-zA-Z0-9 _./:@"'=\-\\]+$/;
|
|
|
130
142
|
|
|
131
143
|
function validateMemberCommand(command, memberName) {
|
|
132
144
|
if (typeof command !== "string" || command.trim().length === 0) {
|
|
133
|
-
throw new Error(
|
|
145
|
+
throw new Error(
|
|
146
|
+
`member "${memberName}": command must be a non-empty string`,
|
|
147
|
+
);
|
|
134
148
|
}
|
|
135
149
|
if (!SAFE_COMMAND_RE.test(command)) {
|
|
136
150
|
throw new Error(
|
|
137
151
|
`member "${memberName}": command contains disallowed characters — ` +
|
|
138
|
-
|
|
152
|
+
`shell metacharacters (;&|$\`()<>{}\\n\\r) are not permitted`,
|
|
139
153
|
);
|
|
140
154
|
}
|
|
141
155
|
}
|
|
142
156
|
|
|
143
157
|
function spawnMember(member) {
|
|
144
|
-
|
|
158
|
+
const command = ensureBashScriptExecution(member.command, {
|
|
159
|
+
bashCommand: resolveBashExecutable(),
|
|
160
|
+
});
|
|
161
|
+
validateMemberCommand(command, member.name);
|
|
145
162
|
|
|
146
163
|
const outPath = join(logsDir, `${member.name}.out.log`);
|
|
147
164
|
const errPath = join(logsDir, `${member.name}.err.log`);
|
|
@@ -149,12 +166,25 @@ function spawnMember(member) {
|
|
|
149
166
|
const outWs = createWriteStream(outPath, { flags: "a" });
|
|
150
167
|
const errWs = createWriteStream(errPath, { flags: "a" });
|
|
151
168
|
|
|
152
|
-
const
|
|
169
|
+
const baseEnv = {
|
|
170
|
+
...process.env,
|
|
171
|
+
TERM:
|
|
172
|
+
process.env.TERM && process.env.TERM !== "dumb"
|
|
173
|
+
? process.env.TERM
|
|
174
|
+
: "xterm-256color",
|
|
175
|
+
};
|
|
176
|
+
const sandbox =
|
|
177
|
+
member.role === "worker"
|
|
178
|
+
? buildWorkerSandboxEnv({
|
|
179
|
+
cwd: config.workdir || process.cwd(),
|
|
180
|
+
sessionId: `${sessionName}-${member.name}`,
|
|
181
|
+
env: baseEnv,
|
|
182
|
+
})
|
|
183
|
+
: { env: {}, root: null };
|
|
184
|
+
|
|
185
|
+
const child = spawn(command, {
|
|
153
186
|
shell: true,
|
|
154
|
-
env: {
|
|
155
|
-
...process.env,
|
|
156
|
-
TERM: process.env.TERM && process.env.TERM !== "dumb" ? process.env.TERM : "xterm-256color",
|
|
157
|
-
},
|
|
187
|
+
env: { ...baseEnv, ...sandbox.env },
|
|
158
188
|
stdio: ["pipe", "pipe", "pipe"],
|
|
159
189
|
windowsHide: true,
|
|
160
190
|
});
|
|
@@ -169,6 +199,7 @@ function spawnMember(member) {
|
|
|
169
199
|
status: "running",
|
|
170
200
|
exitCode: null,
|
|
171
201
|
lastPreview: "",
|
|
202
|
+
workerSandboxRoot: sandbox.root || null,
|
|
172
203
|
};
|
|
173
204
|
|
|
174
205
|
child.stdout.on("data", (buf) => {
|
|
@@ -176,7 +207,8 @@ function spawnMember(member) {
|
|
|
176
207
|
const txt = safeText(buf).trim();
|
|
177
208
|
if (txt) {
|
|
178
209
|
const lines = txt.split(/\r?\n/).filter(Boolean);
|
|
179
|
-
if (lines.length)
|
|
210
|
+
if (lines.length)
|
|
211
|
+
state.lastPreview = lines[lines.length - 1].slice(0, 280);
|
|
180
212
|
}
|
|
181
213
|
});
|
|
182
214
|
|
|
@@ -185,7 +217,8 @@ function spawnMember(member) {
|
|
|
185
217
|
const txt = safeText(buf).trim();
|
|
186
218
|
if (txt) {
|
|
187
219
|
const lines = txt.split(/\r?\n/).filter(Boolean);
|
|
188
|
-
if (lines.length)
|
|
220
|
+
if (lines.length)
|
|
221
|
+
state.lastPreview = `[err] ${lines[lines.length - 1].slice(0, 260)}`;
|
|
189
222
|
}
|
|
190
223
|
});
|
|
191
224
|
|
|
@@ -193,8 +226,12 @@ function spawnMember(member) {
|
|
|
193
226
|
state.status = "exited";
|
|
194
227
|
state.exitCode = code;
|
|
195
228
|
finalizeRouteVerification(state);
|
|
196
|
-
try {
|
|
197
|
-
|
|
229
|
+
try {
|
|
230
|
+
outWs.end();
|
|
231
|
+
} catch {}
|
|
232
|
+
try {
|
|
233
|
+
errWs.end();
|
|
234
|
+
} catch {}
|
|
198
235
|
maybeAutoShutdown();
|
|
199
236
|
});
|
|
200
237
|
|
|
@@ -202,8 +239,12 @@ function spawnMember(member) {
|
|
|
202
239
|
state.status = "exited";
|
|
203
240
|
state.exitCode = -1;
|
|
204
241
|
state.lastPreview = `[spawn error] ${err.message}`;
|
|
205
|
-
try {
|
|
206
|
-
|
|
242
|
+
try {
|
|
243
|
+
outWs.end();
|
|
244
|
+
} catch {}
|
|
245
|
+
try {
|
|
246
|
+
errWs.end();
|
|
247
|
+
} catch {}
|
|
207
248
|
maybeAutoShutdown();
|
|
208
249
|
});
|
|
209
250
|
|
|
@@ -213,7 +254,8 @@ function spawnMember(member) {
|
|
|
213
254
|
function sendInput(memberName, text) {
|
|
214
255
|
const state = processMap.get(memberName);
|
|
215
256
|
if (!state) return { ok: false, error: "member_not_found" };
|
|
216
|
-
if (state.status !== "running")
|
|
257
|
+
if (state.status !== "running")
|
|
258
|
+
return { ok: false, error: "member_not_running" };
|
|
217
259
|
try {
|
|
218
260
|
state.child.stdin.write(`${safeText(text)}\n`);
|
|
219
261
|
return { ok: true };
|
|
@@ -225,7 +267,8 @@ function sendInput(memberName, text) {
|
|
|
225
267
|
function interruptMember(memberName) {
|
|
226
268
|
const state = processMap.get(memberName);
|
|
227
269
|
if (!state) return { ok: false, error: "member_not_found" };
|
|
228
|
-
if (state.status !== "running")
|
|
270
|
+
if (state.status !== "running")
|
|
271
|
+
return { ok: false, error: "member_not_running" };
|
|
229
272
|
|
|
230
273
|
let signaled = false;
|
|
231
274
|
try {
|
|
@@ -250,7 +293,9 @@ let isShuttingDown = false;
|
|
|
250
293
|
|
|
251
294
|
function maybeAutoShutdown() {
|
|
252
295
|
if (isShuttingDown) return;
|
|
253
|
-
const allExited = [...processMap.values()].every(
|
|
296
|
+
const allExited = [...processMap.values()].every(
|
|
297
|
+
(s) => s.status === "exited",
|
|
298
|
+
);
|
|
254
299
|
if (!allExited) return;
|
|
255
300
|
shutdown();
|
|
256
301
|
}
|
|
@@ -261,11 +306,19 @@ async function shutdown() {
|
|
|
261
306
|
|
|
262
307
|
for (const state of processMap.values()) {
|
|
263
308
|
if (state.status === "running") {
|
|
264
|
-
try {
|
|
265
|
-
|
|
309
|
+
try {
|
|
310
|
+
state.child.stdin.write("exit\n");
|
|
311
|
+
} catch {}
|
|
312
|
+
try {
|
|
313
|
+
state.child.kill("SIGTERM");
|
|
314
|
+
} catch {}
|
|
266
315
|
}
|
|
267
|
-
try {
|
|
268
|
-
|
|
316
|
+
try {
|
|
317
|
+
state.outWs.end();
|
|
318
|
+
} catch {}
|
|
319
|
+
try {
|
|
320
|
+
state.errWs.end();
|
|
321
|
+
} catch {}
|
|
269
322
|
}
|
|
270
323
|
|
|
271
324
|
try {
|
|
@@ -278,9 +331,17 @@ async function shutdown() {
|
|
|
278
331
|
const pid = state.child?.pid;
|
|
279
332
|
if (process.platform === "win32" && Number.isInteger(pid) && pid > 0) {
|
|
280
333
|
// Windows: 프로세스 트리 전체 강제 종료 (손자 MCP 서버 포함)
|
|
281
|
-
try {
|
|
334
|
+
try {
|
|
335
|
+
execSyncSupervisor(`taskkill /T /F /PID ${pid}`, {
|
|
336
|
+
stdio: "pipe",
|
|
337
|
+
windowsHide: true,
|
|
338
|
+
timeout: 5000,
|
|
339
|
+
});
|
|
340
|
+
} catch {}
|
|
282
341
|
} else {
|
|
283
|
-
try {
|
|
342
|
+
try {
|
|
343
|
+
state.child.kill("SIGKILL");
|
|
344
|
+
} catch {}
|
|
284
345
|
}
|
|
285
346
|
}
|
|
286
347
|
}
|
|
@@ -321,7 +382,10 @@ const server = createServer(async (req, res) => {
|
|
|
321
382
|
let totalLen = 0;
|
|
322
383
|
for await (const c of req) {
|
|
323
384
|
totalLen += c.length;
|
|
324
|
-
if (totalLen > MAX_BODY) {
|
|
385
|
+
if (totalLen > MAX_BODY) {
|
|
386
|
+
send(413, { ok: false, error: "payload_too_large" });
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
325
389
|
chunks.push(c);
|
|
326
390
|
}
|
|
327
391
|
const raw = Buffer.concat(chunks).toString("utf8") || "{}";
|
package/hub/team/native.mjs
CHANGED
|
@@ -21,7 +21,8 @@ export const SCOUT_ROLE_CONFIG = {
|
|
|
21
21
|
readOnly: true,
|
|
22
22
|
};
|
|
23
23
|
const ROUTE_LOG_RE = /\[tfx-route\]/i;
|
|
24
|
-
const ROUTE_COMMAND_RE =
|
|
24
|
+
const ROUTE_COMMAND_RE =
|
|
25
|
+
/(?:^|[\s"'`])(?:bash\s+)?(?:[^"'`\s]*\/)?tfx-route\.sh\b/i;
|
|
25
26
|
const ROUTE_PROMPT_RE = /tfx-route\.sh/i;
|
|
26
27
|
const DIRECT_TOOL_BYPASS_RE = /\b(?:Read|Edit|Write)\s*\(/;
|
|
27
28
|
|
|
@@ -33,12 +34,14 @@ function inferWorkerIndex(agentName = "") {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
function buildRouteEnvPrefix(agentName, workerIndex, searchTool) {
|
|
36
|
-
const effectiveWorkerIndex =
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
const effectiveWorkerIndex =
|
|
38
|
+
Number.isInteger(workerIndex) && workerIndex > 0
|
|
39
|
+
? workerIndex
|
|
40
|
+
: inferWorkerIndex(agentName);
|
|
39
41
|
|
|
40
42
|
let envPrefix = "";
|
|
41
|
-
if (effectiveWorkerIndex)
|
|
43
|
+
if (effectiveWorkerIndex)
|
|
44
|
+
envPrefix += ` TFX_WORKER_INDEX="${effectiveWorkerIndex}"`;
|
|
42
45
|
if (searchTool) envPrefix += ` TFX_SEARCH_TOOL="${searchTool}"`;
|
|
43
46
|
return envPrefix;
|
|
44
47
|
}
|
|
@@ -91,7 +94,8 @@ export function verifySlimWrapperRouteExecution(input = {}) {
|
|
|
91
94
|
const sawDirectToolBypass = DIRECT_TOOL_BYPASS_RE.test(stdoutText);
|
|
92
95
|
const usedRoute = sawRouteCommand || sawRouteLog;
|
|
93
96
|
const expectedRouteInvocation = promptMentionsRoute;
|
|
94
|
-
const abnormal =
|
|
97
|
+
const abnormal =
|
|
98
|
+
expectedRouteInvocation && (sawDirectToolBypass || !usedRoute);
|
|
95
99
|
const reason = !abnormal
|
|
96
100
|
? null
|
|
97
101
|
: sawDirectToolBypass
|
|
@@ -123,14 +127,26 @@ function getRouteTimeout(role, _mcpProfile) {
|
|
|
123
127
|
// tfx-route.sh route_agent()의 DEFAULT_TIMEOUT 기반, 최소 1080초(18분) 보장.
|
|
124
128
|
// Bash timeout = 이 값 + 60초 여유. 짧은 역할도 네트워크/스케줄 지연 대비.
|
|
125
129
|
const TIMEOUTS = {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
"build-fixer": 1080,
|
|
131
|
+
debugger: 1080,
|
|
132
|
+
executor: 1080,
|
|
133
|
+
"deep-executor": 3600,
|
|
134
|
+
architect: 3600,
|
|
135
|
+
planner: 3600,
|
|
136
|
+
critic: 3600,
|
|
137
|
+
analyst: 3600,
|
|
138
|
+
scientist: 1800,
|
|
139
|
+
"scientist-deep": 3600,
|
|
140
|
+
"document-specialist": 1800,
|
|
141
|
+
"code-reviewer": 1800,
|
|
142
|
+
"security-reviewer": 1800,
|
|
143
|
+
"quality-reviewer": 1800,
|
|
144
|
+
verifier: 1800,
|
|
145
|
+
designer: 1080,
|
|
146
|
+
writer: 1080,
|
|
147
|
+
explore: 1080,
|
|
148
|
+
"test-engineer": 1080,
|
|
149
|
+
"qa-tester": 1080,
|
|
134
150
|
spark: 600,
|
|
135
151
|
};
|
|
136
152
|
return TIMEOUTS[role] || 1080;
|
|
@@ -176,16 +192,21 @@ export function buildSlimWrapperPrompt(cli, opts = {}) {
|
|
|
176
192
|
const escaped = subtask.replace(/'/g, "'\\''");
|
|
177
193
|
const pipelineHint = pipelinePhase
|
|
178
194
|
? `\n파이프라인 단계: ${pipelinePhase}`
|
|
179
|
-
: '';
|
|
180
|
-
const routeEnvPrefix = buildRouteEnvPrefix(agentName, workerIndex, searchTool);
|
|
181
|
-
const scoutConstraint = (role === "scout" || role === "scientist")
|
|
182
|
-
? "\n이 워커는 scout(탐색 전용)이다. 코드를 수정하거나 파일을 생성하지 마라. 기존 코드를 읽고 분석하여 보고만 하라."
|
|
183
195
|
: "";
|
|
196
|
+
const routeEnvPrefix = buildRouteEnvPrefix(
|
|
197
|
+
agentName,
|
|
198
|
+
workerIndex,
|
|
199
|
+
searchTool,
|
|
200
|
+
);
|
|
201
|
+
const scoutConstraint =
|
|
202
|
+
role === "scout" || role === "scientist"
|
|
203
|
+
? "\n이 워커는 scout(탐색 전용)이다. 코드를 수정하거나 파일을 생성하지 마라. 기존 코드를 읽고 분석하여 보고만 하라."
|
|
204
|
+
: "";
|
|
184
205
|
|
|
185
206
|
// Bash 도구 timeout (모두 600초 이내)
|
|
186
|
-
const launchTimeoutMs = 15000;
|
|
187
|
-
const waitTimeoutMs = 570000;
|
|
188
|
-
const resultTimeoutMs = 30000;
|
|
207
|
+
const launchTimeoutMs = 15000; // Step 1: fork + job_id 반환
|
|
208
|
+
const waitTimeoutMs = 570000; // Step 2: 내부 폴링 (540초 대기 + 여유)
|
|
209
|
+
const resultTimeoutMs = 30000; // Step 3: 결과 읽기
|
|
189
210
|
|
|
190
211
|
return `실행 프로토콜 (subagent_type="${SLIM_WRAPPER_SUBAGENT_TYPE}", async + feedback):
|
|
191
212
|
MAX_ITERATIONS = ${maxIterations}
|
|
@@ -252,9 +273,7 @@ SendMessage(type: "message", recipient: "${leadName}", content: "최종 완료:
|
|
|
252
273
|
*/
|
|
253
274
|
export function buildScoutDispatchPrompt(opts = {}) {
|
|
254
275
|
const { question, scope = "", teamName, taskId, agentName, leadName } = opts;
|
|
255
|
-
const subtask = scope
|
|
256
|
-
? `${question} 탐색 범위: ${scope}`
|
|
257
|
-
: question;
|
|
276
|
+
const subtask = scope ? `${question} 탐색 범위: ${scope}` : question;
|
|
258
277
|
return buildSlimWrapperPrompt("codex", {
|
|
259
278
|
subtask,
|
|
260
279
|
role: "scientist",
|
|
@@ -305,10 +324,16 @@ export function buildHybridWrapperPrompt(cli, opts = {}) {
|
|
|
305
324
|
} = opts;
|
|
306
325
|
|
|
307
326
|
const escaped = subtask.replace(/'/g, "'\\''");
|
|
308
|
-
const pipelineHint = pipelinePhase
|
|
327
|
+
const pipelineHint = pipelinePhase
|
|
328
|
+
? `\n파이프라인 단계: ${pipelinePhase}`
|
|
329
|
+
: "";
|
|
309
330
|
const taskIdRef = taskId ? `taskId: "${taskId}"` : "";
|
|
310
331
|
const taskIdArg = taskIdRef ? `${taskIdRef}, ` : "";
|
|
311
|
-
const routeEnvPrefix = buildRouteEnvPrefix(
|
|
332
|
+
const routeEnvPrefix = buildRouteEnvPrefix(
|
|
333
|
+
agentName,
|
|
334
|
+
workerIndex,
|
|
335
|
+
searchTool,
|
|
336
|
+
);
|
|
312
337
|
|
|
313
338
|
const routeCmd = `TFX_TEAM_NAME="${teamName}" TFX_TEAM_TASK_ID="${taskId}" TFX_TEAM_AGENT_NAME="${agentName}" TFX_TEAM_LEAD_NAME="${leadName}"${routeEnvPrefix} bash ${ROUTE_SCRIPT} "${role}" '${escaped}' ${mcp_profile}`;
|
|
314
339
|
|
|
@@ -374,7 +399,12 @@ export async function pollTeamResults(teamName, expectedTaskIds = []) {
|
|
|
374
399
|
return { completed: [], pending: normalizedTaskIds };
|
|
375
400
|
}
|
|
376
401
|
|
|
377
|
-
const resultDir = path.join(
|
|
402
|
+
const resultDir = path.join(
|
|
403
|
+
os.homedir(),
|
|
404
|
+
".claude",
|
|
405
|
+
"tfx-results",
|
|
406
|
+
normalizedTeamName,
|
|
407
|
+
);
|
|
378
408
|
|
|
379
409
|
let entries;
|
|
380
410
|
try {
|
|
@@ -387,9 +417,7 @@ export async function pollTeamResults(teamName, expectedTaskIds = []) {
|
|
|
387
417
|
}
|
|
388
418
|
|
|
389
419
|
const availableFiles = new Set(
|
|
390
|
-
entries
|
|
391
|
-
.filter((entry) => entry.isFile())
|
|
392
|
-
.map((entry) => entry.name),
|
|
420
|
+
entries.filter((entry) => entry.isFile()).map((entry) => entry.name),
|
|
393
421
|
);
|
|
394
422
|
|
|
395
423
|
const completedCandidates = await Promise.all(
|
|
@@ -418,7 +446,9 @@ export async function pollTeamResults(teamName, expectedTaskIds = []) {
|
|
|
418
446
|
|
|
419
447
|
const completed = completedCandidates.filter(Boolean);
|
|
420
448
|
const completedTaskIds = new Set(completed.map((item) => item.taskId));
|
|
421
|
-
const pending = normalizedTaskIds.filter(
|
|
449
|
+
const pending = normalizedTaskIds.filter(
|
|
450
|
+
(taskId) => !completedTaskIds.has(taskId),
|
|
451
|
+
);
|
|
422
452
|
|
|
423
453
|
return { completed, pending };
|
|
424
454
|
}
|
|
@@ -430,7 +460,9 @@ export async function pollTeamResults(teamName, expectedTaskIds = []) {
|
|
|
430
460
|
* @returns {string}
|
|
431
461
|
*/
|
|
432
462
|
export function formatPollReport(pollResult = {}) {
|
|
433
|
-
const completed = Array.isArray(pollResult.completed)
|
|
463
|
+
const completed = Array.isArray(pollResult.completed)
|
|
464
|
+
? pollResult.completed
|
|
465
|
+
: [];
|
|
434
466
|
const pending = Array.isArray(pollResult.pending) ? pollResult.pending : [];
|
|
435
467
|
const total = completed.length + pending.length;
|
|
436
468
|
|
|
@@ -453,7 +485,10 @@ export function formatPollReport(pollResult = {}) {
|
|
|
453
485
|
* @returns {string}
|
|
454
486
|
*/
|
|
455
487
|
function normalizeText(s) {
|
|
456
|
-
return String(s || "")
|
|
488
|
+
return String(s || "")
|
|
489
|
+
.toLowerCase()
|
|
490
|
+
.replace(/\s+/g, " ")
|
|
491
|
+
.trim();
|
|
457
492
|
}
|
|
458
493
|
|
|
459
494
|
/**
|
|
@@ -523,7 +558,8 @@ export function compressScoutReport(rawReport) {
|
|
|
523
558
|
const text = String(rawReport || "");
|
|
524
559
|
|
|
525
560
|
// 파일:라인 패턴 추출 (path/to/file.ext:123 형태 + 뒤따르는 설명)
|
|
526
|
-
const fileLineRe =
|
|
561
|
+
const fileLineRe =
|
|
562
|
+
/([a-zA-Z0-9_./-]+\.[a-zA-Z]{1,10}):(\d+)\s*[:\-–—]?\s*(.+)/g;
|
|
527
563
|
const findings = [];
|
|
528
564
|
let match;
|
|
529
565
|
while ((match = fileLineRe.exec(text)) !== null) {
|
|
@@ -546,7 +582,10 @@ export function compressScoutReport(rawReport) {
|
|
|
546
582
|
|
|
547
583
|
// findings를 먼저 계산하고, 남은 공간에 맞춰 summary를 자른다
|
|
548
584
|
const findingsJson = JSON.stringify(findings);
|
|
549
|
-
const findingsBudget = Math.min(
|
|
585
|
+
const findingsBudget = Math.min(
|
|
586
|
+
findingsJson.length,
|
|
587
|
+
Math.floor(MAX_CHARS * 0.3),
|
|
588
|
+
);
|
|
550
589
|
|
|
551
590
|
let trimmedFindings = findings;
|
|
552
591
|
if (findingsJson.length > findingsBudget && findings.length > 0) {
|