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.
Files changed (73) hide show
  1. package/.claude-plugin/marketplace.json +31 -31
  2. package/.claude-plugin/plugin.json +22 -23
  3. package/bin/triflux.mjs +18 -5
  4. package/hooks/keyword-rules.json +393 -361
  5. package/hub/bridge.mjs +799 -786
  6. package/hub/delegator/contracts.mjs +37 -38
  7. package/hub/delegator/schema/delegator-tools.schema.json +250 -250
  8. package/hub/delegator/service.mjs +307 -302
  9. package/hub/intent.mjs +108 -11
  10. package/hub/lib/process-utils.mjs +20 -0
  11. package/hub/pipe.mjs +589 -589
  12. package/hub/pipeline/gates/confidence.mjs +1 -1
  13. package/hub/pipeline/gates/selfcheck.mjs +2 -4
  14. package/hub/pipeline/state.mjs +191 -187
  15. package/hub/pipeline/transitions.mjs +124 -120
  16. package/hub/public/dashboard.html +355 -349
  17. package/hub/quality/deslop.mjs +5 -3
  18. package/hub/reflexion.mjs +5 -1
  19. package/hub/research.mjs +6 -1
  20. package/hub/router.mjs +791 -782
  21. package/hub/server.mjs +893 -822
  22. package/hub/store.mjs +807 -778
  23. package/hub/team/agent-map.json +10 -0
  24. package/hub/team/ansi.mjs +3 -4
  25. package/hub/team/cli/commands/control.mjs +43 -43
  26. package/hub/team/cli/commands/interrupt.mjs +36 -36
  27. package/hub/team/cli/commands/kill.mjs +3 -3
  28. package/hub/team/cli/commands/send.mjs +37 -37
  29. package/hub/team/cli/commands/start/index.mjs +18 -8
  30. package/hub/team/cli/commands/start/parse-args.mjs +3 -1
  31. package/hub/team/cli/commands/start/start-headless.mjs +4 -1
  32. package/hub/team/cli/commands/status.mjs +87 -87
  33. package/hub/team/cli/commands/stop.mjs +1 -1
  34. package/hub/team/cli/commands/task.mjs +1 -1
  35. package/hub/team/cli/index.mjs +41 -39
  36. package/hub/team/cli/manifest.mjs +29 -28
  37. package/hub/team/cli/services/hub-client.mjs +37 -0
  38. package/hub/team/cli/services/state-store.mjs +26 -12
  39. package/hub/team/dashboard.mjs +11 -4
  40. package/hub/team/handoff.mjs +12 -0
  41. package/hub/team/headless.mjs +202 -200
  42. package/hub/team/native-supervisor.mjs +386 -346
  43. package/hub/team/nativeProxy.mjs +680 -692
  44. package/hub/team/staleState.mjs +361 -369
  45. package/hub/team/tui-viewer.mjs +27 -3
  46. package/hub/team/tui.mjs +1 -0
  47. package/hub/token-mode.mjs +114 -24
  48. package/hub/workers/delegator-mcp.mjs +1059 -1057
  49. package/hud/colors.mjs +88 -0
  50. package/hud/constants.mjs +78 -0
  51. package/hud/hud-qos-status.mjs +206 -1872
  52. package/hud/providers/claude.mjs +309 -0
  53. package/hud/providers/codex.mjs +151 -0
  54. package/hud/providers/gemini.mjs +320 -0
  55. package/hud/renderers.mjs +424 -0
  56. package/hud/terminal.mjs +140 -0
  57. package/hud/utils.mjs +271 -0
  58. package/package.json +1 -2
  59. package/scripts/__tests__/keyword-detector.test.mjs +234 -234
  60. package/scripts/headless-guard-fast.sh +21 -0
  61. package/scripts/headless-guard.mjs +26 -6
  62. package/scripts/lib/keyword-rules.mjs +166 -168
  63. package/scripts/setup.mjs +725 -690
  64. package/scripts/tfx-route-post.mjs +424 -424
  65. package/scripts/tfx-route.sh +1671 -1650
  66. package/scripts/tmp-cleanup.mjs +74 -0
  67. package/skills/tfx-auto/SKILL.md +279 -278
  68. package/skills/tfx-auto-codex/SKILL.md +98 -77
  69. package/skills/tfx-codex/SKILL.md +65 -65
  70. package/skills/tfx-gemini/SKILL.md +83 -82
  71. package/skills/tfx-hub/SKILL.md +205 -136
  72. package/skills/tfx-multi/SKILL.md +11 -5
  73. 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, dirname } 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 };
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 };