@triflux/remote 10.1.0 → 10.26.0
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 +2179 -709
- 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 +78 -17
- package/hub/team/build-worker-prompt.mjs +73 -0
- package/hub/team/check-mcp-hub.mjs +49 -0
- package/hub/team/claude-daemon-control.mjs +1130 -0
- package/hub/team/claude-native-bridge.mjs +1011 -0
- package/hub/team/claude-session-projection.mjs +74 -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 +151 -33
- package/hub/team/cli/commands/start/parse-args.mjs +97 -13
- package/hub/team/cli/commands/start/start-headless.mjs +155 -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/help.mjs +1 -1
- 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 +61 -4
- 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 +256 -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 +2213 -1156
- package/hub/team/health-probe.mjs +232 -69
- package/hub/team/interactive-native-launcher.mjs +106 -0
- package/hub/team/interactive-native-worker.mjs +80 -0
- package/hub/team/interactive-tui-transport.mjs +465 -0
- package/hub/team/launcher-template.mjs +49 -17
- 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 +120 -42
- 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 -20
- package/hub/team/process-cleanup.mjs +69 -25
- package/hub/team/psmux.mjs +1022 -281
- 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 +336 -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 +574 -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 +1544 -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 +319 -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 +292 -0
- package/hub/team/tui-lite.mjs +237 -148
- package/hub/team/tui-remote-adapter.mjs +18 -34
- package/hub/team/tui-synapse.mjs +223 -0
- package/hub/team/tui-viewer.mjs +108 -48
- package/hub/team/tui-widgets.mjs +262 -0
- package/hub/team/tui.mjs +931 -439
- package/hub/team/worker-completion-validator.mjs +84 -0
- package/hub/team/worker-sandbox.mjs +97 -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 +477 -252
- package/hub/tray.mjs +411 -368
- package/hub/workers/antigravity-route-worker.mjs +169 -0
- 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 +570 -499
- package/hub/workers/factory.mjs +103 -21
- package/hub/workers/gemini-worker.mjs +126 -284
- package/hub/workers/interface.mjs +24 -2
- package/hub/workers/lib/jsonrpc-stdio.mjs +463 -0
- package/hub/workers/worker-utils.mjs +23 -17
- 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 +37 -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-health-check.mjs +159 -0
- package/scripts/lib/mcp-gateway-servers.mjs +70 -0
- package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -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 +224 -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
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
// teammate 프롬프트 템플릿 + 팀 설정 빌더
|
|
3
3
|
//
|
|
4
4
|
// Claude Code 네이티브 Agent Teams (CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1)
|
|
5
|
-
// 환경에서 teammate를 Codex/
|
|
5
|
+
// 환경에서 teammate를 Codex/Antigravity CLI 래퍼로 구성하는 유틸리티.
|
|
6
6
|
// SKILL.md가 인라인 프롬프트를 사용하므로, 이 모듈은 CLI(tfx multi --native)에서
|
|
7
7
|
// 팀 설정을 프로그래밍적으로 생성할 때 사용한다.
|
|
8
8
|
|
|
9
9
|
import * as fs from "node:fs/promises";
|
|
10
|
+
import { createRequire } from "node:module";
|
|
10
11
|
import os from "node:os";
|
|
11
12
|
import path from "node:path";
|
|
12
13
|
|
|
@@ -21,9 +22,13 @@ export const SCOUT_ROLE_CONFIG = {
|
|
|
21
22
|
readOnly: true,
|
|
22
23
|
};
|
|
23
24
|
const ROUTE_LOG_RE = /\[tfx-route\]/i;
|
|
24
|
-
const ROUTE_COMMAND_RE =
|
|
25
|
+
const ROUTE_COMMAND_RE =
|
|
26
|
+
/(?:^|[\s"'`])(?:bash\s+)?(?:[^"'`\s]*\/)?tfx-route\.sh\b/i;
|
|
25
27
|
const ROUTE_PROMPT_RE = /tfx-route\.sh/i;
|
|
26
28
|
const DIRECT_TOOL_BYPASS_RE = /\b(?:Read|Edit|Write)\s*\(/;
|
|
29
|
+
const _require = createRequire(import.meta.url);
|
|
30
|
+
const AGENT_TO_CLI = _require("./agent-map.json");
|
|
31
|
+
const VALID_ROUTE_AGENTS = new Set(Object.keys(AGENT_TO_CLI));
|
|
27
32
|
|
|
28
33
|
function inferWorkerIndex(agentName = "") {
|
|
29
34
|
const match = /(\d+)(?!.*\d)/.exec(agentName);
|
|
@@ -32,13 +37,46 @@ function inferWorkerIndex(agentName = "") {
|
|
|
32
37
|
return Number.isInteger(index) && index > 0 ? index : null;
|
|
33
38
|
}
|
|
34
39
|
|
|
35
|
-
function
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
function normalizeRouteMode(cli) {
|
|
41
|
+
const normalized = String(cli || "").trim();
|
|
42
|
+
if (["gemini", "antigravity", "agy"].includes(normalized)) {
|
|
43
|
+
return "antigravity";
|
|
44
|
+
}
|
|
45
|
+
if (normalized === "codex") return "codex";
|
|
46
|
+
return "";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveRouteCli(cli) {
|
|
50
|
+
const normalized = String(cli || "").trim();
|
|
51
|
+
const mapped = AGENT_TO_CLI[normalized] || normalized;
|
|
52
|
+
return mapped === "gemini" ? "antigravity" : mapped;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolveSlimWrapperRouteAgent(cli, role) {
|
|
56
|
+
const routeRole = String(role || "").trim();
|
|
57
|
+
if (
|
|
58
|
+
routeRole &&
|
|
59
|
+
VALID_ROUTE_AGENTS.has(routeRole) &&
|
|
60
|
+
!["gemini", "antigravity", "agy"].includes(routeRole)
|
|
61
|
+
) {
|
|
62
|
+
return routeRole;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const resolvedCli = resolveRouteCli(cli);
|
|
66
|
+
return VALID_ROUTE_AGENTS.has(resolvedCli) ? resolvedCli : "codex";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildRouteEnvPrefix(agentName, workerIndex, searchTool, cli) {
|
|
70
|
+
const effectiveWorkerIndex =
|
|
71
|
+
Number.isInteger(workerIndex) && workerIndex > 0
|
|
72
|
+
? workerIndex
|
|
73
|
+
: inferWorkerIndex(agentName);
|
|
74
|
+
const routeMode = normalizeRouteMode(cli);
|
|
39
75
|
|
|
40
76
|
let envPrefix = "";
|
|
41
|
-
if (
|
|
77
|
+
if (routeMode) envPrefix += ` TFX_CLI_MODE="${routeMode}"`;
|
|
78
|
+
if (effectiveWorkerIndex)
|
|
79
|
+
envPrefix += ` TFX_WORKER_INDEX="${effectiveWorkerIndex}"`;
|
|
42
80
|
if (searchTool) envPrefix += ` TFX_SEARCH_TOOL="${searchTool}"`;
|
|
43
81
|
return envPrefix;
|
|
44
82
|
}
|
|
@@ -91,7 +129,8 @@ export function verifySlimWrapperRouteExecution(input = {}) {
|
|
|
91
129
|
const sawDirectToolBypass = DIRECT_TOOL_BYPASS_RE.test(stdoutText);
|
|
92
130
|
const usedRoute = sawRouteCommand || sawRouteLog;
|
|
93
131
|
const expectedRouteInvocation = promptMentionsRoute;
|
|
94
|
-
const abnormal =
|
|
132
|
+
const abnormal =
|
|
133
|
+
expectedRouteInvocation && (sawDirectToolBypass || !usedRoute);
|
|
95
134
|
const reason = !abnormal
|
|
96
135
|
? null
|
|
97
136
|
: sawDirectToolBypass
|
|
@@ -123,14 +162,26 @@ function getRouteTimeout(role, _mcpProfile) {
|
|
|
123
162
|
// tfx-route.sh route_agent()의 DEFAULT_TIMEOUT 기반, 최소 1080초(18분) 보장.
|
|
124
163
|
// Bash timeout = 이 값 + 60초 여유. 짧은 역할도 네트워크/스케줄 지연 대비.
|
|
125
164
|
const TIMEOUTS = {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
165
|
+
"build-fixer": 1080,
|
|
166
|
+
debugger: 1080,
|
|
167
|
+
executor: 1080,
|
|
168
|
+
"deep-executor": 3600,
|
|
169
|
+
architect: 3600,
|
|
170
|
+
planner: 3600,
|
|
171
|
+
critic: 3600,
|
|
172
|
+
analyst: 3600,
|
|
173
|
+
scientist: 1800,
|
|
174
|
+
"scientist-deep": 3600,
|
|
175
|
+
"document-specialist": 1800,
|
|
176
|
+
"code-reviewer": 1800,
|
|
177
|
+
"security-reviewer": 1800,
|
|
178
|
+
"quality-reviewer": 1800,
|
|
179
|
+
verifier: 1800,
|
|
180
|
+
designer: 1080,
|
|
181
|
+
writer: 1080,
|
|
182
|
+
explore: 1080,
|
|
183
|
+
"test-engineer": 1080,
|
|
184
|
+
"qa-tester": 1080,
|
|
134
185
|
spark: 600,
|
|
135
186
|
};
|
|
136
187
|
return TIMEOUTS[role] || 1080;
|
|
@@ -172,20 +223,27 @@ export function buildSlimWrapperPrompt(cli, opts = {}) {
|
|
|
172
223
|
maxIterations = 3,
|
|
173
224
|
} = opts;
|
|
174
225
|
|
|
175
|
-
const
|
|
226
|
+
const routeAgent = resolveSlimWrapperRouteAgent(cli, role);
|
|
227
|
+
const routeTimeoutSec = getRouteTimeout(routeAgent, mcp_profile);
|
|
176
228
|
const escaped = subtask.replace(/'/g, "'\\''");
|
|
177
229
|
const pipelineHint = pipelinePhase
|
|
178
230
|
? `\n파이프라인 단계: ${pipelinePhase}`
|
|
179
|
-
: '';
|
|
180
|
-
const routeEnvPrefix = buildRouteEnvPrefix(agentName, workerIndex, searchTool);
|
|
181
|
-
const scoutConstraint = (role === "scout" || role === "scientist")
|
|
182
|
-
? "\n이 워커는 scout(탐색 전용)이다. 코드를 수정하거나 파일을 생성하지 마라. 기존 코드를 읽고 분석하여 보고만 하라."
|
|
183
231
|
: "";
|
|
232
|
+
const routeEnvPrefix = buildRouteEnvPrefix(
|
|
233
|
+
agentName,
|
|
234
|
+
workerIndex,
|
|
235
|
+
searchTool,
|
|
236
|
+
cli,
|
|
237
|
+
);
|
|
238
|
+
const scoutConstraint =
|
|
239
|
+
role === "scout" || role === "scientist"
|
|
240
|
+
? "\n이 워커는 scout(탐색 전용)이다. 코드를 수정하거나 파일을 생성하지 마라. 기존 코드를 읽고 분석하여 보고만 하라."
|
|
241
|
+
: "";
|
|
184
242
|
|
|
185
243
|
// Bash 도구 timeout (모두 600초 이내)
|
|
186
|
-
const launchTimeoutMs = 15000;
|
|
187
|
-
const waitTimeoutMs = 570000;
|
|
188
|
-
const resultTimeoutMs = 30000;
|
|
244
|
+
const launchTimeoutMs = 15000; // Step 1: fork + job_id 반환
|
|
245
|
+
const waitTimeoutMs = 570000; // Step 2: 내부 폴링 (540초 대기 + 여유)
|
|
246
|
+
const resultTimeoutMs = 30000; // Step 3: 결과 읽기
|
|
189
247
|
|
|
190
248
|
return `실행 프로토콜 (subagent_type="${SLIM_WRAPPER_SUBAGENT_TYPE}", async + feedback):
|
|
191
249
|
MAX_ITERATIONS = ${maxIterations}
|
|
@@ -197,14 +255,14 @@ SendMessage(type: "message", recipient: "${leadName}", content: "작업 시작:
|
|
|
197
255
|
|
|
198
256
|
[HARD CONSTRAINT] 허용 도구: Bash, TaskUpdate, TaskGet, TaskList, SendMessage만 사용한다.
|
|
199
257
|
Read, Edit, Write, Grep, Glob, Agent, WebSearch, WebFetch 등 다른 모든 도구 사용을 금지한다.
|
|
200
|
-
코드를 직접 읽거나 수정하면 안 된다. 반드시 아래 Bash 명령(tfx-route.sh)을 통해 Codex/
|
|
258
|
+
코드를 직접 읽거나 수정하면 안 된다. 반드시 아래 Bash 명령(tfx-route.sh)을 통해 Codex/Antigravity에 위임하라.
|
|
201
259
|
이 규칙을 위반하면 작업 실패로 간주한다.${scoutConstraint}
|
|
202
260
|
|
|
203
261
|
gemini/codex를 직접 호출하지 마라. 반드시 tfx-route.sh를 거쳐야 한다.
|
|
204
262
|
프롬프트를 파일로 저장하지 마라. tfx-route.sh가 인자로 받는다.
|
|
205
263
|
|
|
206
264
|
Step 1 — Async 시작 (즉시 리턴, <1초):
|
|
207
|
-
Bash(command: 'TFX_TEAM_NAME="${teamName}" TFX_TEAM_TASK_ID="${taskId}" TFX_TEAM_AGENT_NAME="${agentName}" TFX_TEAM_LEAD_NAME="${leadName}"${routeEnvPrefix} bash ${ROUTE_SCRIPT} --async "${
|
|
265
|
+
Bash(command: 'TFX_TEAM_NAME="${teamName}" TFX_TEAM_TASK_ID="${taskId}" TFX_TEAM_AGENT_NAME="${agentName}" TFX_TEAM_LEAD_NAME="${leadName}"${routeEnvPrefix} bash ${ROUTE_SCRIPT} --async "${routeAgent}" '"'"'${escaped}'"'"' ${mcp_profile} ${routeTimeoutSec}', timeout: ${launchTimeoutMs})
|
|
208
266
|
→ 출력 한 줄이 JOB_ID이다. 반드시 기억하라.
|
|
209
267
|
|
|
210
268
|
Step 2 — 완료 대기 (내부 폴링, 최대 540초):
|
|
@@ -252,9 +310,7 @@ SendMessage(type: "message", recipient: "${leadName}", content: "최종 완료:
|
|
|
252
310
|
*/
|
|
253
311
|
export function buildScoutDispatchPrompt(opts = {}) {
|
|
254
312
|
const { question, scope = "", teamName, taskId, agentName, leadName } = opts;
|
|
255
|
-
const subtask = scope
|
|
256
|
-
? `${question} 탐색 범위: ${scope}`
|
|
257
|
-
: question;
|
|
313
|
+
const subtask = scope ? `${question} 탐색 범위: ${scope}` : question;
|
|
258
314
|
return buildSlimWrapperPrompt("codex", {
|
|
259
315
|
subtask,
|
|
260
316
|
role: "scientist",
|
|
@@ -305,12 +361,20 @@ export function buildHybridWrapperPrompt(cli, opts = {}) {
|
|
|
305
361
|
} = opts;
|
|
306
362
|
|
|
307
363
|
const escaped = subtask.replace(/'/g, "'\\''");
|
|
308
|
-
const pipelineHint = pipelinePhase
|
|
364
|
+
const pipelineHint = pipelinePhase
|
|
365
|
+
? `\n파이프라인 단계: ${pipelinePhase}`
|
|
366
|
+
: "";
|
|
309
367
|
const taskIdRef = taskId ? `taskId: "${taskId}"` : "";
|
|
310
368
|
const taskIdArg = taskIdRef ? `${taskIdRef}, ` : "";
|
|
311
|
-
const routeEnvPrefix = buildRouteEnvPrefix(
|
|
369
|
+
const routeEnvPrefix = buildRouteEnvPrefix(
|
|
370
|
+
agentName,
|
|
371
|
+
workerIndex,
|
|
372
|
+
searchTool,
|
|
373
|
+
cli,
|
|
374
|
+
);
|
|
375
|
+
const routeAgent = resolveSlimWrapperRouteAgent(cli, role);
|
|
312
376
|
|
|
313
|
-
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} "${
|
|
377
|
+
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} "${routeAgent}" '${escaped}' ${mcp_profile}`;
|
|
314
378
|
|
|
315
379
|
return `하이브리드 psmux 워커 프로토콜:
|
|
316
380
|
|
|
@@ -335,7 +399,7 @@ export function buildHybridWrapperPrompt(cli, opts = {}) {
|
|
|
335
399
|
${pipelineHint}
|
|
336
400
|
[HARD CONSTRAINT] 너는 Bash, TaskUpdate, TaskGet, TaskList, SendMessage만 사용할 수 있다.
|
|
337
401
|
Read, Edit, Write, Grep, Glob, Agent, WebSearch, WebFetch 등 다른 모든 도구 사용을 금지한다.
|
|
338
|
-
코드를 직접 읽거나 수정하면 안 된다. 반드시 아래 Bash 명령(tfx-route.sh)을 통해 Codex/
|
|
402
|
+
코드를 직접 읽거나 수정하면 안 된다. 반드시 아래 Bash 명령(tfx-route.sh)을 통해 Codex/Antigravity에 위임하라.
|
|
339
403
|
이 규칙을 위반하면 작업 실패로 간주한다.
|
|
340
404
|
|
|
341
405
|
gemini/codex를 직접 호출하지 마라. psmux spawn이 tfx-route.sh를 통해 실행한다.
|
|
@@ -374,7 +438,12 @@ export async function pollTeamResults(teamName, expectedTaskIds = []) {
|
|
|
374
438
|
return { completed: [], pending: normalizedTaskIds };
|
|
375
439
|
}
|
|
376
440
|
|
|
377
|
-
const resultDir = path.join(
|
|
441
|
+
const resultDir = path.join(
|
|
442
|
+
os.homedir(),
|
|
443
|
+
".claude",
|
|
444
|
+
"tfx-results",
|
|
445
|
+
normalizedTeamName,
|
|
446
|
+
);
|
|
378
447
|
|
|
379
448
|
let entries;
|
|
380
449
|
try {
|
|
@@ -387,9 +456,7 @@ export async function pollTeamResults(teamName, expectedTaskIds = []) {
|
|
|
387
456
|
}
|
|
388
457
|
|
|
389
458
|
const availableFiles = new Set(
|
|
390
|
-
entries
|
|
391
|
-
.filter((entry) => entry.isFile())
|
|
392
|
-
.map((entry) => entry.name),
|
|
459
|
+
entries.filter((entry) => entry.isFile()).map((entry) => entry.name),
|
|
393
460
|
);
|
|
394
461
|
|
|
395
462
|
const completedCandidates = await Promise.all(
|
|
@@ -418,7 +485,9 @@ export async function pollTeamResults(teamName, expectedTaskIds = []) {
|
|
|
418
485
|
|
|
419
486
|
const completed = completedCandidates.filter(Boolean);
|
|
420
487
|
const completedTaskIds = new Set(completed.map((item) => item.taskId));
|
|
421
|
-
const pending = normalizedTaskIds.filter(
|
|
488
|
+
const pending = normalizedTaskIds.filter(
|
|
489
|
+
(taskId) => !completedTaskIds.has(taskId),
|
|
490
|
+
);
|
|
422
491
|
|
|
423
492
|
return { completed, pending };
|
|
424
493
|
}
|
|
@@ -430,7 +499,9 @@ export async function pollTeamResults(teamName, expectedTaskIds = []) {
|
|
|
430
499
|
* @returns {string}
|
|
431
500
|
*/
|
|
432
501
|
export function formatPollReport(pollResult = {}) {
|
|
433
|
-
const completed = Array.isArray(pollResult.completed)
|
|
502
|
+
const completed = Array.isArray(pollResult.completed)
|
|
503
|
+
? pollResult.completed
|
|
504
|
+
: [];
|
|
434
505
|
const pending = Array.isArray(pollResult.pending) ? pollResult.pending : [];
|
|
435
506
|
const total = completed.length + pending.length;
|
|
436
507
|
|
|
@@ -453,7 +524,10 @@ export function formatPollReport(pollResult = {}) {
|
|
|
453
524
|
* @returns {string}
|
|
454
525
|
*/
|
|
455
526
|
function normalizeText(s) {
|
|
456
|
-
return String(s || "")
|
|
527
|
+
return String(s || "")
|
|
528
|
+
.toLowerCase()
|
|
529
|
+
.replace(/\s+/g, " ")
|
|
530
|
+
.trim();
|
|
457
531
|
}
|
|
458
532
|
|
|
459
533
|
/**
|
|
@@ -523,7 +597,8 @@ export function compressScoutReport(rawReport) {
|
|
|
523
597
|
const text = String(rawReport || "");
|
|
524
598
|
|
|
525
599
|
// 파일:라인 패턴 추출 (path/to/file.ext:123 형태 + 뒤따르는 설명)
|
|
526
|
-
const fileLineRe =
|
|
600
|
+
const fileLineRe =
|
|
601
|
+
/([a-zA-Z0-9_./-]+\.[a-zA-Z]{1,10}):(\d+)\s*[:\-–—]?\s*(.+)/g;
|
|
527
602
|
const findings = [];
|
|
528
603
|
let match;
|
|
529
604
|
while ((match = fileLineRe.exec(text)) !== null) {
|
|
@@ -546,7 +621,10 @@ export function compressScoutReport(rawReport) {
|
|
|
546
621
|
|
|
547
622
|
// findings를 먼저 계산하고, 남은 공간에 맞춰 summary를 자른다
|
|
548
623
|
const findingsJson = JSON.stringify(findings);
|
|
549
|
-
const findingsBudget = Math.min(
|
|
624
|
+
const findingsBudget = Math.min(
|
|
625
|
+
findingsJson.length,
|
|
626
|
+
Math.floor(MAX_CHARS * 0.3),
|
|
627
|
+
);
|
|
550
628
|
|
|
551
629
|
let trimmedFindings = findings;
|
|
552
630
|
if (findingsJson.length > findingsBudget && findings.length > 0) {
|