triflux 3.3.0-dev.1 → 3.3.0-dev.5
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/bin/triflux.mjs +169 -39
- package/hooks/hooks.json +5 -0
- package/hub/assign-callbacks.mjs +136 -0
- package/hub/bridge.mjs +283 -97
- package/hub/pipe.mjs +104 -0
- package/hub/router.mjs +322 -1
- package/hub/schema.sql +40 -7
- package/hub/server.mjs +151 -53
- package/hub/store.mjs +293 -1
- package/hub/team/cli-team-status.mjs +17 -3
- package/hub/team/native-supervisor.mjs +62 -22
- package/hub/team/native.mjs +86 -10
- package/hub/team/psmux.mjs +555 -115
- package/hub/tools.mjs +101 -26
- package/hub/workers/delegator-mcp.mjs +1045 -0
- package/hub/workers/factory.mjs +3 -0
- package/hub/workers/interface.mjs +2 -2
- package/hud/hud-qos-status.mjs +1735 -1790
- package/package.json +60 -60
- package/scripts/__tests__/keyword-detector.test.mjs +3 -3
- package/scripts/__tests__/smoke.test.mjs +34 -0
- package/scripts/hub-ensure.mjs +21 -3
- package/scripts/lib/mcp-filter.mjs +637 -0
- package/scripts/lib/mcp-server-catalog.mjs +118 -0
- package/scripts/mcp-check.mjs +126 -88
- package/scripts/setup.mjs +15 -10
- package/scripts/test-tfx-route-no-claude-native.mjs +10 -2
- package/scripts/tfx-route.sh +434 -179
|
@@ -54,6 +54,21 @@ function renderTasks(tasks = []) {
|
|
|
54
54
|
console.log("");
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
function formatCompletionSuffix(member) {
|
|
58
|
+
if (!member?.completionStatus) return "";
|
|
59
|
+
if (member.completionStatus === "abnormal") {
|
|
60
|
+
const reason = member.completionReason || "unknown";
|
|
61
|
+
return ` ${RED}[abnormal:${reason}]${RESET}`;
|
|
62
|
+
}
|
|
63
|
+
if (member.completionStatus === "normal") {
|
|
64
|
+
return ` ${GREEN}[route-ok]${RESET}`;
|
|
65
|
+
}
|
|
66
|
+
if (member.completionStatus === "unchecked") {
|
|
67
|
+
return ` ${GRAY}[route-unchecked]${RESET}`;
|
|
68
|
+
}
|
|
69
|
+
return "";
|
|
70
|
+
}
|
|
71
|
+
|
|
57
72
|
export async function teamStatus() {
|
|
58
73
|
const state = loadTeamState();
|
|
59
74
|
if (!state) {
|
|
@@ -91,7 +106,7 @@ export async function teamStatus() {
|
|
|
91
106
|
if (nativeMembers.length) {
|
|
92
107
|
console.log("");
|
|
93
108
|
for (const m of nativeMembers) {
|
|
94
|
-
console.log(` • ${m.name}: ${m.status}${m.lastPreview ? ` ${DIM}${m.lastPreview}${RESET}` : ""}`);
|
|
109
|
+
console.log(` • ${m.name}: ${m.status}${formatCompletionSuffix(m)}${m.lastPreview ? ` ${DIM}${m.lastPreview}${RESET}` : ""}`);
|
|
95
110
|
}
|
|
96
111
|
}
|
|
97
112
|
}
|
|
@@ -216,7 +231,7 @@ export async function teamDebug() {
|
|
|
216
231
|
console.log(` ${DIM}(no data)${RESET}`);
|
|
217
232
|
} else {
|
|
218
233
|
for (const m of members) {
|
|
219
|
-
console.log(` - ${m.name}: ${m.status}${m.lastPreview ? ` ${DIM}${m.lastPreview}${RESET}` : ""}`);
|
|
234
|
+
console.log(` - ${m.name}: ${m.status}${formatCompletionSuffix(m)}${m.lastPreview ? ` ${DIM}${m.lastPreview}${RESET}` : ""}`);
|
|
220
235
|
}
|
|
221
236
|
}
|
|
222
237
|
console.log("");
|
|
@@ -266,4 +281,3 @@ export function teamList() {
|
|
|
266
281
|
}
|
|
267
282
|
console.log("");
|
|
268
283
|
}
|
|
269
|
-
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// hub/team/native-supervisor.mjs — tmux 없이 멀티 CLI를 직접 띄우는 네이티브 팀 런타임
|
|
2
|
-
import { createServer } from "node:http";
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
|
-
import { mkdirSync, readFileSync, writeFileSync, createWriteStream } from "node:fs";
|
|
5
|
-
import { dirname, join } from "node:path";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { mkdirSync, readFileSync, writeFileSync, createWriteStream } from "node:fs";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { verifySlimWrapperRouteExecution } from "./native.mjs";
|
|
7
|
+
|
|
8
|
+
const ROUTE_LOG_TAIL_BYTES = 65536;
|
|
6
9
|
|
|
7
10
|
function parseArgs(argv) {
|
|
8
11
|
const out = {};
|
|
@@ -19,10 +22,43 @@ async function readJson(path) {
|
|
|
19
22
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
function safeText(v, fallback = "") {
|
|
23
|
-
if (v == null) return fallback;
|
|
24
|
-
return String(v);
|
|
25
|
-
}
|
|
25
|
+
function safeText(v, fallback = "") {
|
|
26
|
+
if (v == null) return fallback;
|
|
27
|
+
return String(v);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readTailText(path, maxBytes = ROUTE_LOG_TAIL_BYTES) {
|
|
31
|
+
try {
|
|
32
|
+
const raw = readFileSync(path, "utf8");
|
|
33
|
+
if (raw.length <= maxBytes) return raw;
|
|
34
|
+
return raw.slice(-maxBytes);
|
|
35
|
+
} catch {
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function finalizeRouteVerification(state) {
|
|
41
|
+
if (state?.member?.role !== "worker") return;
|
|
42
|
+
|
|
43
|
+
const verification = verifySlimWrapperRouteExecution({
|
|
44
|
+
promptText: safeText(state.member?.prompt),
|
|
45
|
+
stdoutText: readTailText(state.logFile),
|
|
46
|
+
stderrText: readTailText(state.errFile),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
state.routeVerification = verification;
|
|
50
|
+
if (!verification.expectedRouteInvocation) {
|
|
51
|
+
state.completionStatus = "unchecked";
|
|
52
|
+
state.completionReason = null;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
state.completionStatus = verification.abnormal ? "abnormal" : "normal";
|
|
57
|
+
state.completionReason = verification.reason;
|
|
58
|
+
if (verification.abnormal) {
|
|
59
|
+
state.lastPreview = "[abnormal] tfx-route.sh evidence missing";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
26
62
|
|
|
27
63
|
function nowMs() {
|
|
28
64
|
return Date.now();
|
|
@@ -60,13 +96,16 @@ function memberStateSnapshot() {
|
|
|
60
96
|
agentId: m.agentId,
|
|
61
97
|
command: m.command,
|
|
62
98
|
pid: state?.child?.pid || null,
|
|
63
|
-
status: state?.status || "unknown",
|
|
64
|
-
exitCode: state?.exitCode ?? null,
|
|
65
|
-
lastPreview: state?.lastPreview || "",
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
99
|
+
status: state?.status || "unknown",
|
|
100
|
+
exitCode: state?.exitCode ?? null,
|
|
101
|
+
lastPreview: state?.lastPreview || "",
|
|
102
|
+
completionStatus: state?.completionStatus || null,
|
|
103
|
+
completionReason: state?.completionReason || null,
|
|
104
|
+
routeVerification: state?.routeVerification || null,
|
|
105
|
+
logFile: state?.logFile || null,
|
|
106
|
+
errFile: state?.errFile || null,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
70
109
|
return states;
|
|
71
110
|
}
|
|
72
111
|
|
|
@@ -128,13 +167,14 @@ function spawnMember(member) {
|
|
|
128
167
|
}
|
|
129
168
|
});
|
|
130
169
|
|
|
131
|
-
child.on("exit", (code) => {
|
|
132
|
-
state.status = "exited";
|
|
133
|
-
state.exitCode = code;
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
|
|
137
|
-
|
|
170
|
+
child.on("exit", (code) => {
|
|
171
|
+
state.status = "exited";
|
|
172
|
+
state.exitCode = code;
|
|
173
|
+
finalizeRouteVerification(state);
|
|
174
|
+
try { outWs.end(); } catch {}
|
|
175
|
+
try { errWs.end(); } catch {}
|
|
176
|
+
maybeAutoShutdown();
|
|
177
|
+
});
|
|
138
178
|
|
|
139
179
|
processMap.set(member.name, state);
|
|
140
180
|
}
|
package/hub/team/native.mjs
CHANGED
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
// 팀 설정을 프로그래밍적으로 생성할 때 사용한다.
|
|
8
8
|
|
|
9
9
|
const ROUTE_SCRIPT = "~/.claude/scripts/tfx-route.sh";
|
|
10
|
+
export const SLIM_WRAPPER_SUBAGENT_TYPE = "slim-wrapper";
|
|
11
|
+
const ROUTE_LOG_RE = /\[tfx-route\]/i;
|
|
12
|
+
const ROUTE_COMMAND_RE = /(?:^|[\s"'`])(?:bash\s+)?(?:[^"'`\s]*\/)?tfx-route\.sh\b/i;
|
|
13
|
+
const ROUTE_PROMPT_RE = /tfx-route\.sh/i;
|
|
10
14
|
|
|
11
15
|
function inferWorkerIndex(agentName = "") {
|
|
12
16
|
const match = /(\d+)(?!.*\d)/.exec(agentName);
|
|
@@ -26,6 +30,63 @@ function buildRouteEnvPrefix(agentName, workerIndex, searchTool) {
|
|
|
26
30
|
return envPrefix;
|
|
27
31
|
}
|
|
28
32
|
|
|
33
|
+
/**
|
|
34
|
+
* slim-wrapper 커스텀 subagent 사양.
|
|
35
|
+
* Claude Code custom subagent(`.claude/agents/slim-wrapper.md`)와 짝을 이룬다.
|
|
36
|
+
*
|
|
37
|
+
* @param {'codex'|'gemini'} cli
|
|
38
|
+
* @param {object} opts
|
|
39
|
+
* @returns {{name:string, cli:string, subagent_type:string, prompt:string}}
|
|
40
|
+
*/
|
|
41
|
+
export function buildSlimWrapperAgent(cli, opts = {}) {
|
|
42
|
+
return {
|
|
43
|
+
name: opts.agentName || `${cli}-wrapper`,
|
|
44
|
+
cli,
|
|
45
|
+
subagent_type: SLIM_WRAPPER_SUBAGENT_TYPE,
|
|
46
|
+
prompt: buildSlimWrapperPrompt(cli, opts),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* slim-wrapper 로그에서 tfx-route.sh 경유 흔적을 판정한다.
|
|
52
|
+
* route stderr prefix(`[tfx-route]`) 또는 Bash command trace를 근거로 본다.
|
|
53
|
+
*
|
|
54
|
+
* @param {object} input
|
|
55
|
+
* @param {string} [input.promptText]
|
|
56
|
+
* @param {string} [input.stdoutText]
|
|
57
|
+
* @param {string} [input.stderrText]
|
|
58
|
+
* @returns {{
|
|
59
|
+
* expectedRouteInvocation: boolean,
|
|
60
|
+
* promptMentionsRoute: boolean,
|
|
61
|
+
* sawRouteCommand: boolean,
|
|
62
|
+
* sawRouteLog: boolean,
|
|
63
|
+
* usedRoute: boolean,
|
|
64
|
+
* abnormal: boolean,
|
|
65
|
+
* reason: string|null,
|
|
66
|
+
* }}
|
|
67
|
+
*/
|
|
68
|
+
export function verifySlimWrapperRouteExecution(input = {}) {
|
|
69
|
+
const promptText = String(input.promptText || "");
|
|
70
|
+
const stdoutText = String(input.stdoutText || "");
|
|
71
|
+
const stderrText = String(input.stderrText || "");
|
|
72
|
+
const combinedLogs = `${stdoutText}\n${stderrText}`;
|
|
73
|
+
const promptMentionsRoute = ROUTE_PROMPT_RE.test(promptText);
|
|
74
|
+
const sawRouteCommand = ROUTE_COMMAND_RE.test(combinedLogs);
|
|
75
|
+
const sawRouteLog = ROUTE_LOG_RE.test(combinedLogs);
|
|
76
|
+
const usedRoute = sawRouteCommand || sawRouteLog;
|
|
77
|
+
const expectedRouteInvocation = promptMentionsRoute;
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
expectedRouteInvocation,
|
|
81
|
+
promptMentionsRoute,
|
|
82
|
+
sawRouteCommand,
|
|
83
|
+
sawRouteLog,
|
|
84
|
+
usedRoute,
|
|
85
|
+
abnormal: expectedRouteInvocation && !usedRoute,
|
|
86
|
+
reason: expectedRouteInvocation && !usedRoute ? "missing_tfx_route_evidence" : null,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
29
90
|
/**
|
|
30
91
|
* role/mcp_profile별 tfx-route.sh 기본 timeout (초)
|
|
31
92
|
* analyze/review 프로필이나 설계·분석 역할은 더 긴 timeout을 부여한다.
|
|
@@ -83,24 +144,34 @@ export function buildSlimWrapperPrompt(cli, opts = {}) {
|
|
|
83
144
|
: '';
|
|
84
145
|
const routeEnvPrefix = buildRouteEnvPrefix(agentName, workerIndex, searchTool);
|
|
85
146
|
|
|
86
|
-
|
|
147
|
+
return `실행 프로토콜 (subagent_type="${SLIM_WRAPPER_SUBAGENT_TYPE}"):
|
|
148
|
+
1. Bash(command, timeout: ${bashTimeoutMs}) — 아래 명령 1회만 실행
|
|
149
|
+
2. Bash 종료 후 TaskUpdate + SendMessage로 Claude Code 태스크 동기화
|
|
150
|
+
3. 종료${pipelineHint}
|
|
87
151
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
4. 결과 보고 후 반드시 종료${pipelineHint}
|
|
152
|
+
[HARD CONSTRAINT] 허용 도구: Bash, TaskUpdate, SendMessage만 사용한다.
|
|
153
|
+
Read, Edit, Write, Grep, Glob, Agent, WebSearch, WebFetch 등 다른 모든 도구 사용을 금지한다.
|
|
154
|
+
코드를 직접 읽거나 수정하면 안 된다. 반드시 아래 Bash 명령(tfx-route.sh)을 통해 Codex/Gemini에 위임하라.
|
|
155
|
+
이 규칙을 위반하면 작업 실패로 간주한다.
|
|
93
156
|
|
|
94
157
|
gemini/codex를 직접 호출하지 마라. 반드시 tfx-route.sh를 거쳐야 한다.
|
|
95
158
|
프롬프트를 파일로 저장하지 마라. tfx-route.sh가 인자로 받는다.
|
|
96
159
|
|
|
160
|
+
Step 1 — Bash 실행:
|
|
97
161
|
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} "${role}" '"'"'${escaped}'"'"' ${mcp_profile}', timeout: ${bashTimeoutMs})
|
|
98
162
|
|
|
99
|
-
|
|
100
|
-
|
|
163
|
+
Step 2 — Claude Code 태스크 동기화 (Bash 완료 후 반드시 실행):
|
|
164
|
+
exit_code=0이면:
|
|
165
|
+
TaskUpdate(taskId: "${taskId}", status: "completed", metadata: {result: "success"})
|
|
166
|
+
SendMessage(type: "message", recipient: "${leadName}", content: "완료: ${agentName}", summary: "task ${taskId} success")
|
|
167
|
+
exit_code≠0이면:
|
|
168
|
+
TaskUpdate(taskId: "${taskId}", status: "completed", metadata: {result: "failed", error: "exit_code=N"})
|
|
169
|
+
SendMessage(type: "message", recipient: "${leadName}", content: "실패: ${agentName} (exit=N)", summary: "task ${taskId} failed")
|
|
170
|
+
TFX_NEEDS_FALLBACK 출력 감지 시:
|
|
171
|
+
TaskUpdate(taskId: "${taskId}", status: "completed", metadata: {result: "fallback", reason: "claude-native"})
|
|
172
|
+
SendMessage(type: "message", recipient: "${leadName}", content: "fallback 필요: ${agentName} — claude-native 역할은 Claude Agent로 위임 필요", summary: "task ${taskId} fallback")
|
|
101
173
|
|
|
102
|
-
|
|
103
|
-
실패 여부는 metadata.result로 구분. Bash 실패 시에도 반드시 TaskUpdate + SendMessage 후 종료.`;
|
|
174
|
+
Step 3 — TaskUpdate + SendMessage 후 즉시 종료. 추가 도구 호출 금지.`;
|
|
104
175
|
}
|
|
105
176
|
|
|
106
177
|
/**
|
|
@@ -169,6 +240,11 @@ export function buildHybridWrapperPrompt(cli, opts = {}) {
|
|
|
169
240
|
Bash: node ${psmuxPath} output --session "${sessionName}" --name "${agentName}" --lines 100
|
|
170
241
|
→ 결과를 TaskUpdate + SendMessage로 보고
|
|
171
242
|
${pipelineHint}
|
|
243
|
+
[HARD CONSTRAINT] 너는 Bash, TaskUpdate, TaskGet, TaskList, SendMessage만 사용할 수 있다.
|
|
244
|
+
Read, Edit, Write, Grep, Glob, Agent, WebSearch, WebFetch 등 다른 모든 도구 사용을 금지한다.
|
|
245
|
+
코드를 직접 읽거나 수정하면 안 된다. 반드시 아래 Bash 명령(tfx-route.sh)을 통해 Codex/Gemini에 위임하라.
|
|
246
|
+
이 규칙을 위반하면 작업 실패로 간주한다.
|
|
247
|
+
|
|
172
248
|
gemini/codex를 직접 호출하지 마라. psmux spawn이 tfx-route.sh를 통해 실행한다.
|
|
173
249
|
프롬프트를 파일로 저장하지 마라. psmux spawn --cmd 인자로 전달된다.
|
|
174
250
|
|