@triflux/core 10.50.2 → 10.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/hooks/agy-session-hook.mjs +11 -56
  2. package/hooks/codex-session-hook.mjs +11 -101
  3. package/hub/bridge.mjs +1 -79
  4. package/hub/cli-adapter-base.mjs +12 -90
  5. package/hub/codex-adapter.mjs +0 -1
  6. package/hub/codex-preflight.mjs +5 -28
  7. package/hub/index.mjs +1 -1
  8. package/hub/lib/known-errors.json +2 -2
  9. package/hub/lib/process-utils.mjs +0 -123
  10. package/hub/lib/spawn-trace.mjs +3 -2
  11. package/hub/lib/timeout-defaults.mjs +0 -1
  12. package/hub/role-contract.mjs +0 -2
  13. package/hud/cli-policy.mjs +0 -22
  14. package/hud/constants.mjs +11 -26
  15. package/hud/hud-qos-status.mjs +23 -51
  16. package/hud/providers/gemini.mjs +105 -20
  17. package/hud/renderers.mjs +50 -104
  18. package/hud/utils.mjs +2 -2
  19. package/package.json +1 -3
  20. package/scripts/lib/agent-route-policy.mjs +162 -14
  21. package/scripts/lib/claudemd-scanner.mjs +2 -199
  22. package/scripts/lib/cli-claude.mjs +0 -2
  23. package/scripts/lib/context.mjs +3 -19
  24. package/scripts/lib/env-probe.mjs +5 -64
  25. package/scripts/lib/gemini-profiles.mjs +2 -69
  26. package/scripts/lib/machine-profile.mjs +3 -17
  27. package/scripts/lib/mcp-filter.mjs +109 -53
  28. package/scripts/lib/mcp-health.mjs +2 -2
  29. package/scripts/lib/mcp-server-catalog.mjs +2 -2
  30. package/scripts/lib/psmux-info.mjs +11 -59
  31. package/scripts/lib/session-presence.mjs +1 -1
  32. package/scripts/lib/skill-template.mjs +0 -245
  33. package/hub/dynamic-routing-engine.mjs +0 -511
  34. package/hub/lib/risk-tier.mjs +0 -53
  35. package/hub/lib/ssh-command.mjs +0 -231
  36. package/hub/lib/staged-file-detect.mjs +0 -43
  37. package/hub/lib/tfx-route-args.mjs +0 -353
  38. package/hub/routing-snapshot.mjs +0 -261
  39. package/mesh/index.mjs +0 -69
  40. package/mesh/mesh-budget.mjs +0 -137
  41. package/mesh/mesh-heartbeat.mjs +0 -100
  42. package/mesh/mesh-protocol.mjs +0 -96
  43. package/mesh/mesh-queue.mjs +0 -163
  44. package/mesh/mesh-registry.mjs +0 -78
  45. package/mesh/mesh-router.mjs +0 -77
  46. package/scripts/lib/dynamic-route-cli.mjs +0 -107
package/hud/renderers.mjs CHANGED
@@ -1,8 +1,6 @@
1
1
  // ============================================================================
2
2
  // 라인 렌더러 (tier별 행 생성)
3
3
  // ============================================================================
