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.
@@ -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
- logFile: state?.logFile || null,
67
- errFile: state?.errFile || null,
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
- try { outWs.end(); } catch {}
135
- try { errWs.end(); } catch {}
136
- maybeAutoShutdown();
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
  }
@@ -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
- const taskIdRef = taskId ? `taskId: "${taskId}"` : "";
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
- return `인터럽트 프로토콜:
89
- 1. TaskUpdate(${taskIdRef ? `${taskIdRef}, ` : ""}status: in_progress) task claim
90
- 2. SendMessage(to: ${leadName}, "작업 시작: ${agentName}") 시작 보고 ( 경계 생성)
91
- 3. Bash(command, timeout: ${bashTimeoutMs}) 아래 명령 1회 실행
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
- 성공 TaskUpdate(${taskIdRef ? `${taskIdRef}, ` : ""}status: completed, metadata: {result: "success"}) + SendMessage(to: ${leadName}).
100
- 실패 → TaskUpdate(${taskIdRef ? `${taskIdRef}, ` : ""}status: completed, metadata: {result: "failed", error: "에러 요약"}) + SendMessage(to: ${leadName}).
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
- 중요: TaskUpdate status는 "completed"만 사용. "failed"는 API 미지원.
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