triflux 7.1.4 → 7.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +31 -31
- package/.claude-plugin/plugin.json +22 -23
- package/bin/triflux.mjs +18 -5
- package/hooks/keyword-rules.json +393 -361
- package/hub/bridge.mjs +799 -786
- package/hub/delegator/contracts.mjs +37 -38
- package/hub/delegator/schema/delegator-tools.schema.json +250 -250
- package/hub/delegator/service.mjs +307 -302
- package/hub/intent.mjs +108 -11
- package/hub/lib/process-utils.mjs +20 -0
- package/hub/pipe.mjs +589 -589
- package/hub/pipeline/gates/confidence.mjs +1 -1
- package/hub/pipeline/gates/selfcheck.mjs +2 -4
- package/hub/pipeline/state.mjs +191 -187
- package/hub/pipeline/transitions.mjs +124 -120
- package/hub/public/dashboard.html +355 -349
- package/hub/quality/deslop.mjs +5 -3
- package/hub/reflexion.mjs +5 -1
- package/hub/research.mjs +6 -1
- package/hub/router.mjs +791 -782
- package/hub/server.mjs +893 -822
- package/hub/store.mjs +807 -778
- package/hub/team/agent-map.json +10 -0
- package/hub/team/ansi.mjs +3 -4
- package/hub/team/cli/commands/control.mjs +43 -43
- package/hub/team/cli/commands/interrupt.mjs +36 -36
- package/hub/team/cli/commands/kill.mjs +3 -3
- package/hub/team/cli/commands/send.mjs +37 -37
- package/hub/team/cli/commands/start/index.mjs +18 -8
- package/hub/team/cli/commands/start/parse-args.mjs +3 -1
- package/hub/team/cli/commands/start/start-headless.mjs +4 -1
- package/hub/team/cli/commands/status.mjs +87 -87
- package/hub/team/cli/commands/stop.mjs +1 -1
- package/hub/team/cli/commands/task.mjs +1 -1
- package/hub/team/cli/index.mjs +41 -39
- package/hub/team/cli/manifest.mjs +29 -28
- package/hub/team/cli/services/hub-client.mjs +37 -0
- package/hub/team/cli/services/state-store.mjs +26 -12
- package/hub/team/dashboard.mjs +11 -4
- package/hub/team/handoff.mjs +12 -0
- package/hub/team/headless.mjs +202 -200
- package/hub/team/native-supervisor.mjs +386 -346
- package/hub/team/nativeProxy.mjs +680 -692
- package/hub/team/staleState.mjs +361 -369
- package/hub/team/tui-viewer.mjs +27 -3
- package/hub/team/tui.mjs +1 -0
- package/hub/token-mode.mjs +114 -24
- package/hub/workers/delegator-mcp.mjs +1059 -1057
- package/hud/colors.mjs +88 -0
- package/hud/constants.mjs +78 -0
- package/hud/hud-qos-status.mjs +206 -1872
- package/hud/providers/claude.mjs +309 -0
- package/hud/providers/codex.mjs +151 -0
- package/hud/providers/gemini.mjs +320 -0
- package/hud/renderers.mjs +424 -0
- package/hud/terminal.mjs +140 -0
- package/hud/utils.mjs +271 -0
- package/package.json +1 -2
- package/scripts/__tests__/keyword-detector.test.mjs +234 -234
- package/scripts/headless-guard-fast.sh +21 -0
- package/scripts/headless-guard.mjs +26 -6
- package/scripts/lib/keyword-rules.mjs +166 -168
- package/scripts/setup.mjs +725 -690
- package/scripts/tfx-route-post.mjs +424 -424
- package/scripts/tfx-route.sh +1671 -1650
- package/scripts/tmp-cleanup.mjs +74 -0
- package/skills/tfx-auto/SKILL.md +279 -278
- package/skills/tfx-auto-codex/SKILL.md +98 -77
- package/skills/tfx-codex/SKILL.md +65 -65
- package/skills/tfx-gemini/SKILL.md +83 -82
- package/skills/tfx-hub/SKILL.md +205 -136
- package/skills/tfx-multi/SKILL.md +11 -5
- package/.mcp.json +0 -8
|
@@ -1,424 +1,424 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// tfx-route-post.mjs v2.0 — tfx-route.sh 후처리 (단일 프로세스)
|
|
3
|
-
//
|
|
4
|
-
// cli-route.sh v1.x의 5개 런타임(jq, python3, node)을 node 단일로 통합.
|
|
5
|
-
// ~100ms (node 1회 기동) vs ~1000ms (python3×2 + jq×3 + node×2)
|
|
6
|
-
//
|
|
7
|
-
// 처리:
|
|
8
|
-
// 1. 토큰 추출 (Codex stderr / Gemini session JSON)
|
|
9
|
-
// 2. Codex JSON-line 출력 필터링
|
|
10
|
-
// 3. 실행 로그 기록 (JSONL)
|
|
11
|
-
// 4. 토큰 누적 (sv-accumulator.json)
|
|
12
|
-
// 5. AIMD 배치 이벤트 기록 (append-only JSONL — 락 불필요)
|
|
13
|
-
// 6. CLI 이슈 자동 수집
|
|
14
|
-
// 7. 구조화된 결과 출력 (=== TFX-ROUTE RESULT ===)
|
|
15
|
-
|
|
16
|
-
import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
17
|
-
import { join
|
|
18
|
-
import { homedir } from "os";
|
|
19
|
-
|
|
20
|
-
const HOME = homedir();
|
|
21
|
-
const CACHE_DIR = join(HOME, ".claude", "cache");
|
|
22
|
-
const LOG_DIR = join(HOME, ".claude", "logs");
|
|
23
|
-
|
|
24
|
-
// ── 인자 파싱 ──
|
|
25
|
-
function parseArgs() {
|
|
26
|
-
const args = {};
|
|
27
|
-
for (let i = 2; i < process.argv.length; i++) {
|
|
28
|
-
if (process.argv[i].startsWith("--")) {
|
|
29
|
-
const key = process.argv[i].slice(2).replace(/-/g, "_");
|
|
30
|
-
args[key] = process.argv[i + 1] || "";
|
|
31
|
-
i++;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return args;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// ── 토큰 추출 ──
|
|
38
|
-
function extractTokens(cliType, stderrFile) {
|
|
39
|
-
if (cliType === "codex") {
|
|
40
|
-
// Codex CLI: stderr에 "tokens used\n76,239" 형식
|
|
41
|
-
try {
|
|
42
|
-
const stderr = readFileSync(stderrFile, "utf-8");
|
|
43
|
-
const match = stderr.match(/tokens used\s*\n\s*([\d,]+)/i);
|
|
44
|
-
if (match) {
|
|
45
|
-
const total = parseInt(match[1].replace(/,/g, ""));
|
|
46
|
-
if (total > 0) return { input: total, output: 0 };
|
|
47
|
-
}
|
|
48
|
-
} catch {}
|
|
49
|
-
return { input: 0, output: 0 };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (cliType === "gemini") {
|
|
53
|
-
// Gemini CLI: ~/.gemini/tmp/*/chats/session-*.json에서 최신 세션
|
|
54
|
-
const geminiTmp = join(HOME, ".gemini", "tmp");
|
|
55
|
-
if (!existsSync(geminiTmp)) return { input: 0, output: 0 };
|
|
56
|
-
|
|
57
|
-
let latestFile = null;
|
|
58
|
-
let latestMtime = 0;
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
for (const dir of readdirSync(geminiTmp)) {
|
|
62
|
-
const chatsDir = join(geminiTmp, dir, "chats");
|
|
63
|
-
if (!existsSync(chatsDir)) continue;
|
|
64
|
-
for (const f of readdirSync(chatsDir)) {
|
|
65
|
-
if (!f.startsWith("session-") || !f.endsWith(".json")) continue;
|
|
66
|
-
const fp = join(chatsDir, f);
|
|
67
|
-
try {
|
|
68
|
-
const mtime = statSync(fp).mtimeMs;
|
|
69
|
-
if (mtime > latestMtime) {
|
|
70
|
-
latestMtime = mtime;
|
|
71
|
-
latestFile = fp;
|
|
72
|
-
}
|
|
73
|
-
} catch {}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
} catch {}
|
|
77
|
-
|
|
78
|
-
if (!latestFile) return { input: 0, output: 0 };
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
const data = JSON.parse(readFileSync(latestFile, "utf-8"));
|
|
82
|
-
let inp = 0,
|
|
83
|
-
out = 0;
|
|
84
|
-
for (const msg of data.messages || []) {
|
|
85
|
-
inp += msg.tokens?.input || 0;
|
|
86
|
-
out += msg.tokens?.output || 0;
|
|
87
|
-
}
|
|
88
|
-
if (inp + out > 0) return { input: inp, output: out };
|
|
89
|
-
} catch {}
|
|
90
|
-
return { input: 0, output: 0 };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return { input: 0, output: 0 };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ── Codex JSON-line 출력 필터링 ──
|
|
97
|
-
// 단일 패스: JSON이면 파싱, 아니면 그대로 출력 (python3 이중 호출 제거)
|
|
98
|
-
function filterCodexOutput(rawOutput) {
|
|
99
|
-
const lines = rawOutput.split("\n");
|
|
100
|
-
const result = [];
|
|
101
|
-
|
|
102
|
-
for (const line of lines) {
|
|
103
|
-
const trimmed = line.trim();
|
|
104
|
-
if (!trimmed) continue;
|
|
105
|
-
|
|
106
|
-
try {
|
|
107
|
-
const obj = JSON.parse(trimmed);
|
|
108
|
-
if (["message", "completed", "output_text"].includes(obj.type)) {
|
|
109
|
-
const text = obj.text || obj.content || obj.output || "";
|
|
110
|
-
if (text) result.push(text);
|
|
111
|
-
}
|
|
112
|
-
} catch {
|
|
113
|
-
// JSON 아님 → 그대로 통과
|
|
114
|
-
result.push(line);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return result.join("\n");
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function cleanTuiArtifacts(output, cliType) {
|
|
122
|
-
if (!output) return output;
|
|
123
|
-
|
|
124
|
-
const normalizedCliType = cliType || "";
|
|
125
|
-
|
|
126
|
-
let cleaned = output
|
|
127
|
-
.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "")
|
|
128
|
-
.replace(/\x1b\][^\x07]*\x07/g, "")
|
|
129
|
-
.replace(/\x1b\[[0-9;]*[mGKHJsu]/g, "");
|
|
130
|
-
|
|
131
|
-
cleaned = cleaned.replace(/\r/g, "");
|
|
132
|
-
|
|
133
|
-
if (normalizedCliType.startsWith("codex")) {
|
|
134
|
-
cleaned = cleaned
|
|
135
|
-
.replace(/^[^\S\n]*[╭╮╰╯│─┌┐└┘├┤┬┴┼].*$/gm, "")
|
|
136
|
-
.replace(/^[^\S\n]*[›❯]\s*$/gm, "")
|
|
137
|
-
.replace(/^\s*codex\s*$/gm, "")
|
|
138
|
-
.replace(/^[^\S\n]*[›❯]\s*Applied.*$/gm, "");
|
|
139
|
-
} else if (normalizedCliType.startsWith("gemini")) {
|
|
140
|
-
cleaned = cleaned.replace(/^[^\S\n]*[╭╮╰╯│─═].*$/gm, "").replace(/^[^\S\n]*>\s*$/gm, "");
|
|
141
|
-
} else if (normalizedCliType.startsWith("claude")) {
|
|
142
|
-
cleaned = cleaned.replace(/^[^\S\n]*[━─]{5,}.*$/gm, "");
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
cleaned = cleaned.replace(/\n{3,}/g, "\n\n");
|
|
146
|
-
cleaned = cleaned.trim();
|
|
147
|
-
|
|
148
|
-
return cleaned;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// ── 실행 로그 기록 (JSONL, append-only) ──
|
|
152
|
-
function logExecution(params) {
|
|
153
|
-
const logFile = join(LOG_DIR, "tfx-route-stats.jsonl");
|
|
154
|
-
|
|
155
|
-
try {
|
|
156
|
-
mkdirSync(LOG_DIR, { recursive: true });
|
|
157
|
-
|
|
158
|
-
const entry = JSON.stringify({
|
|
159
|
-
ts: new Date().toISOString(),
|
|
160
|
-
agent: params.agent,
|
|
161
|
-
cli: params.cli,
|
|
162
|
-
effort: params.effort,
|
|
163
|
-
run_mode: params.run_mode,
|
|
164
|
-
opus_oversight: params.opus,
|
|
165
|
-
status: params.status,
|
|
166
|
-
exit_code: params.exit_code,
|
|
167
|
-
elapsed_sec: params.elapsed,
|
|
168
|
-
timeout_sec: params.timeout,
|
|
169
|
-
mcp_profile: params.mcp_profile,
|
|
170
|
-
input_tokens: params.tokens.input,
|
|
171
|
-
output_tokens: params.tokens.output,
|
|
172
|
-
total_tokens: params.tokens.input + params.tokens.output,
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
appendFileSync(logFile, entry + "\n");
|
|
176
|
-
} catch {}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// ── 토큰 누적 (sv-accumulator.json) ──
|
|
180
|
-
function accumulateTokens(cliType, tokens) {
|
|
181
|
-
if (tokens.input + tokens.output === 0) return;
|
|
182
|
-
|
|
183
|
-
const accFile = join(CACHE_DIR, "sv-accumulator.json");
|
|
184
|
-
try {
|
|
185
|
-
mkdirSync(CACHE_DIR, { recursive: true });
|
|
186
|
-
let data;
|
|
187
|
-
try {
|
|
188
|
-
data = JSON.parse(readFileSync(accFile, "utf-8"));
|
|
189
|
-
} catch {
|
|
190
|
-
data = {};
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (!data.codex) data.codex = { tokens: 0, calls: 0 };
|
|
194
|
-
if (!data.gemini) data.gemini = { tokens: 0, calls: 0 };
|
|
195
|
-
|
|
196
|
-
const key = cliType === "gemini" ? "gemini" : "codex";
|
|
197
|
-
data[key].tokens += tokens.input + tokens.output;
|
|
198
|
-
data[key].calls += 1;
|
|
199
|
-
data.lastUpdated = new Date().toISOString();
|
|
200
|
-
|
|
201
|
-
writeFileSync(accFile, JSON.stringify(data, null, 2));
|
|
202
|
-
} catch {}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// ── AIMD 배치 이벤트 (append-only JSONL — 락 불필요) ──
|
|
206
|
-
// 오케스트레이터가 이 파일을 읽어 batch_size를 계산
|
|
207
|
-
function recordBatchEvent(result, agent) {
|
|
208
|
-
const eventsFile = join(CACHE_DIR, "batch-events.jsonl");
|
|
209
|
-
try {
|
|
210
|
-
mkdirSync(CACHE_DIR, { recursive: true });
|
|
211
|
-
appendFileSync(eventsFile, JSON.stringify({ ts: Date.now(), agent, result }) + "\n");
|
|
212
|
-
|
|
213
|
-
// 자동 회전: 200줄 초과 시 최근 100줄 유지
|
|
214
|
-
const content = readFileSync(eventsFile, "utf-8").trim();
|
|
215
|
-
const lines = content.split("\n");
|
|
216
|
-
if (lines.length > 200) {
|
|
217
|
-
writeFileSync(eventsFile, lines.slice(-100).join("\n") + "\n");
|
|
218
|
-
}
|
|
219
|
-
} catch {}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// ── CLI 이슈 추적 ──
|
|
223
|
-
function trackCliIssue(cliType, agent, stderrText, exitCode) {
|
|
224
|
-
if (!stderrText && exitCode === 0) return;
|
|
225
|
-
|
|
226
|
-
const patterns = [
|
|
227
|
-
{ regex: /sandbox image.*missing/i, pattern: "sandbox_missing", msg: "Docker sandbox image not found", severity: "warn" },
|
|
228
|
-
{ regex: /rate.limit|429|too many requests/i, pattern: "rate_limit", msg: "API rate limit exceeded", severity: "warn" },
|
|
229
|
-
{ regex: /ECONNREFUSED|ENOTFOUND|network/i, pattern: "network_error", msg: "Network connection failed", severity: "error" },
|
|
230
|
-
{ regex: /deprecated/i, pattern: "deprecated_flag", msg: "Deprecated flag/feature detected", severity: "warn" },
|
|
231
|
-
{ regex: /API_KEY.*not.set|auth.*fail|unauthorized|401/i, pattern: "auth_error", msg: "Authentication failed", severity: "error" },
|
|
232
|
-
{ regex: /ENOMEM|out of memory|heap/i, pattern: "oom", msg: "Out of memory", severity: "error" },
|
|
233
|
-
];
|
|
234
|
-
|
|
235
|
-
let matched = null;
|
|
236
|
-
for (const p of patterns) {
|
|
237
|
-
if (p.regex.test(stderrText)) {
|
|
238
|
-
matched = p;
|
|
239
|
-
break;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (!matched && exitCode !== 0 && exitCode !== 124) {
|
|
244
|
-
matched = { pattern: "unknown_error", msg: `Exit code ${exitCode}`, severity: "warn" };
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (!matched) return;
|
|
248
|
-
|
|
249
|
-
const issuesFile = join(CACHE_DIR, "cli-issues.jsonl");
|
|
250
|
-
try {
|
|
251
|
-
mkdirSync(CACHE_DIR, { recursive: true });
|
|
252
|
-
|
|
253
|
-
// 중복 방지: 같은 패턴+cli가 최근 5분 내 기록됐으면 건너뜀
|
|
254
|
-
if (existsSync(issuesFile)) {
|
|
255
|
-
const lines = readFileSync(issuesFile, "utf-8").trim().split("\n").slice(-5);
|
|
256
|
-
const now = Date.now();
|
|
257
|
-
for (const line of lines) {
|
|
258
|
-
try {
|
|
259
|
-
const entry = JSON.parse(line);
|
|
260
|
-
if (entry.pattern === matched.pattern && entry.cli === cliType && now - entry.ts < 300000) return;
|
|
261
|
-
} catch {}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const snippet = stderrText.substring(0, 200).replace(/\n/g, " ");
|
|
266
|
-
|
|
267
|
-
appendFileSync(
|
|
268
|
-
issuesFile,
|
|
269
|
-
JSON.stringify({
|
|
270
|
-
ts: Date.now(),
|
|
271
|
-
cli: cliType,
|
|
272
|
-
agent,
|
|
273
|
-
pattern: matched.pattern,
|
|
274
|
-
msg: matched.msg,
|
|
275
|
-
severity: matched.severity,
|
|
276
|
-
snippet,
|
|
277
|
-
resolved: false,
|
|
278
|
-
}) + "\n",
|
|
279
|
-
);
|
|
280
|
-
|
|
281
|
-
// 자동 회전
|
|
282
|
-
const content = readFileSync(issuesFile, "utf-8").trim();
|
|
283
|
-
const allLines = content.split("\n");
|
|
284
|
-
if (allLines.length > 200) {
|
|
285
|
-
writeFileSync(issuesFile, allLines.slice(-100).join("\n") + "\n");
|
|
286
|
-
}
|
|
287
|
-
} catch {}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// ── 출력 절삭 ──
|
|
291
|
-
function truncateOutput(text, maxBytes) {
|
|
292
|
-
const buf = Buffer.from(text);
|
|
293
|
-
if (buf.length > maxBytes) {
|
|
294
|
-
return (
|
|
295
|
-
buf.subarray(0, maxBytes).toString("utf-8") +
|
|
296
|
-
`\n--- [출력 ${buf.length}B → ${maxBytes}B로 절삭됨] ---`
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
return text;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// ── 메인 ──
|
|
303
|
-
function main() {
|
|
304
|
-
const a = parseArgs();
|
|
305
|
-
|
|
306
|
-
const agent = a.agent || "unknown";
|
|
307
|
-
const cliType = a.cli || "codex";
|
|
308
|
-
const effort = a.effort || "high";
|
|
309
|
-
const runMode = a.run_mode || "bg";
|
|
310
|
-
const opus = a.opus || "false";
|
|
311
|
-
const exitCode = parseInt(a.exit_code || "0");
|
|
312
|
-
const elapsed = parseInt(a.elapsed || "0");
|
|
313
|
-
const timeout = parseInt(a.timeout || "300");
|
|
314
|
-
const mcpProfile = a.mcp_profile || "auto";
|
|
315
|
-
const stderrLog = a.stderr_log || "";
|
|
316
|
-
const stdoutLog = a.stdout_log || "";
|
|
317
|
-
const maxBytes = parseInt(a.max_bytes || "51200");
|
|
318
|
-
const cliCmd = a.cli_cmd || cliType;
|
|
319
|
-
|
|
320
|
-
// stderr/stdout 읽기
|
|
321
|
-
let stderrContent = "";
|
|
322
|
-
try {
|
|
323
|
-
stderrContent = readFileSync(stderrLog, "utf-8");
|
|
324
|
-
} catch {}
|
|
325
|
-
let rawOutput = "";
|
|
326
|
-
try {
|
|
327
|
-
rawOutput = readFileSync(stdoutLog, "utf-8");
|
|
328
|
-
} catch {}
|
|
329
|
-
|
|
330
|
-
// 1. 토큰 추출
|
|
331
|
-
const tokens = extractTokens(cliType, stderrLog);
|
|
332
|
-
|
|
333
|
-
// 2. 상태 판단
|
|
334
|
-
let status;
|
|
335
|
-
if (exitCode === 0) {
|
|
336
|
-
status = stderrContent ? "success_with_warnings" : "success";
|
|
337
|
-
} else if (exitCode === 124) {
|
|
338
|
-
status = "timeout";
|
|
339
|
-
} else {
|
|
340
|
-
status = "failed";
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// 3. 실행 로그
|
|
344
|
-
logExecution({
|
|
345
|
-
agent,
|
|
346
|
-
cli: cliType,
|
|
347
|
-
effort,
|
|
348
|
-
run_mode: runMode,
|
|
349
|
-
opus,
|
|
350
|
-
status,
|
|
351
|
-
exit_code: exitCode,
|
|
352
|
-
elapsed,
|
|
353
|
-
timeout,
|
|
354
|
-
mcp_profile: mcpProfile,
|
|
355
|
-
tokens,
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
// 4. 성공 시 토큰 누적
|
|
359
|
-
if (exitCode === 0) accumulateTokens(cliType, tokens);
|
|
360
|
-
|
|
361
|
-
// 5. AIMD 배치 이벤트
|
|
362
|
-
const aimdResult = exitCode === 0 ? "success" : exitCode === 124 ? "timeout" : "failed";
|
|
363
|
-
recordBatchEvent(aimdResult, agent);
|
|
364
|
-
|
|
365
|
-
// 6. CLI 이슈 추적
|
|
366
|
-
trackCliIssue(cliType, agent, stderrContent, exitCode);
|
|
367
|
-
|
|
368
|
-
// 7. 구조화된 결과 출력
|
|
369
|
-
console.log("=== TFX-ROUTE RESULT ===");
|
|
370
|
-
console.log(`agent: ${agent}`);
|
|
371
|
-
console.log(`cli: ${cliType} (${cliCmd})`);
|
|
372
|
-
console.log(`effort: ${effort}`);
|
|
373
|
-
console.log(`run_mode: ${runMode}`);
|
|
374
|
-
console.log(`opus_oversight: ${opus}`);
|
|
375
|
-
console.log(`exit_code: ${exitCode}`);
|
|
376
|
-
console.log(`timeout: ${timeout}s`);
|
|
377
|
-
console.log(`elapsed: ${elapsed}s`);
|
|
378
|
-
console.log(`mcp_profile: ${mcpProfile}`);
|
|
379
|
-
console.log(`stderr_log: ${stderrLog}`);
|
|
380
|
-
|
|
381
|
-
if (exitCode === 0) {
|
|
382
|
-
if (stderrContent) {
|
|
383
|
-
console.log("status: success_with_warnings");
|
|
384
|
-
console.log(`warnings: ${stderrContent.split("\n").slice(0, 3).join(" ")}`);
|
|
385
|
-
} else {
|
|
386
|
-
console.log("status: success");
|
|
387
|
-
}
|
|
388
|
-
console.log("=== OUTPUT ===");
|
|
389
|
-
let filtered = cliType === "codex" ? filterCodexOutput(rawOutput) : rawOutput;
|
|
390
|
-
if (a.clean_tui !== "false" && process.env.TFX_CLEAN_TUI !== "0") {
|
|
391
|
-
filtered = cleanTuiArtifacts(filtered, cliType);
|
|
392
|
-
}
|
|
393
|
-
console.log(truncateOutput(filtered, maxBytes));
|
|
394
|
-
} else if (exitCode === 124) {
|
|
395
|
-
console.log(`status: timeout (${timeout}s 초과)`);
|
|
396
|
-
console.log("=== PARTIAL OUTPUT ===");
|
|
397
|
-
let partialFiltered = rawOutput;
|
|
398
|
-
if (a.clean_tui !== "false" && process.env.TFX_CLEAN_TUI !== "0") {
|
|
399
|
-
partialFiltered = cleanTuiArtifacts(partialFiltered, cliType);
|
|
400
|
-
}
|
|
401
|
-
console.log(truncateOutput(partialFiltered, maxBytes));
|
|
402
|
-
console.log("=== STDERR ===");
|
|
403
|
-
console.log(stderrContent.split("\n").slice(-10).join("\n"));
|
|
404
|
-
} else {
|
|
405
|
-
console.log(`status: failed (exit_code=${exitCode})`);
|
|
406
|
-
console.log("=== STDERR ===");
|
|
407
|
-
console.log(stderrContent.split("\n").slice(-20).join("\n"));
|
|
408
|
-
if (rawOutput) {
|
|
409
|
-
console.log("=== PARTIAL OUTPUT ===");
|
|
410
|
-
let partialFiltered = rawOutput;
|
|
411
|
-
if (a.clean_tui !== "false" && process.env.TFX_CLEAN_TUI !== "0") {
|
|
412
|
-
partialFiltered = cleanTuiArtifacts(partialFiltered, cliType);
|
|
413
|
-
}
|
|
414
|
-
console.log(truncateOutput(partialFiltered, maxBytes));
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
import { fileURLToPath } from "url";
|
|
420
|
-
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
421
|
-
main();
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
export { cleanTuiArtifacts };
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// tfx-route-post.mjs v2.0 — tfx-route.sh 후처리 (단일 프로세스)
|
|
3
|
+
//
|
|
4
|
+
// cli-route.sh v1.x의 5개 런타임(jq, python3, node)을 node 단일로 통합.
|
|
5
|
+
// ~100ms (node 1회 기동) vs ~1000ms (python3×2 + jq×3 + node×2)
|
|
6
|
+
//
|
|
7
|
+
// 처리:
|
|
8
|
+
// 1. 토큰 추출 (Codex stderr / Gemini session JSON)
|
|
9
|
+
// 2. Codex JSON-line 출력 필터링
|
|
10
|
+
// 3. 실행 로그 기록 (JSONL)
|
|
11
|
+
// 4. 토큰 누적 (sv-accumulator.json)
|
|
12
|
+
// 5. AIMD 배치 이벤트 기록 (append-only JSONL — 락 불필요)
|
|
13
|
+
// 6. CLI 이슈 자동 수집
|
|
14
|
+
// 7. 구조화된 결과 출력 (=== TFX-ROUTE RESULT ===)
|
|
15
|
+
|
|
16
|
+
import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
17
|
+
import { join } from "path";
|
|
18
|
+
import { homedir } from "os";
|
|
19
|
+
|
|
20
|
+
const HOME = homedir();
|
|
21
|
+
const CACHE_DIR = join(HOME, ".claude", "cache");
|
|
22
|
+
const LOG_DIR = join(HOME, ".claude", "logs");
|
|
23
|
+
|
|
24
|
+
// ── 인자 파싱 ──
|
|
25
|
+
function parseArgs() {
|
|
26
|
+
const args = {};
|
|
27
|
+
for (let i = 2; i < process.argv.length; i++) {
|
|
28
|
+
if (process.argv[i].startsWith("--")) {
|
|
29
|
+
const key = process.argv[i].slice(2).replace(/-/g, "_");
|
|
30
|
+
args[key] = process.argv[i + 1] || "";
|
|
31
|
+
i++;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return args;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── 토큰 추출 ──
|
|
38
|
+
function extractTokens(cliType, stderrFile) {
|
|
39
|
+
if (cliType === "codex") {
|
|
40
|
+
// Codex CLI: stderr에 "tokens used\n76,239" 형식
|
|
41
|
+
try {
|
|
42
|
+
const stderr = readFileSync(stderrFile, "utf-8");
|
|
43
|
+
const match = stderr.match(/tokens used\s*\n\s*([\d,]+)/i);
|
|
44
|
+
if (match) {
|
|
45
|
+
const total = parseInt(match[1].replace(/,/g, ""));
|
|
46
|
+
if (total > 0) return { input: total, output: 0 };
|
|
47
|
+
}
|
|
48
|
+
} catch {}
|
|
49
|
+
return { input: 0, output: 0 };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (cliType === "gemini") {
|
|
53
|
+
// Gemini CLI: ~/.gemini/tmp/*/chats/session-*.json에서 최신 세션
|
|
54
|
+
const geminiTmp = join(HOME, ".gemini", "tmp");
|
|
55
|
+
if (!existsSync(geminiTmp)) return { input: 0, output: 0 };
|
|
56
|
+
|
|
57
|
+
let latestFile = null;
|
|
58
|
+
let latestMtime = 0;
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
for (const dir of readdirSync(geminiTmp)) {
|
|
62
|
+
const chatsDir = join(geminiTmp, dir, "chats");
|
|
63
|
+
if (!existsSync(chatsDir)) continue;
|
|
64
|
+
for (const f of readdirSync(chatsDir)) {
|
|
65
|
+
if (!f.startsWith("session-") || !f.endsWith(".json")) continue;
|
|
66
|
+
const fp = join(chatsDir, f);
|
|
67
|
+
try {
|
|
68
|
+
const mtime = statSync(fp).mtimeMs;
|
|
69
|
+
if (mtime > latestMtime) {
|
|
70
|
+
latestMtime = mtime;
|
|
71
|
+
latestFile = fp;
|
|
72
|
+
}
|
|
73
|
+
} catch {}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} catch {}
|
|
77
|
+
|
|
78
|
+
if (!latestFile) return { input: 0, output: 0 };
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const data = JSON.parse(readFileSync(latestFile, "utf-8"));
|
|
82
|
+
let inp = 0,
|
|
83
|
+
out = 0;
|
|
84
|
+
for (const msg of data.messages || []) {
|
|
85
|
+
inp += msg.tokens?.input || 0;
|
|
86
|
+
out += msg.tokens?.output || 0;
|
|
87
|
+
}
|
|
88
|
+
if (inp + out > 0) return { input: inp, output: out };
|
|
89
|
+
} catch {}
|
|
90
|
+
return { input: 0, output: 0 };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { input: 0, output: 0 };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Codex JSON-line 출력 필터링 ──
|
|
97
|
+
// 단일 패스: JSON이면 파싱, 아니면 그대로 출력 (python3 이중 호출 제거)
|
|
98
|
+
function filterCodexOutput(rawOutput) {
|
|
99
|
+
const lines = rawOutput.split("\n");
|
|
100
|
+
const result = [];
|
|
101
|
+
|
|
102
|
+
for (const line of lines) {
|
|
103
|
+
const trimmed = line.trim();
|
|
104
|
+
if (!trimmed) continue;
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const obj = JSON.parse(trimmed);
|
|
108
|
+
if (["message", "completed", "output_text"].includes(obj.type)) {
|
|
109
|
+
const text = obj.text || obj.content || obj.output || "";
|
|
110
|
+
if (text) result.push(text);
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
// JSON 아님 → 그대로 통과
|
|
114
|
+
result.push(line);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return result.join("\n");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function cleanTuiArtifacts(output, cliType) {
|
|
122
|
+
if (!output) return output;
|
|
123
|
+
|
|
124
|
+
const normalizedCliType = cliType || "";
|
|
125
|
+
|
|
126
|
+
let cleaned = output
|
|
127
|
+
.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "")
|
|
128
|
+
.replace(/\x1b\][^\x07]*\x07/g, "")
|
|
129
|
+
.replace(/\x1b\[[0-9;]*[mGKHJsu]/g, "");
|
|
130
|
+
|
|
131
|
+
cleaned = cleaned.replace(/\r/g, "");
|
|
132
|
+
|
|
133
|
+
if (normalizedCliType.startsWith("codex")) {
|
|
134
|
+
cleaned = cleaned
|
|
135
|
+
.replace(/^[^\S\n]*[╭╮╰╯│─┌┐└┘├┤┬┴┼].*$/gm, "")
|
|
136
|
+
.replace(/^[^\S\n]*[›❯]\s*$/gm, "")
|
|
137
|
+
.replace(/^\s*codex\s*$/gm, "")
|
|
138
|
+
.replace(/^[^\S\n]*[›❯]\s*Applied.*$/gm, "");
|
|
139
|
+
} else if (normalizedCliType.startsWith("gemini")) {
|
|
140
|
+
cleaned = cleaned.replace(/^[^\S\n]*[╭╮╰╯│─═].*$/gm, "").replace(/^[^\S\n]*>\s*$/gm, "");
|
|
141
|
+
} else if (normalizedCliType.startsWith("claude")) {
|
|
142
|
+
cleaned = cleaned.replace(/^[^\S\n]*[━─]{5,}.*$/gm, "");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
cleaned = cleaned.replace(/\n{3,}/g, "\n\n");
|
|
146
|
+
cleaned = cleaned.trim();
|
|
147
|
+
|
|
148
|
+
return cleaned;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── 실행 로그 기록 (JSONL, append-only) ──
|
|
152
|
+
function logExecution(params) {
|
|
153
|
+
const logFile = join(LOG_DIR, "tfx-route-stats.jsonl");
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
mkdirSync(LOG_DIR, { recursive: true });
|
|
157
|
+
|
|
158
|
+
const entry = JSON.stringify({
|
|
159
|
+
ts: new Date().toISOString(),
|
|
160
|
+
agent: params.agent,
|
|
161
|
+
cli: params.cli,
|
|
162
|
+
effort: params.effort,
|
|
163
|
+
run_mode: params.run_mode,
|
|
164
|
+
opus_oversight: params.opus,
|
|
165
|
+
status: params.status,
|
|
166
|
+
exit_code: params.exit_code,
|
|
167
|
+
elapsed_sec: params.elapsed,
|
|
168
|
+
timeout_sec: params.timeout,
|
|
169
|
+
mcp_profile: params.mcp_profile,
|
|
170
|
+
input_tokens: params.tokens.input,
|
|
171
|
+
output_tokens: params.tokens.output,
|
|
172
|
+
total_tokens: params.tokens.input + params.tokens.output,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
appendFileSync(logFile, entry + "\n");
|
|
176
|
+
} catch {}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── 토큰 누적 (sv-accumulator.json) ──
|
|
180
|
+
function accumulateTokens(cliType, tokens) {
|
|
181
|
+
if (tokens.input + tokens.output === 0) return;
|
|
182
|
+
|
|
183
|
+
const accFile = join(CACHE_DIR, "sv-accumulator.json");
|
|
184
|
+
try {
|
|
185
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
186
|
+
let data;
|
|
187
|
+
try {
|
|
188
|
+
data = JSON.parse(readFileSync(accFile, "utf-8"));
|
|
189
|
+
} catch {
|
|
190
|
+
data = {};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!data.codex) data.codex = { tokens: 0, calls: 0 };
|
|
194
|
+
if (!data.gemini) data.gemini = { tokens: 0, calls: 0 };
|
|
195
|
+
|
|
196
|
+
const key = cliType === "gemini" ? "gemini" : "codex";
|
|
197
|
+
data[key].tokens += tokens.input + tokens.output;
|
|
198
|
+
data[key].calls += 1;
|
|
199
|
+
data.lastUpdated = new Date().toISOString();
|
|
200
|
+
|
|
201
|
+
writeFileSync(accFile, JSON.stringify(data, null, 2));
|
|
202
|
+
} catch {}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── AIMD 배치 이벤트 (append-only JSONL — 락 불필요) ──
|
|
206
|
+
// 오케스트레이터가 이 파일을 읽어 batch_size를 계산
|
|
207
|
+
function recordBatchEvent(result, agent) {
|
|
208
|
+
const eventsFile = join(CACHE_DIR, "batch-events.jsonl");
|
|
209
|
+
try {
|
|
210
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
211
|
+
appendFileSync(eventsFile, JSON.stringify({ ts: Date.now(), agent, result }) + "\n");
|
|
212
|
+
|
|
213
|
+
// 자동 회전: 200줄 초과 시 최근 100줄 유지
|
|
214
|
+
const content = readFileSync(eventsFile, "utf-8").trim();
|
|
215
|
+
const lines = content.split("\n");
|
|
216
|
+
if (lines.length > 200) {
|
|
217
|
+
writeFileSync(eventsFile, lines.slice(-100).join("\n") + "\n");
|
|
218
|
+
}
|
|
219
|
+
} catch {}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── CLI 이슈 추적 ──
|
|
223
|
+
function trackCliIssue(cliType, agent, stderrText, exitCode) {
|
|
224
|
+
if (!stderrText && exitCode === 0) return;
|
|
225
|
+
|
|
226
|
+
const patterns = [
|
|
227
|
+
{ regex: /sandbox image.*missing/i, pattern: "sandbox_missing", msg: "Docker sandbox image not found", severity: "warn" },
|
|
228
|
+
{ regex: /rate.limit|429|too many requests/i, pattern: "rate_limit", msg: "API rate limit exceeded", severity: "warn" },
|
|
229
|
+
{ regex: /ECONNREFUSED|ENOTFOUND|network/i, pattern: "network_error", msg: "Network connection failed", severity: "error" },
|
|
230
|
+
{ regex: /deprecated/i, pattern: "deprecated_flag", msg: "Deprecated flag/feature detected", severity: "warn" },
|
|
231
|
+
{ regex: /API_KEY.*not.set|auth.*fail|unauthorized|401/i, pattern: "auth_error", msg: "Authentication failed", severity: "error" },
|
|
232
|
+
{ regex: /ENOMEM|out of memory|heap/i, pattern: "oom", msg: "Out of memory", severity: "error" },
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
let matched = null;
|
|
236
|
+
for (const p of patterns) {
|
|
237
|
+
if (p.regex.test(stderrText)) {
|
|
238
|
+
matched = p;
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!matched && exitCode !== 0 && exitCode !== 124) {
|
|
244
|
+
matched = { pattern: "unknown_error", msg: `Exit code ${exitCode}`, severity: "warn" };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!matched) return;
|
|
248
|
+
|
|
249
|
+
const issuesFile = join(CACHE_DIR, "cli-issues.jsonl");
|
|
250
|
+
try {
|
|
251
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
252
|
+
|
|
253
|
+
// 중복 방지: 같은 패턴+cli가 최근 5분 내 기록됐으면 건너뜀
|
|
254
|
+
if (existsSync(issuesFile)) {
|
|
255
|
+
const lines = readFileSync(issuesFile, "utf-8").trim().split("\n").slice(-5);
|
|
256
|
+
const now = Date.now();
|
|
257
|
+
for (const line of lines) {
|
|
258
|
+
try {
|
|
259
|
+
const entry = JSON.parse(line);
|
|
260
|
+
if (entry.pattern === matched.pattern && entry.cli === cliType && now - entry.ts < 300000) return;
|
|
261
|
+
} catch {}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const snippet = stderrText.substring(0, 200).replace(/\n/g, " ");
|
|
266
|
+
|
|
267
|
+
appendFileSync(
|
|
268
|
+
issuesFile,
|
|
269
|
+
JSON.stringify({
|
|
270
|
+
ts: Date.now(),
|
|
271
|
+
cli: cliType,
|
|
272
|
+
agent,
|
|
273
|
+
pattern: matched.pattern,
|
|
274
|
+
msg: matched.msg,
|
|
275
|
+
severity: matched.severity,
|
|
276
|
+
snippet,
|
|
277
|
+
resolved: false,
|
|
278
|
+
}) + "\n",
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
// 자동 회전
|
|
282
|
+
const content = readFileSync(issuesFile, "utf-8").trim();
|
|
283
|
+
const allLines = content.split("\n");
|
|
284
|
+
if (allLines.length > 200) {
|
|
285
|
+
writeFileSync(issuesFile, allLines.slice(-100).join("\n") + "\n");
|
|
286
|
+
}
|
|
287
|
+
} catch {}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ── 출력 절삭 ──
|
|
291
|
+
function truncateOutput(text, maxBytes) {
|
|
292
|
+
const buf = Buffer.from(text);
|
|
293
|
+
if (buf.length > maxBytes) {
|
|
294
|
+
return (
|
|
295
|
+
buf.subarray(0, maxBytes).toString("utf-8") +
|
|
296
|
+
`\n--- [출력 ${buf.length}B → ${maxBytes}B로 절삭됨] ---`
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
return text;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── 메인 ──
|
|
303
|
+
function main() {
|
|
304
|
+
const a = parseArgs();
|
|
305
|
+
|
|
306
|
+
const agent = a.agent || "unknown";
|
|
307
|
+
const cliType = a.cli || "codex";
|
|
308
|
+
const effort = a.effort || "high";
|
|
309
|
+
const runMode = a.run_mode || "bg";
|
|
310
|
+
const opus = a.opus || "false";
|
|
311
|
+
const exitCode = parseInt(a.exit_code || "0");
|
|
312
|
+
const elapsed = parseInt(a.elapsed || "0");
|
|
313
|
+
const timeout = parseInt(a.timeout || "300");
|
|
314
|
+
const mcpProfile = a.mcp_profile || "auto";
|
|
315
|
+
const stderrLog = a.stderr_log || "";
|
|
316
|
+
const stdoutLog = a.stdout_log || "";
|
|
317
|
+
const maxBytes = parseInt(a.max_bytes || "51200");
|
|
318
|
+
const cliCmd = a.cli_cmd || cliType;
|
|
319
|
+
|
|
320
|
+
// stderr/stdout 읽기
|
|
321
|
+
let stderrContent = "";
|
|
322
|
+
try {
|
|
323
|
+
stderrContent = readFileSync(stderrLog, "utf-8");
|
|
324
|
+
} catch {}
|
|
325
|
+
let rawOutput = "";
|
|
326
|
+
try {
|
|
327
|
+
rawOutput = readFileSync(stdoutLog, "utf-8");
|
|
328
|
+
} catch {}
|
|
329
|
+
|
|
330
|
+
// 1. 토큰 추출
|
|
331
|
+
const tokens = extractTokens(cliType, stderrLog);
|
|
332
|
+
|
|
333
|
+
// 2. 상태 판단
|
|
334
|
+
let status;
|
|
335
|
+
if (exitCode === 0) {
|
|
336
|
+
status = stderrContent ? "success_with_warnings" : "success";
|
|
337
|
+
} else if (exitCode === 124) {
|
|
338
|
+
status = "timeout";
|
|
339
|
+
} else {
|
|
340
|
+
status = "failed";
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// 3. 실행 로그
|
|
344
|
+
logExecution({
|
|
345
|
+
agent,
|
|
346
|
+
cli: cliType,
|
|
347
|
+
effort,
|
|
348
|
+
run_mode: runMode,
|
|
349
|
+
opus,
|
|
350
|
+
status,
|
|
351
|
+
exit_code: exitCode,
|
|
352
|
+
elapsed,
|
|
353
|
+
timeout,
|
|
354
|
+
mcp_profile: mcpProfile,
|
|
355
|
+
tokens,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// 4. 성공 시 토큰 누적
|
|
359
|
+
if (exitCode === 0) accumulateTokens(cliType, tokens);
|
|
360
|
+
|
|
361
|
+
// 5. AIMD 배치 이벤트
|
|
362
|
+
const aimdResult = exitCode === 0 ? "success" : exitCode === 124 ? "timeout" : "failed";
|
|
363
|
+
recordBatchEvent(aimdResult, agent);
|
|
364
|
+
|
|
365
|
+
// 6. CLI 이슈 추적
|
|
366
|
+
trackCliIssue(cliType, agent, stderrContent, exitCode);
|
|
367
|
+
|
|
368
|
+
// 7. 구조화된 결과 출력
|
|
369
|
+
console.log("=== TFX-ROUTE RESULT ===");
|
|
370
|
+
console.log(`agent: ${agent}`);
|
|
371
|
+
console.log(`cli: ${cliType} (${cliCmd})`);
|
|
372
|
+
console.log(`effort: ${effort}`);
|
|
373
|
+
console.log(`run_mode: ${runMode}`);
|
|
374
|
+
console.log(`opus_oversight: ${opus}`);
|
|
375
|
+
console.log(`exit_code: ${exitCode}`);
|
|
376
|
+
console.log(`timeout: ${timeout}s`);
|
|
377
|
+
console.log(`elapsed: ${elapsed}s`);
|
|
378
|
+
console.log(`mcp_profile: ${mcpProfile}`);
|
|
379
|
+
console.log(`stderr_log: ${stderrLog}`);
|
|
380
|
+
|
|
381
|
+
if (exitCode === 0) {
|
|
382
|
+
if (stderrContent) {
|
|
383
|
+
console.log("status: success_with_warnings");
|
|
384
|
+
console.log(`warnings: ${stderrContent.split("\n").slice(0, 3).join(" ")}`);
|
|
385
|
+
} else {
|
|
386
|
+
console.log("status: success");
|
|
387
|
+
}
|
|
388
|
+
console.log("=== OUTPUT ===");
|
|
389
|
+
let filtered = cliType === "codex" ? filterCodexOutput(rawOutput) : rawOutput;
|
|
390
|
+
if (a.clean_tui !== "false" && process.env.TFX_CLEAN_TUI !== "0") {
|
|
391
|
+
filtered = cleanTuiArtifacts(filtered, cliType);
|
|
392
|
+
}
|
|
393
|
+
console.log(truncateOutput(filtered, maxBytes));
|
|
394
|
+
} else if (exitCode === 124) {
|
|
395
|
+
console.log(`status: timeout (${timeout}s 초과)`);
|
|
396
|
+
console.log("=== PARTIAL OUTPUT ===");
|
|
397
|
+
let partialFiltered = rawOutput;
|
|
398
|
+
if (a.clean_tui !== "false" && process.env.TFX_CLEAN_TUI !== "0") {
|
|
399
|
+
partialFiltered = cleanTuiArtifacts(partialFiltered, cliType);
|
|
400
|
+
}
|
|
401
|
+
console.log(truncateOutput(partialFiltered, maxBytes));
|
|
402
|
+
console.log("=== STDERR ===");
|
|
403
|
+
console.log(stderrContent.split("\n").slice(-10).join("\n"));
|
|
404
|
+
} else {
|
|
405
|
+
console.log(`status: failed (exit_code=${exitCode})`);
|
|
406
|
+
console.log("=== STDERR ===");
|
|
407
|
+
console.log(stderrContent.split("\n").slice(-20).join("\n"));
|
|
408
|
+
if (rawOutput) {
|
|
409
|
+
console.log("=== PARTIAL OUTPUT ===");
|
|
410
|
+
let partialFiltered = rawOutput;
|
|
411
|
+
if (a.clean_tui !== "false" && process.env.TFX_CLEAN_TUI !== "0") {
|
|
412
|
+
partialFiltered = cleanTuiArtifacts(partialFiltered, cliType);
|
|
413
|
+
}
|
|
414
|
+
console.log(truncateOutput(partialFiltered, maxBytes));
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
import { fileURLToPath } from "url";
|
|
420
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
421
|
+
main();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export { cleanTuiArtifacts };
|