4
- import { readdirSync, statSync } from "node:fs";
5
- import { join } from "node:path";
6
4
  import {
7
5
  bold,
8
6
  CLAUDE_ORANGE,
@@ -12,17 +10,18 @@ import {
12
10
  colorByPercent,
13
11
  colorByProvider,
14
12
  dim,
13
+ GAUGE_WIDTH,
14
+ GEMINI_BLUE,
15
15
  geminiBlue,
16
- green,
17
- red,
18
16
  yellow,
19
17
  } from "./colors.mjs";
20
18
  import {
21
19
  ACCOUNT_LABEL_WIDTH,
22
20
  FIVE_HOUR_MS,
21
+ ONE_DAY_MS,
22
+ PERCENT_CELL_WIDTH,
23
23
  PROVIDER_PREFIX_WIDTH,
24
24
  SEVEN_DAY_MS,
25
- TEAM_STATE_DIR,
26
25
  } from "./constants.mjs";
27
26
  import { buildContextUsageView } from "./context-monitor.mjs";
28
27
  import { getTerminalColumns, tierBar, tierDimBar } from "./terminal.mjs";
@@ -36,94 +35,10 @@ import {
36
35
  formatTimeCell,
37
36
  formatTimeCellDH,
38
37
  padAnsiRight,
39
- readJson,
40
38
  stripAnsi,
41
39
  truncateAnsi,
42
40
  } from "./utils.mjs";
43
41
 
44
- // ============================================================================
45
- // tfx-multi 상태 행 생성 (v2.2 HUD 통합)
46
- // ============================================================================
47
- export function getTeamRow(currentTier, stateDir = TEAM_STATE_DIR) {
48
- let latestPath = null;
49
- let latestMtime = Date.now() - 24 * 60 * 60 * 1000;
50
- try {
51
- for (const name of readdirSync(stateDir)) {
52
- if (!/^team-state-.+\.json$/.test(name)) continue;
53
- const path = join(stateDir, name);
54
- try {
55
- const stat = statSync(path);
56
- if (stat.isFile() && stat.mtimeMs > latestMtime) {
57
- latestPath = path;
58
- latestMtime = stat.mtimeMs;
59
- }
60
- } catch {
61
- // 세션 종료 중 사라진 파일은 제외한다.
62
- }
63
- }
64
- } catch {
65
- return null;
66
- }
67
- const teamState = latestPath ? readJson(latestPath, null) : null;
68
- if (!teamState?.sessionName) return null;
69
-
70
- const workers = (teamState.members || []).filter((m) => m.role === "worker");
71
- if (!workers.length) return null;
72
-
73
- const tasks = teamState.tasks || [];
74
- const completed = tasks.filter((t) => t.status === "completed").length;
75
- const failed = tasks.filter((t) => t.status === "failed").length;
76
- const total = tasks.length || workers.length;
77
-
78
- // 경과 시간 (80col 이상에서만 표시)
79
- const elapsed =
80
- teamState.startedAt && (currentTier === "full" || currentTier === "compact")
81
- ? `${Math.round((Date.now() - teamState.startedAt) / 60000)}m`
82
- : "";
83
-
84
- // CLI 브랜드: 단일문자 + ANSI 색상 (x=codex, g=gemini, c=claude)
85
- const cliTag = (cli) =>
86
- cli === "codex"
87
- ? bold(codexWhite("x"))
88
- : cli === "gemini"
89
- ? bold(geminiBlue("g"))
90
- : bold(claudeOrange("c"));
91
- // 멤버 상태: 태그 + 상태기호 (60col 이상)
92
- const memberIcons =
93
- currentTier === "full" ||
94
- currentTier === "compact" ||
95
- currentTier === "minimal"
96
- ? workers
97
- .map((m) => {
98
- const task = tasks.find((t) => t.owner === m.name);
99
- const status =
100
- task?.status === "completed"
101
- ? green("\u2713")
102
- : task?.status === "in_progress"
103
- ? yellow("\u22EF")
104
- : task?.status === "failed"
105
- ? red("\u2717")
106
- : dim("\u25CC");
107
- return `${cliTag(m.cli)}${status}`;
108
- })
109
- .join(" ")
110
- : "";
111
-
112
- // 진행 텍스트
113
- const doneText =
114
- failed > 0
115
- ? `${completed}/${total} ${red(`${failed}\u2717`)}`
116
- : `${completed}/${total}`;
117
-
118
- const leftText = elapsed ? `${doneText} ${dim(elapsed)}` : doneText;
119
-
120
- return {
121
- prefix: bold(claudeOrange("\u25B2")),
122
- left: leftText,
123
- right: memberIcons,
124
- };
125
- }
126
-
127
42
  // ============================================================================
128
43
  // 행 정렬 렌더링
129
44
  // ============================================================================
@@ -142,9 +57,7 @@ export function renderAlignedRows(rows) {
142
57
  if (!hasRight) {
143
58
  return truncateAnsi(`${prefix} ${row.left}`, cols);
144
59
  }
145
- // 자기 left 대비 패딩 상한: 최대 2칸까지만 패딩 (과도한 공백 방지)
146
- const ownLen = stripAnsi(row.left).length;
147
- const effectiveWidth = Math.min(rawLeftWidth, ownLen + 2);
60
+ const effectiveWidth = rawLeftWidth;
148
61
  const left = padAnsiRight(row.left, effectiveWidth);
149
62
  // 우선순위 기반 truncate: right 먼저 축소, 그래도 넘치면 left 축소
150
63
  // prefix(PROVIDER_PREFIX_WIDTH) + " "(1) + left(effectiveWidth) + " | "(3) + right
@@ -169,10 +82,9 @@ export function getMicroLine(
169
82
  contextView,
170
83
  claudeUsage,
171
84
  codexBuckets,
172
- antigravityReady,
173
85
  options = {},
174
86
  ) {
175
- const { showCodex = true, showGemini = true } = options;
87
+ const { showCodex = true, showAntigravity = true } = options;
176
88
  const ctxView = contextView || buildContextUsageView({});
177
89
  // Claude 5h/1w
178
90
  const cF =
@@ -211,15 +123,26 @@ export function getMicroLine(
211
123
  if (showCodex) {
212
124
  segments.push(`${bold(codexWhite("x"))}${dim(":")}${xVal}`);
213
125
  }
214
- if (showGemini) {
126
+ if (showAntigravity) {
127
+ const marker = options.antigravityQuota ? bold(geminiBlue("a")) : dim("a");
215
128
  segments.push(
216
- `${bold(geminiBlue(antigravityReady ? "a" : "g"))}${dim(":")}${antigravityReady ? "agy" : dim("--")}`,
129
+ `${marker}${dim(":")}${antigravityPercentText(options.antigravityQuota)}${options.antigravityQuota?.stale ? dim("*") : ""}`,
217
130
  );
218
131
  }
219
132
  segments.push(`${dim("CTX:")}${contextPercentText(ctxView)}`);
220
133
  return truncateAnsi(segments.join(" "), cols);
221
134
  }
222
135
 
136
+ function antigravityPercentText(quota) {
137
+ return quota?.usedPercent != null
138
+ ? colorByProvider(
139
+ quota.usedPercent,
140
+ formatPercentCell(quota.usedPercent),
141
+ geminiBlue,
142
+ )
143
+ : dim("--".padStart(PERCENT_CELL_WIDTH));
144
+ }
145
+
223
146
  // context 는 토큰 수 대신 사용률만 보여 준다.
224
147
  function contextPercentText(ctxView) {
225
148
  const text = ctxView.display === "--" ? "--" : `${ctxView.percent}%`;
@@ -324,7 +247,10 @@ export function getAccountLabel(
324
247
  const providerState = accountsState?.providers?.[provider] || {};
325
248
  const lastId = providerState.last_selected_id;
326
249
  const picked = providerConfig.find((a) => a.id === lastId) ||
327
- providerConfig[0] || { id: `${provider}-main`, label: provider };
250
+ providerConfig[0] || {
251
+ id: `${provider}-main`,
252
+ label: provider === "antigravity" ? "agy" : provider,
253
+ };
328
254
  let label = picked.label || picked.id;
329
255
  if (codexEmail) label = codexEmail;
330
256
  if (label.includes("@")) label = label.split("@")[0];
@@ -347,14 +273,34 @@ export function getProviderRow(
347
273
  );
348
274
 
349
275
  const prefix = `${bold(markerColor(marker))}:`;
350
- if (provider === "antigravity" || provider === "gemini") {
276
+ if (provider === "antigravity") {
277
+ const usedPercent = realQuota?.usedPercent;
278
+ const active = usedPercent != null;
279
+ const quotaPercent = antigravityPercentText(realQuota);
280
+ const right = `${active ? markerColor(accountLabel) : dim(accountLabel)}${realQuota?.stale ? dim(" [stale]") : ""}`;
281
+ if (currentTier === "nano" || currentTier === "micro") {
282
+ return {
283
+ prefix: active ? prefix : dim(`${marker}:`),
284
+ left: quotaPercent,
285
+ right,
286
+ };
287
+ }
288
+ const bar = active
289
+ ? tierBar(currentTier, usedPercent, GEMINI_BLUE)
290
+ : currentTier === "full"
291
+ ? " ".repeat(GAUGE_WIDTH + 1)
292
+ : "";
293
+ const reset =
294
+ (Date.parse(realQuota?.resetTime) - Date.now() >= ONE_DAY_MS
295
+ ? formatResetRemainingDayHour(realQuota.resetTime)
296
+ : formatResetRemaining(realQuota?.resetTime)) || "n/a";
297
+ const showTime = currentTier === "full" || currentTier === "compact";
298
+ // /usage의 모델별 값에는 5h/1w 식별자가 없으므로 창을 추측하지 않는다.
299
+ const slot = `${dim("--:")}${bar}${quotaPercent}${showTime ? ` ${dim(formatTimeCell(reset))}` : ""}`;
351
300
  return {
352
- prefix,
353
- left:
354
- provider === "antigravity"
355
- ? markerColor(realQuota?.currentAbbrev || "agy")
356
- : dim("--"),
357
- right: accountLabel ? markerColor(accountLabel) : "",
301
+ prefix: active ? prefix : dim(`${marker}:`),
302
+ left: `${slot} ${" ".repeat(stripAnsi(slot).length)}`,
303
+ right,
358
304
  };
359
305
  }
360
306
  const provAnsi = CODEX_WHITE;
package/hud/utils.mjs CHANGED
@@ -164,10 +164,10 @@ export function formatTimeCell(value) {
164
164
  return `(${text.padStart(TIME_CELL_INNER_WIDTH, padChar)})`;
165
165
  }
166
166
 
167
- // 주간(d/h) 전용 — 최대 7d00h(5자)이므로 공백 불필요
167
+ // n/a도 정상 주간 시간과 같은 너비를 유지한다.
168
168
  export function formatTimeCellDH(value) {
169
169
  const text = normalizeTimeToken(value);
170
- return `(${text})`;
170
+ return `(${text.padStart(TIME_CELL_INNER_WIDTH, " ")})`;
171
171
  }
172
172
 
173
173
  export function getCliArgValue(flag) {
package/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "@triflux/core",
3
- "version": "10.50.2",
3
+ "version": "10.51.0",
4
4
  "description": "triflux core — CLI routing, pipeline, adapters. Zero native dependencies.",
5
5
  "type": "module",
6
6
  "main": "hub/index.mjs",
7
7
  "exports": {
8
8
  ".": "./hub/index.mjs",
9
9
  "./hub/*": "./hub/*",
10
- "./mesh/*": "./mesh/*",
11
10
  "./scripts/*": "./scripts/*"
12
11
  },
13
12
  "engines": {
@@ -21,7 +20,6 @@
21
20
  },
22
21
  "files": [
23
22
  "hub",
24
- "mesh",
25
23
  "scripts/lib",
26
24
  "hooks",
27
25
  "hud"
@@ -1,9 +1,12 @@
1
1
  // Canonical Codex role policy. Concrete model IDs stay in profile config files.
2
2
 
3
+ import { readFileSync } from "node:fs";
4
+ import { pathToFileURL } from "node:url";
3
5
  import {
4
6
  normalizeCodexProfileName,
5
7
  resolveCodexProfileConfigValues,
6
8
  } from "./codex-profile-config.mjs";
9
+ import { resolveGeminiProfileForPurpose } from "./gemini-profiles.mjs";
7
10
 
8
11
  const policy = {
9
12
  executor: {
@@ -198,7 +201,7 @@ function freezePolicy(entry) {
198
201
  return Object.freeze({ ...entry });
199
202
  }
200
203
 
201
- export const CODEX_AGENT_POLICY = Object.freeze(
204
+ const CODEX_AGENT_POLICY = Object.freeze(
202
205
  Object.fromEntries(
203
206
  Object.entries(policy).map(([agent, entry]) => [
204
207
  agent,
@@ -206,9 +209,9 @@ export const CODEX_AGENT_POLICY = Object.freeze(
206
209
  ]),
207
210
  ),
208
211
  );
209
- export const DEFAULT_CODEX_AGENT = "executor";
212
+ const DEFAULT_CODEX_AGENT = "executor";
210
213
 
211
- export const CANONICAL_CODEX_PROFILE_OVERRIDES = Object.freeze(
214
+ const CANONICAL_CODEX_PROFILE_OVERRIDES = Object.freeze(
212
215
  new Set([
213
216
  "gpt6_luna_low",
214
217
  "gpt6_luna_high",
@@ -219,7 +222,7 @@ export const CANONICAL_CODEX_PROFILE_OVERRIDES = Object.freeze(
219
222
  "gpt6_astra_ultra",
220
223
  ]),
221
224
  );
222
- export const ULTRA_ELIGIBLE_CODEX_AGENTS = Object.freeze(
225
+ const ULTRA_ELIGIBLE_CODEX_AGENTS = Object.freeze(
223
226
  new Set(["deep-executor", "scientist-deep"]),
224
227
  );
225
228
 
@@ -316,31 +319,176 @@ export function resolveNestedCodexAgentProfile(
316
319
  return effort.toLowerCase() === "ultra" ? "gpt6_astra_max" : candidate;
317
320
  }
318
321
 
319
- export function describeCodexAgentPolicy() {
320
- return {
321
- defaultAgent: DEFAULT_CODEX_AGENT,
322
- agents: Object.keys(CODEX_AGENT_POLICY),
323
- };
322
+ const MIN_TIMEOUT_BY_AGENT = Object.freeze({
323
+ "deep-executor": 900,
324
+ architect: 900,
325
+ planner: 900,
326
+ critic: 900,
327
+ analyst: 900,
328
+ "document-specialist": 900,
329
+ scientist: 900,
330
+ "scientist-deep": 900,
331
+ "code-reviewer": 600,
332
+ "security-reviewer": 600,
333
+ "quality-reviewer": 600,
334
+ executor: 300,
335
+ debugger: 300,
336
+ });
337
+
338
+ const EXPECTED_DURATION_BY_AGENT = Object.freeze({
339
+ writer: 90,
340
+ verifier: 90,
341
+ "qa-tester": 90,
342
+ executor: 300,
343
+ debugger: 300,
344
+ "test-engineer": 300,
345
+ "code-reviewer": 600,
346
+ "security-reviewer": 600,
347
+ architect: 600,
348
+ planner: 600,
349
+ critic: 600,
350
+ analyst: 600,
351
+ scientist: 900,
352
+ "scientist-deep": 900,
353
+ "deep-executor": 900,
354
+ "document-specialist": 900,
355
+ });
356
+
357
+ const NATIVE_CODEX_AGENTS = new Set([
358
+ "explore",
359
+ "verifier",
360
+ "test-engineer",
361
+ "qa-tester",
362
+ ]);
363
+
364
+ function resolveDirectCodexRoutePolicy(agent) {
365
+ if (agent === "explore") {
366
+ return { ...CODEX_AGENT_POLICY.spark, timeoutSec: 600, mcpHint: "analyze" };
367
+ }
368
+ if (agent === "antigravity" || agent === "agy") {
369
+ return CODEX_AGENT_POLICY.designer;
370
+ }
371
+ return resolveCodexAgentPolicy(agent);
372
+ }
373
+
374
+ export function resolveAgentRoute(
375
+ agent,
376
+ map,
377
+ profileOverride = "auto",
378
+ nested = false,
379
+ geminiProfileResolver = resolveGeminiProfileForPurpose,
380
+ ) {
381
+ if (!/^[A-Za-z0-9_.-]+$/.test(agent)) {
382
+ throw new Error(`invalid agent: ${agent}`);
383
+ }
384
+ if (!Object.hasOwn(map, agent)) {
385
+ throw new Error(
386
+ `알 수 없는 에이전트 타입: ${agent}; available: ${Object.keys(map).join(", ")}`,
387
+ );
388
+ }
389
+ const provider = map[agent];
390
+ if (!["codex", "antigravity", "claude"].includes(provider)) {
391
+ throw new Error(`invalid provider for ${agent}: ${provider}`);
392
+ }
393
+ const policy = resolveDirectCodexRoutePolicy(agent);
394
+ const normalizedOverride = normalizeCodexProfileName(profileOverride);
395
+ let resolvedOverride = policy.profile;
396
+ if (normalizedOverride !== "auto") {
397
+ try {
398
+ resolvedOverride = resolveCodexAgentProfile(agent, {
399
+ profileOverride: normalizedOverride,
400
+ nested,
401
+ });
402
+ } catch {
403
+ throw new Error(
404
+ `TFX_CODEX_PROFILE 값이 유효하지 않습니다: ${profileOverride}`,
405
+ );
406
+ }
407
+ }
408
+ return [
409
+ provider,
410
+ policy.profile,
411
+ policy.timeoutSec,
412
+ policy.runMode,
413
+ policy.opusOversight,
414
+ policy.mcpHint,
415
+ policy.subcommand,
416
+ normalizedOverride,
417
+ resolvedOverride,
418
+ geminiProfileResolver(agent),
419
+ MIN_TIMEOUT_BY_AGENT[agent] ?? 120,
420
+ EXPECTED_DURATION_BY_AGENT[agent] ?? 30,
421
+ NATIVE_CODEX_AGENTS.has(agent),
422
+ ];
324
423
  }
325
424
 
326
425
  function parseCliArgs(argv) {
327
426
  const args = [...argv];
328
427
  let format = "json";
329
428
  let agent = DEFAULT_CODEX_AGENT;
429
+ let mapFile = "";
430
+ let profileOverride = "auto";
431
+ let geminiProfilesModule = "";
432
+ let nested = false;
330
433
  while (args.length > 0) {
331
434
  const flag = args.shift();
332
435
  if (flag === "--format") format = args.shift() || "";
333
436
  else if (flag === "--agent") agent = args.shift() || "";
437
+ else if (flag === "--map-file") mapFile = args.shift() || "";
438
+ else if (flag === "--profile-override")
439
+ profileOverride = args.shift() || "";
440
+ else if (flag === "--gemini-profiles-module")
441
+ geminiProfilesModule = args.shift() || "";
442
+ else if (flag === "--nested") nested = true;
334
443
  else throw new Error(`unknown argument: ${flag}`);
335
444
  }
336
- if (!new Set(["json", "tsv"]).has(format)) {
445
+ if (!new Set(["json", "tsv", "route"]).has(format)) {
337
446
  throw new Error(`unsupported format: ${format}`);
338
447
  }
339
- return { format, agent };
448
+ return {
449
+ format,
450
+ agent,
451
+ mapFile,
452
+ profileOverride,
453
+ geminiProfilesModule,
454
+ nested,
455
+ };
340
456
  }
341
457
 
342
- function runCli() {
343
- const { format, agent } = parseCliArgs(process.argv.slice(2));
458
+ async function runCli() {
459
+ const {
460
+ format,
461
+ agent,
462
+ mapFile,
463
+ profileOverride,
464
+ geminiProfilesModule,
465
+ nested,
466
+ } = parseCliArgs(process.argv.slice(2));
467
+ if (format === "route") {
468
+ if (!mapFile) throw new Error("--map-file is required for route format");
469
+ const map = JSON.parse(readFileSync(mapFile, "utf8"));
470
+ let resolver = resolveGeminiProfileForPurpose;
471
+ if (geminiProfilesModule) {
472
+ try {
473
+ const module = await import(pathToFileURL(geminiProfilesModule).href);
474
+ resolver = (agent) => {
475
+ try {
476
+ return module.resolveGeminiProfileForPurpose(agent) || "flash38";
477
+ } catch {
478
+ return "flash38";
479
+ }
480
+ };
481
+ } catch {
482
+ resolver = () => "flash38";
483
+ }
484
+ }
485
+ process.stdout.write(
486
+ resolveAgentRoute(agent, map, profileOverride, nested, resolver).join(
487
+ "\x1e",
488
+ ),
489
+ );
490
+ return;
491
+ }
344
492
  const resolved = resolveCodexAgentPolicy(agent);
345
493
  if (format === "tsv") {
346
494
  process.stdout.write(
@@ -360,7 +508,7 @@ function runCli() {
360
508
 
361
509
  if (import.meta.url === new URL(process.argv[1], "file:").href) {
362
510
  try {
363
- runCli();
511
+ await runCli();
364
512
  } catch (error) {
365
513
  process.stderr.write(`[agent-route-policy] ${error.message}\n`);
366
514
  process.exitCode = 1;