@triflux/core 10.0.3 → 10.25.1

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 (143) hide show
  1. package/hooks/agent-route-guard.mjs +14 -10
  2. package/hooks/cross-review-tracker.mjs +21 -8
  3. package/hooks/error-context.mjs +47 -7
  4. package/hooks/hook-adaptive-collector.mjs +18 -16
  5. package/hooks/hook-manager.mjs +93 -32
  6. package/hooks/hook-orchestrator.mjs +279 -33
  7. package/hooks/hook-registry.json +104 -7
  8. package/hooks/hooks.json +49 -1
  9. package/hooks/keyword-rules.json +183 -23
  10. package/hooks/lib/resolve-root.mjs +1 -1
  11. package/hooks/mcp-config-watcher.mjs +6 -2
  12. package/hooks/permission-safe-allow.mjs +233 -0
  13. package/hooks/pipeline-stop.mjs +369 -22
  14. package/hooks/post-tool-tips.mjs +306 -0
  15. package/hooks/pre-compact-snapshot.mjs +181 -0
  16. package/hooks/safety-guard.mjs +308 -18
  17. package/hooks/session-end-cleanup.mjs +345 -0
  18. package/hooks/session-start-fast.mjs +214 -0
  19. package/hooks/subagent-tracker.mjs +267 -0
  20. package/hooks/subagent-verifier.mjs +9 -4
  21. package/hub/account-broker.mjs +1036 -66
  22. package/hub/adaptive-diagnostic.mjs +75 -48
  23. package/hub/adaptive-inject.mjs +96 -58
  24. package/hub/adaptive-memory.mjs +156 -42
  25. package/hub/adaptive.mjs +60 -31
  26. package/hub/assign-callbacks.mjs +163 -126
  27. package/hub/bridge.mjs +832 -291
  28. package/hub/cli-adapter-base.mjs +321 -97
  29. package/hub/codex-adapter.mjs +179 -93
  30. package/hub/codex-compat.mjs +4 -3
  31. package/hub/codex-preflight.mjs +113 -37
  32. package/hub/delegator/contracts.mjs +19 -23
  33. package/hub/delegator/index.mjs +14 -14
  34. package/hub/delegator/service.mjs +88 -64
  35. package/hub/delegator/tool-definitions.mjs +35 -35
  36. package/hub/dynamic-routing-engine.mjs +511 -0
  37. package/hub/fullcycle.mjs +34 -17
  38. package/hub/gemini-adapter.mjs +77 -95
  39. package/hub/hitl.mjs +202 -143
  40. package/hub/intent.mjs +171 -39
  41. package/hub/lib/bash-path.mjs +73 -0
  42. package/hub/lib/cache-guard.mjs +43 -17
  43. package/hub/lib/env-detect.mjs +145 -0
  44. package/hub/lib/hosts-compat.mjs +319 -0
  45. package/hub/lib/mcp-response-cache.mjs +239 -0
  46. package/hub/lib/memory-store.mjs +303 -107
  47. package/hub/lib/path-utils.mjs +165 -0
  48. package/hub/lib/phase-manager.mjs +493 -0
  49. package/hub/lib/process-utils.mjs +797 -104
  50. package/hub/lib/prompt-tmp.mjs +33 -0
  51. package/hub/lib/risk-tier.mjs +53 -0
  52. package/hub/lib/spawn-trace.mjs +561 -0
  53. package/hub/lib/ssh-command.mjs +44 -24
  54. package/hub/lib/ssh-retry.mjs +5 -1
  55. package/hub/lib/staged-file-detect.mjs +43 -0
  56. package/hub/lib/state-snapshot.mjs +293 -0
  57. package/hub/lib/tfx-route-args.mjs +309 -0
  58. package/hub/lib/timeline-adapter.mjs +190 -0
  59. package/hub/lib/trace-recorder.mjs +153 -0
  60. package/hub/lib/uuidv7.mjs +4 -3
  61. package/hub/memory-doctor.mjs +266 -106
  62. package/hub/middleware/quota-middleware.mjs +70 -0
  63. package/hub/middleware/request-logger.mjs +61 -34
  64. package/hub/paths.mjs +9 -9
  65. package/hub/pipeline/gates/confidence.mjs +34 -15
  66. package/hub/pipeline/gates/consensus.mjs +27 -15
  67. package/hub/pipeline/gates/index.mjs +7 -3
  68. package/hub/pipeline/gates/selfcheck.mjs +57 -19
  69. package/hub/pipeline/index.mjs +77 -42
  70. package/hub/pipeline/state.mjs +10 -10
  71. package/hub/pipeline/transitions.mjs +40 -23
  72. package/hub/platform.mjs +65 -48
  73. package/hub/quality/deslop.mjs +70 -49
  74. package/hub/reflexion.mjs +116 -58
  75. package/hub/research.mjs +34 -25
  76. package/hub/router.mjs +941 -791
  77. package/hub/routing/complexity.mjs +132 -29
  78. package/hub/routing/index.mjs +17 -12
  79. package/hub/routing/q-learning.mjs +76 -28
  80. package/hub/routing-snapshot.mjs +261 -0
  81. package/hub/session-fingerprint.mjs +129 -63
  82. package/hub/state.mjs +86 -44
  83. package/hub/team/retry-state-machine.mjs +267 -0
  84. package/hub/token-mode.mjs +76 -72
  85. package/hub/workers/worker-utils.mjs +22 -16
  86. package/hud/colors.mjs +27 -9
  87. package/hud/constants.mjs +184 -27
  88. package/hud/context-monitor.mjs +133 -44
  89. package/hud/hud-qos-status.mjs +153 -50
  90. package/hud/mission-board.mjs +56 -0
  91. package/hud/providers/claude.mjs +398 -114
  92. package/hud/providers/codex.mjs +96 -21
  93. package/hud/providers/gemini.mjs +184 -51
  94. package/hud/renderers.mjs +413 -123
  95. package/hud/terminal.mjs +101 -31
  96. package/hud/utils.mjs +81 -39
  97. package/mesh/index.mjs +69 -0
  98. package/mesh/mesh-budget.mjs +137 -0
  99. package/mesh/mesh-heartbeat.mjs +100 -0
  100. package/mesh/mesh-protocol.mjs +96 -0
  101. package/mesh/mesh-queue.mjs +163 -0
  102. package/mesh/mesh-registry.mjs +78 -0
  103. package/mesh/mesh-router.mjs +77 -0
  104. package/package.json +5 -1
  105. package/scripts/lib/agent-json.mjs +27 -0
  106. package/scripts/lib/async.mjs +174 -0
  107. package/scripts/lib/claudemd-scanner.mjs +6 -1
  108. package/scripts/lib/cli-agy.mjs +62 -0
  109. package/scripts/lib/cli-claude.mjs +78 -0
  110. package/scripts/lib/cli-codex.mjs +199 -0
  111. package/scripts/lib/cli-gemini.mjs +67 -0
  112. package/scripts/lib/codex-recovery.sh +50 -0
  113. package/scripts/lib/context.mjs +3 -3
  114. package/scripts/lib/cross-review-utils.mjs +13 -5
  115. package/scripts/lib/doctor-env-checks.mjs +121 -0
  116. package/scripts/lib/dynamic-route-cli.mjs +107 -0
  117. package/scripts/lib/env-probe.mjs +58 -30
  118. package/scripts/lib/env.mjs +14 -0
  119. package/scripts/lib/gemini-profiles.mjs +44 -10
  120. package/scripts/lib/handoff.mjs +33 -17
  121. package/scripts/lib/hook-utils.mjs +8 -6
  122. package/scripts/lib/hub.mjs +48 -0
  123. package/scripts/lib/keyword-rules.mjs +48 -19
  124. package/scripts/lib/logger.mjs +24 -24
  125. package/scripts/lib/mcp-filter.mjs +393 -249
  126. package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
  127. package/scripts/lib/mcp-gateway-servers.mjs +70 -0
  128. package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
  129. package/scripts/lib/mcp-guard-engine.mjs +1282 -116
  130. package/scripts/lib/mcp-health.mjs +773 -0
  131. package/scripts/lib/mcp-manifest.mjs +24 -14
  132. package/scripts/lib/mcp-server-catalog.mjs +355 -118
  133. package/scripts/lib/pid.mjs +63 -0
  134. package/scripts/lib/process-utils.mjs +26 -0
  135. package/scripts/lib/psmux-info.mjs +72 -12
  136. package/scripts/lib/quota.mjs +47 -0
  137. package/scripts/lib/remote-spawn-transfer.mjs +44 -14
  138. package/scripts/lib/skill-state.mjs +220 -0
  139. package/scripts/lib/skill-template.mjs +36 -11
  140. package/scripts/lib/team.mjs +75 -0
  141. package/scripts/lib/timeout.mjs +66 -0
  142. package/scripts/lib/tmp.mjs +26 -0
  143. package/scripts/lib/toml.mjs +113 -0
@@ -1,7 +1,6 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { mkdirSync, writeFileSync } from "node:fs";
2
3
  import { dirname, join } from "node:path";
3
- import { randomUUID } from "node:crypto";
4
- import { homedir } from "node:os";
5
4
 
6
5
  import {
7
6
  CONTEXT_MONITOR_CACHE_PATH,
@@ -11,9 +10,40 @@ import {
11
10
  import { clampPercent, formatTokenCount, readJsonMigrate } from "./utils.mjs";
12
11
 
13
12
  const DEFAULT_CONTEXT_LIMIT = 200_000;
13
+ const MILLION_CONTEXT_LIMIT = 1_000_000;
14
14
  const MAX_CAPTURE_BYTES = 256 * 1024;
15
15
  const MAX_TOP_KEYS = 20;
16
16
 
17
+ // stdin 이 context_window_size 를 제공하지 않을 때 모델 ID 로 한도를 추정한다.
18
+ // Anthropic 공식 문서(2026-04 기준): Opus 4.7 / Opus 4.6 / Sonnet 4.6 = 1M,
19
+ // Sonnet 4.5 / Haiku 4.5 = 200K. 그 외 모델은 [1m] suffix 로 opt-in 가능.
20
+ const MODEL_HINT_1M_PREFIXES = [
21
+ "claude-opus-4-7",
22
+ "claude-opus-4-6",
23
+ "claude-sonnet-4-6",
24
+ ];
25
+
26
+ function normalizeModelId(modelId) {
27
+ if (!modelId) return "";
28
+ return String(modelId).toLowerCase();
29
+ }
30
+
31
+ function resolveModelLimit(modelId) {
32
+ const id = normalizeModelId(modelId);
33
+ if (!id) return DEFAULT_CONTEXT_LIMIT;
34
+ if (id.includes("[1m]")) return MILLION_CONTEXT_LIMIT;
35
+ for (const prefix of MODEL_HINT_1M_PREFIXES) {
36
+ if (id.startsWith(prefix)) return MILLION_CONTEXT_LIMIT;
37
+ }
38
+ return DEFAULT_CONTEXT_LIMIT;
39
+ }
40
+
41
+ export function shouldSuppressInfoOnlyContextStatus(modelId) {
42
+ const id = normalizeModelId(modelId);
43
+ if (!id) return false;
44
+ return id.startsWith("claude-opus-4-7") || id.includes("[1m]");
45
+ }
46
+
17
47
  const WARNING_LEVELS = Object.freeze({
18
48
  ok: { min: 0, message: "" },
19
49
  info: { min: 60, message: "컨텍스트 절반 이상 사용" },
@@ -72,21 +102,24 @@ function normalizeUsage(usage) {
72
102
  const input = Number(usage.input_tokens ?? usage.inputTokens ?? 0);
73
103
  const output = Number(usage.output_tokens ?? usage.outputTokens ?? 0);
74
104
  const cacheCreation = Number(
75
- usage.cache_creation_input_tokens
76
- ?? usage.cacheCreationInputTokens
77
- ?? usage.cache_creation_tokens
78
- ?? 0,
105
+ usage.cache_creation_input_tokens ??
106
+ usage.cacheCreationInputTokens ??
107
+ usage.cache_creation_tokens ??
108
+ 0,
79
109
  );
80
110
  const cacheRead = Number(
81
- usage.cache_read_input_tokens
82
- ?? usage.cacheReadInputTokens
83
- ?? usage.cache_read_tokens
84
- ?? 0,
111
+ usage.cache_read_input_tokens ??
112
+ usage.cacheReadInputTokens ??
113
+ usage.cache_read_tokens ??
114
+ 0,
85
115
  );
86
- const totalCandidate = Number(usage.total_tokens ?? usage.totalTokens ?? Number.NaN);
87
- const total = Number.isFinite(totalCandidate) && totalCandidate > 0
88
- ? totalCandidate
89
- : input + output + cacheCreation + cacheRead;
116
+ const totalCandidate = Number(
117
+ usage.total_tokens ?? usage.totalTokens ?? Number.NaN,
118
+ );
119
+ const total =
120
+ Number.isFinite(totalCandidate) && totalCandidate > 0
121
+ ? totalCandidate
122
+ : input + output + cacheCreation + cacheRead;
90
123
  if (!Number.isFinite(total) || total <= 0) return null;
91
124
  return {
92
125
  input: Math.max(0, Math.round(input)),
@@ -159,7 +192,10 @@ function extractFileKeys(args) {
159
192
  function detectSkillHints(payloadText) {
160
193
  if (!payloadText) return [];
161
194
  const matches = payloadText.match(/\$[a-z0-9_-]+/gi) || [];
162
- return Array.from(new Set(matches.map((m) => m.replace(/^\$/, "")))).slice(0, 5);
195
+ return Array.from(new Set(matches.map((m) => m.replace(/^\$/, "")))).slice(
196
+ 0,
197
+ 5,
198
+ );
163
199
  }
164
200
 
165
201
  export function estimateTokens(input) {
@@ -172,21 +208,34 @@ export function parseUsageFromPayload(payload) {
172
208
 
173
209
  export function classifyContextThreshold(percent) {
174
210
  const p = clampThresholdPercent(percent);
175
- if (p >= WARNING_LEVELS.critical.min) return { level: "critical", message: WARNING_LEVELS.critical.message };
176
- if (p >= WARNING_LEVELS.warn.min) return { level: "warn", message: WARNING_LEVELS.warn.message };
177
- if (p >= WARNING_LEVELS.info.min) return { level: "info", message: WARNING_LEVELS.info.message };
211
+ if (p >= WARNING_LEVELS.critical.min)
212
+ return { level: "critical", message: WARNING_LEVELS.critical.message };
213
+ if (p >= WARNING_LEVELS.warn.min)
214
+ return { level: "warn", message: WARNING_LEVELS.warn.message };
215
+ if (p >= WARNING_LEVELS.info.min)
216
+ return { level: "info", message: WARNING_LEVELS.info.message };
178
217
  return { level: "ok", message: "" };
179
218
  }
180
219
 
181
220
  export function formatContextUsage(usedTokens, limitTokens, percent = null) {
182
221
  const used = Math.max(0, Math.round(Number(usedTokens) || 0));
183
- const limit = Math.max(1, Math.round(Number(limitTokens) || DEFAULT_CONTEXT_LIMIT));
184
- const pct = percent == null ? clampPercent((used / limit) * 100) : clampPercent(percent);
222
+ const limit = Math.max(
223
+ 1,
224
+ Math.round(Number(limitTokens) || DEFAULT_CONTEXT_LIMIT),
225
+ );
226
+ const pct =
227
+ percent == null
228
+ ? clampPercent((used / limit) * 100)
229
+ : clampPercent(percent);
185
230
  return `${formatTokenCount(used)}/${formatTokenCount(limit)} (${pct}%)`;
186
231
  }
187
232
 
188
233
  export function readContextMonitorSnapshot() {
189
- return readJsonMigrate(CONTEXT_MONITOR_CACHE_PATH, CONTEXT_MONITOR_LEGACY_PATH, null);
234
+ return readJsonMigrate(
235
+ CONTEXT_MONITOR_CACHE_PATH,
236
+ CONTEXT_MONITOR_LEGACY_PATH,
237
+ null,
238
+ );
190
239
  }
191
240
 
192
241
  function getStdinContextUsage(stdin) {
@@ -194,9 +243,10 @@ function getStdinContextUsage(stdin) {
194
243
  const nativePercent = Number(stdin?.context_window?.used_percentage);
195
244
  const usage = stdin?.context_window?.current_usage || {};
196
245
  const explicitUsed = Number(usage.total_tokens || 0);
197
- const calculatedUsed = Number(usage.input_tokens || 0)
198
- + Number(usage.cache_creation_input_tokens || 0)
199
- + Number(usage.cache_read_input_tokens || 0);
246
+ const calculatedUsed =
247
+ Number(usage.input_tokens || 0) +
248
+ Number(usage.cache_creation_input_tokens || 0) +
249
+ Number(usage.cache_read_input_tokens || 0);
200
250
  const usedTokens = explicitUsed > 0 ? explicitUsed : calculatedUsed;
201
251
 
202
252
  if (limitTokens > 0 && usedTokens > 0) {
@@ -220,30 +270,50 @@ function getStdinContextUsage(stdin) {
220
270
  return null;
221
271
  }
222
272
 
273
+ export function deriveContextLimit(stdin) {
274
+ const explicit = Number(stdin?.context_window?.context_window_size || 0);
275
+ const modelHint = resolveModelLimit(stdin?.model?.id ?? stdin?.model);
276
+ return Math.max(explicit, modelHint);
277
+ }
278
+
223
279
  export function buildContextUsageView(stdin, snapshot = null) {
224
280
  const stdinUsage = getStdinContextUsage(stdin);
225
281
  const monitor = snapshot || readContextMonitorSnapshot();
226
- const fallbackLimit = Number(monitor?.limitTokens || DEFAULT_CONTEXT_LIMIT);
227
-
228
- const usedTokens = stdinUsage?.usedTokens
229
- ?? Number(monitor?.usedTokens || 0);
230
- const limitTokens = stdinUsage?.limitTokens
231
- ?? Math.max(1, fallbackLimit);
232
- const percent = stdinUsage?.percent
233
- ?? (limitTokens > 0 ? clampPercent((usedTokens / limitTokens) * 100) : 0);
282
+ const modelId = stdin?.model?.id ?? stdin?.model;
283
+ const modelHintLimit = resolveModelLimit(modelId);
284
+ const monitorLimit = Number(monitor?.limitTokens || 0);
285
+ const stdinLimit = stdinUsage?.limitTokens;
286
+ const limitTokens =
287
+ stdinLimit != null && stdinLimit > 0
288
+ ? Math.max(1, stdinLimit)
289
+ : modelId
290
+ ? Math.max(1, monitorLimit, modelHintLimit)
291
+ : Math.max(1, monitorLimit || modelHintLimit);
292
+
293
+ const usedTokens = stdinUsage?.usedTokens ?? Number(monitor?.usedTokens || 0);
294
+ const percent =
295
+ limitTokens > 0 ? clampPercent((usedTokens / limitTokens) * 100) : 0;
234
296
 
235
297
  const warning = classifyContextThreshold(percent);
298
+ const showInfoOnlyStatus = !(
299
+ warning.level === "info" && shouldSuppressInfoOnlyContextStatus(modelId)
300
+ );
236
301
  return {
237
302
  usedTokens,
238
303
  limitTokens,
239
304
  percent,
240
305
  display: formatContextUsage(usedTokens, limitTokens, percent),
241
306
  warningLevel: warning.level,
242
- warningMessage: warning.message,
243
- warningTag: warning.level === "warn" ? "⚠ 압축 권장"
244
- : warning.level === "critical" ? "‼ 분할 권장"
245
- : warning.level === "info" ? "ℹ 절반 이상 사용"
246
- : "",
307
+ warningMessage: showInfoOnlyStatus ? warning.message : "",
308
+ warningTag: !showInfoOnlyStatus
309
+ ? ""
310
+ : warning.level === "warn"
311
+ ? "⚠ 압축 권장"
312
+ : warning.level === "critical"
313
+ ? "‼ 분할 권장"
314
+ : warning.level === "info"
315
+ ? "ℹ 절반 이상 사용"
316
+ : "",
247
317
  source: stdinUsage?.source || (monitor ? "monitor" : "none"),
248
318
  };
249
319
  }
@@ -295,7 +365,10 @@ export function createContextMonitor(options = {}) {
295
365
  const percent = clampPercent((state.usedTokens / state.limitTokens) * 100);
296
366
  const warning = classifyContextThreshold(percent);
297
367
  const ts = new Date().toISOString().replace(/[:.]/g, "-");
298
- const reportPath = join(logsDir, `context-usage-${state.sessionId}-${ts}.json`);
368
+ const reportPath = join(
369
+ logsDir,
370
+ `context-usage-${state.sessionId}-${ts}.json`,
371
+ );
299
372
  safeWriteJson(reportPath, {
300
373
  sessionId: state.sessionId,
301
374
  reason,
@@ -329,17 +402,31 @@ export function createContextMonitor(options = {}) {
329
402
  toolName = "",
330
403
  } = {}) => {
331
404
  const started = process.hrtime.bigint();
332
- const reqObj = typeof requestBody === "object" ? requestBody : safeJsonParse(String(requestBody || ""));
333
- const resObj = typeof responseBody === "object" ? responseBody : safeJsonParse(String(responseBody || ""));
405
+ const reqObj =
406
+ typeof requestBody === "object"
407
+ ? requestBody
408
+ : safeJsonParse(String(requestBody || ""));
409
+ const resObj =
410
+ typeof responseBody === "object"
411
+ ? responseBody
412
+ : safeJsonParse(String(responseBody || ""));
334
413
 
335
414
  const usage = parseUsageFromPayload(resObj);
336
- const requestTokens = requestBytes > 0 ? toTokenEstimate(requestBytes) : toTokenEstimate(requestBody);
337
- const responseTokens = usage?.total ?? (responseBytes > 0 ? toTokenEstimate(responseBytes) : toTokenEstimate(responseBody));
415
+ const requestTokens =
416
+ requestBytes > 0
417
+ ? toTokenEstimate(requestBytes)
418
+ : toTokenEstimate(requestBody);
419
+ const responseTokens =
420
+ usage?.total ??
421
+ (responseBytes > 0
422
+ ? toTokenEstimate(responseBytes)
423
+ : toTokenEstimate(responseBody));
338
424
  const totalTokens = Math.max(0, requestTokens + responseTokens);
339
425
 
340
426
  const method = reqObj?.method || reqObj?.params?.name || "";
341
427
  const name = toolName || reqObj?.params?.name || reqObj?.tool || "";
342
- const args = reqObj?.params?.arguments || reqObj?.arguments || reqObj?.params || {};
428
+ const args =
429
+ reqObj?.params?.arguments || reqObj?.arguments || reqObj?.params || {};
343
430
  const payloadText = normalizeText(requestBody).slice(0, MAX_CAPTURE_BYTES);
344
431
  const skills = detectSkillHints(payloadText);
345
432
  const files = extractFileKeys(args);
@@ -383,7 +470,9 @@ export function createContextMonitor(options = {}) {
383
470
 
384
471
  if (registerExitHooks) {
385
472
  const flushOnExit = () => {
386
- try { flush("process.exit"); } catch {}
473
+ try {
474
+ flush("process.exit");
475
+ } catch {}
387
476
  };
388
477
  process.once("exit", flushOnExit);
389
478
  process.once("SIGINT", flushOnExit);
@@ -5,46 +5,78 @@
5
5
  // 각 모듈에서 색상, 터미널, 프로바이더, 렌더러를 가져와 조합한다.
6
6
  // ============================================================================
7
7
 
8
- import { DIM, RESET, bold, dim, green, claudeOrange, codexWhite, geminiBlue, colorByPercent } from "./colors.mjs";
9
8
  import {
10
- QOS_PATH, ACCOUNTS_CONFIG_PATH, ACCOUNTS_STATE_PATH,
11
- CLAUDE_REFRESH_FLAG, CODEX_REFRESH_FLAG,
12
- GEMINI_REFRESH_FLAG, GEMINI_SESSION_REFRESH_FLAG,
13
- GEMINI_PRO_POOL, GEMINI_FLASH_POOL,
9
+ bold,
10
+ claudeOrange,
11
+ codexWhite,
12
+ DIM,
13
+ dim,
14
+ geminiBlue,
15
+ green,
16
+ RESET,
17
+ } from "./colors.mjs";
18
+ import {
19
+ ACCOUNTS_CONFIG_PATH,
20
+ ACCOUNTS_STATE_PATH,
21
+ CLAUDE_REFRESH_FLAG,
22
+ CODEX_REFRESH_FLAG,
23
+ GEMINI_FLASH_POOL,
24
+ GEMINI_PRO_POOL,
25
+ GEMINI_REFRESH_FLAG,
26
+ GEMINI_SESSION_REFRESH_FLAG,
27
+ QOS_PATH,
28
+ TFX_PREFLIGHT_CACHE_PATH,
14
29
  } from "./constants.mjs";
15
30
  import {
16
- readJson, readStdinJson, getProviderAccountId, getCliArgValue,
17
- } from "./utils.mjs";
18
- import { selectTier } from "./terminal.mjs";
19
-
31
+ buildContextUsageView,
32
+ deriveContextLimit,
33
+ } from "./context-monitor.mjs";
34
+ import { getMissionBoardState } from "./mission-board.mjs";
20
35
  // Claude provider
21
36
  import {
22
- readClaudeUsageSnapshot, scheduleClaudeUsageRefresh, fetchClaudeUsage,
37
+ fetchClaudeUsage,
23
38
  readClaudeContextSnapshot,
39
+ readClaudeUsageSnapshot,
40
+ scheduleClaudeUsageRefresh,
24
41
  } from "./providers/claude.mjs";
25
- import { buildContextUsageView } from "./context-monitor.mjs";
26
-
27
42
  // Codex provider
28
43
  import {
29
- getCodexEmail, readCodexRateLimitSnapshot,
30
- refreshCodexRateLimitsCache, scheduleCodexRateLimitRefresh,
44
+ getCodexEmail,
45
+ readCodexRateLimitSnapshot,
46
+ refreshCodexRateLimitsCache,
47
+ scheduleCodexRateLimitRefresh,
31
48
  } from "./providers/codex.mjs";
32
-
33
49
  // Gemini provider
34
50
  import {
35
- getGeminiEmail, buildGeminiAuthContext,
36
- readGeminiQuotaSnapshot, readGeminiSessionSnapshot,
37
- fetchGeminiQuota, refreshGeminiSessionCache,
38
- scheduleGeminiQuotaRefresh, scheduleGeminiSessionRefresh,
51
+ buildGeminiAuthContext,
52
+ fetchGeminiQuota,
53
+ getGeminiEmail,
54
+ readGeminiQuotaSnapshot,
55
+ readGeminiSessionSnapshot,
56
+ refreshGeminiSessionCache,
57
+ scheduleGeminiQuotaRefresh,
58
+ scheduleGeminiSessionRefresh,
39
59
  } from "./providers/gemini.mjs";
40
-
41
60
  // Renderers
42
61
  import {
43
- getClaudeRows, getProviderRow, getTeamRow,
44
- renderAlignedRows, getMicroLine,
45
- readLatestBenchmarkDiff, formatTokenSummary,
46
- readTokenSavings, readSvAccumulator,
62
+ formatTokenSummary,
63
+ getClaudeRows,
64
+ getMicroLine,
65
+ getProviderRow,
66
+ getTeamRow,
67
+ readLatestBenchmarkDiff,
68
+ readSvAccumulator,
69
+ readTokenSavings,
70
+ renderAlignedRows,
71
+ renderMissionBoard,
47
72
  } from "./renderers.mjs";
73
+ import { selectTier } from "./terminal.mjs";
74
+ import {
75
+ getCliArgValue,
76
+ getProviderAccountId,
77
+ readJson,
78
+ readStdinJson,
79
+ } from "./utils.mjs";
48
80
 
49
81
  // ============================================================================
50
82
  // 메인
@@ -78,6 +110,11 @@ async function main() {
78
110
  const stdinPromise = readStdinJson();
79
111
 
80
112
  const qosProfile = readJson(QOS_PATH, { providers: {} });
113
+ const preflightCache = readJson(TFX_PREFLIGHT_CACHE_PATH, null);
114
+ const antigravityReady =
115
+ process.env.TFX_ANTIGRAVITY_OK === "1" ||
116
+ preflightCache?.antigravity?.ok === true ||
117
+ preflightCache?.available_agents?.includes("antigravity");
81
118
  const accountsConfig = readJson(ACCOUNTS_CONFIG_PATH, { providers: {} });
82
119
  const accountsState = readJson(ACCOUNTS_STATE_PATH, { providers: {} });
83
120
  const claudeUsageSnapshot = readClaudeUsageSnapshot();
@@ -85,11 +122,18 @@ async function main() {
85
122
  if (claudeUsageSnapshot.shouldRefresh) {
86
123
  scheduleClaudeUsageRefresh();
87
124
  }
88
- const geminiAccountId = getProviderAccountId("gemini", accountsConfig, accountsState);
125
+ const geminiAccountId = getProviderAccountId(
126
+ "gemini",
127
+ accountsConfig,
128
+ accountsState,
129
+ );
89
130
  const codexSnapshot = readCodexRateLimitSnapshot();
90
131
  const geminiSessionSnapshot = readGeminiSessionSnapshot();
91
132
  const geminiAuthContext = buildGeminiAuthContext(geminiAccountId);
92
- const geminiQuotaSnapshot = readGeminiQuotaSnapshot(geminiAccountId, geminiAuthContext);
133
+ const geminiQuotaSnapshot = readGeminiQuotaSnapshot(
134
+ geminiAccountId,
135
+ geminiAuthContext,
136
+ );
93
137
  if (codexSnapshot.shouldRefresh) {
94
138
  scheduleCodexRateLimitRefresh();
95
139
  }
@@ -103,25 +147,30 @@ async function main() {
103
147
  // 실측 데이터 추출
104
148
  const stdin = await stdinPromise;
105
149
  const contextView = buildContextUsageView(stdin, contextSnapshot);
150
+ const claudeUsage = claudeUsageSnapshot.data;
106
151
  const codexEmail = getCodexEmail();
107
152
  const geminiEmail = getGeminiEmail();
108
153
  const codexBuckets = codexSnapshot.buckets;
109
154
  const geminiSession = geminiSessionSnapshot.session;
110
155
  const geminiQuota = geminiQuotaSnapshot.quota;
156
+ const missionBoardState = await getMissionBoardState();
111
157
 
112
158
  // 누적 절약 데이터 읽기
113
159
  const svSavings = readTokenSavings();
114
160
  const svAccumulator = readSvAccumulator();
115
- const totalCostSaved = svSavings?.totalSaved || svAccumulator?.totalCostSaved || 0;
161
+ const _totalCostSaved =
162
+ svSavings?.totalSaved || svAccumulator?.totalCostSaved || 0;
116
163
 
117
164
  // 세션/누적 토큰 → context 대비 절약 배수 (개별 provider sv%)
118
- const ctxCapacity = stdin?.context_window?.context_window_size || 200000;
165
+ const ctxCapacity = deriveContextLimit(stdin);
119
166
  let codexSv = null;
120
167
  if (svAccumulator?.codex?.tokens > 0) {
121
168
  codexSv = svAccumulator.codex.tokens / ctxCapacity;
122
169
  } else if (codexBuckets) {
123
- const main = codexBuckets.codex || codexBuckets[Object.keys(codexBuckets)[0]];
124
- if (main?.tokens?.total_tokens) codexSv = main.tokens.total_tokens / ctxCapacity;
170
+ const main =
171
+ codexBuckets.codex || codexBuckets[Object.keys(codexBuckets)[0]];
172
+ if (main?.tokens?.total_tokens)
173
+ codexSv = main.tokens.total_tokens / ctxCapacity;
125
174
  }
126
175
  let geminiSv = null;
127
176
  if (svAccumulator?.gemini?.tokens > 0) {
@@ -134,12 +183,16 @@ async function main() {
134
183
  // Gemini: 3풀 버킷 추출 (Pro/Flash/Lite — 각 풀 내 모델들은 쿼터 공유)
135
184
  const geminiModel = geminiSession?.model || "gemini-3-flash-preview";
136
185
  const geminiBuckets = geminiQuota?.buckets || [];
137
- const geminiBucket = geminiBuckets.find((b) => b.modelId === geminiModel)
138
- || geminiBuckets.find((b) => b.modelId === "gemini-3-flash-preview")
139
- || null;
140
- const geminiProBucket = geminiBuckets.find((b) => GEMINI_PRO_POOL.has(b.modelId)) || null;
141
- const geminiFlashBucket = geminiBuckets.find((b) => GEMINI_FLASH_POOL.has(b.modelId)) || null;
142
- const geminiLiteBucket = geminiBuckets.find((b) => b.modelId?.includes("flash-lite")) || null;
186
+ const geminiBucket =
187
+ geminiBuckets.find((b) => b.modelId === geminiModel) ||
188
+ geminiBuckets.find((b) => b.modelId === "gemini-3-flash-preview") ||
189
+ null;
190
+ const geminiProBucket =
191
+ geminiBuckets.find((b) => GEMINI_PRO_POOL.has(b.modelId)) || null;
192
+ const geminiFlashBucket =
193
+ geminiBuckets.find((b) => GEMINI_FLASH_POOL.has(b.modelId)) || null;
194
+ const geminiLiteBucket =
195
+ geminiBuckets.find((b) => b.modelId?.includes("flash-lite")) || null;
143
196
 
144
197
  // 합산 절약: Codex+Gemini sv% 합산 (컨텍스트 대비 위임 토큰 비율)
145
198
  const combinedSvPct = Math.round(((codexSv ?? 0) + (geminiSv ?? 0)) * 100);
@@ -149,32 +202,76 @@ async function main() {
149
202
 
150
203
  // nano tier: 1줄 모드 (극소 폭 또는 알림 배너 대응)
151
204
  if (CURRENT_TIER === "nano") {
152
- const microLine = getMicroLine(contextView, claudeUsageSnapshot.data, codexBuckets,
153
- geminiSession, geminiBucket, combinedSvPct);
205
+ const microLine = getMicroLine(
206
+ contextView,
207
+ claudeUsage,
208
+ codexBuckets,
209
+ geminiSession,
210
+ geminiBucket,
211
+ combinedSvPct,
212
+ antigravityReady ? "a" : "g",
213
+ );
154
214
  process.stdout.write(`\x1b[0m${microLine}\n`);
155
215
  return;
156
216
  }
157
217
 
158
- const codexQuotaData = codexBuckets ? { type: "codex", buckets: codexBuckets } : null;
218
+ const codexQuotaData = codexBuckets
219
+ ? { type: "codex", buckets: codexBuckets }
220
+ : null;
159
221
  const geminiQuotaData = {
160
222
  type: "gemini",
161
223
  quotaBucket: geminiBucket,
162
- pools: { pro: geminiProBucket, flash: geminiFlashBucket, lite: geminiLiteBucket },
224
+ pools: {
225
+ pro: geminiProBucket,
226
+ flash: geminiFlashBucket,
227
+ lite: geminiLiteBucket,
228
+ },
163
229
  session: geminiSession,
164
230
  };
165
231
 
166
232
  const rows = [
167
- ...getClaudeRows(CURRENT_TIER, contextView, claudeUsageSnapshot.data, combinedSvPct),
168
- getProviderRow(CURRENT_TIER, "codex", "x", codexWhite, qosProfile, accountsConfig, accountsState,
169
- codexQuotaData, codexEmail, codexSv, null),
170
- getProviderRow(CURRENT_TIER, "gemini", "g", geminiBlue, qosProfile, accountsConfig, accountsState,
171
- geminiQuotaData, geminiEmail, geminiSv, null),
233
+ ...getClaudeRows(CURRENT_TIER, contextView, claudeUsage, combinedSvPct),
234
+ getProviderRow(
235
+ CURRENT_TIER,
236
+ "codex",
237
+ "x",
238
+ codexWhite,
239
+ qosProfile,
240
+ accountsConfig,
241
+ accountsState,
242
+ codexQuotaData,
243
+ codexEmail,
244
+ codexSv,
245
+ null,
246
+ ),
247
+ getProviderRow(
248
+ CURRENT_TIER,
249
+ antigravityReady ? "antigravity" : "gemini",
250
+ antigravityReady ? "a" : "g",
251
+ geminiBlue,
252
+ qosProfile,
253
+ accountsConfig,
254
+ accountsState,
255
+ geminiQuotaData,
256
+ antigravityReady ? "anti" : geminiEmail,
257
+ geminiSv,
258
+ null,
259
+ ),
172
260
  ];
173
261
 
174
262
  // tfx-multi 활성 시 팀 상태 행 추가 (v2.2)
175
263
  const teamRow = getTeamRow(CURRENT_TIER);
176
264
  if (teamRow) rows.push(teamRow);
177
265
 
266
+ const missionBoard = renderMissionBoard(missionBoardState);
267
+ if (missionBoard) {
268
+ rows.push({
269
+ prefix: bold(claudeOrange("\u25B2")),
270
+ left: missionBoard,
271
+ right: "",
272
+ });
273
+ }
274
+
178
275
  // 최근 벤치마크 diff → 토큰 요약 행 추가
179
276
  const latestDiff = readLatestBenchmarkDiff();
180
277
  if (latestDiff) {
@@ -186,10 +283,14 @@ async function main() {
186
283
 
187
284
  // 비활성 프로바이더 dim 처리: 데이터 없으면 전체 줄 dim
188
285
  const codexActive = codexBuckets != null;
189
- const geminiActive = (geminiSession?.total || 0) > 0 || geminiBucket != null
190
- || geminiProBucket != null || geminiFlashBucket != null;
286
+ const geminiActive =
287
+ antigravityReady ||
288
+ (geminiSession?.total || 0) > 0 ||
289
+ geminiBucket != null ||
290
+ geminiProBucket != null ||
291
+ geminiFlashBucket != null;
191
292
 
192
- let outputLines = renderAlignedRows(rows);
293
+ const outputLines = renderAlignedRows(rows);
193
294
 
194
295
  // 비활성 줄 dim 래핑 (rows 순서: [claude, codex, gemini])
195
296
  if (outputLines.length >= 3) {
@@ -201,10 +302,12 @@ async function main() {
201
302
  const contextPercent = contextView.percent;
202
303
  const leadingBreaks = contextPercent >= 85 ? "\n\n" : "\n";
203
304
  // 줄별 RESET: Claude Code TUI 스타일 간섭 방지 (색상 밝기 버그 수정)
204
- const resetedLines = outputLines.map(line => `\x1b[0m${line}`);
305
+ const resetedLines = outputLines.map((line) => `\x1b[0m${line}`);
205
306
  process.stdout.write(`${leadingBreaks}${resetedLines.join("\n")}\n`);
206
307
  }
207
308
 
208
309
  main().catch(() => {
209
- process.stdout.write(`\x1b[0m${bold(claudeOrange("c"))}: ${dim("5h:")}${green("0%")} ${dim("(n/a)")} ${dim("1w:")}${green("0%")} ${dim("(n/a)")} ${dim("|")} ${dim("ctx:")}${green("0%")}\n`);
310
+ process.stdout.write(
311
+ `\x1b[0m${bold(claudeOrange("c"))}: ${dim("5h:")}${green("0%")} ${dim("(n/a)")} ${dim("1w:")}${green("0%")} ${dim("(n/a)")} ${dim("|")} ${dim("ctx:")}${green("0%")}\n`,
312
+ );
210
313
  });
@@ -0,0 +1,56 @@
1
+ // ============================================================================
2
+ // HUD Mission Board — .omc/state/sessions/ 기반 에이전트 진행률 집계
3
+ // ============================================================================
4
+ import { readdir, readFile } from "node:fs/promises";
5
+ import { homedir } from "node:os";
6
+ import { join } from "node:path";
7
+
8
+ const SESSIONS_DIR = join(homedir(), ".omc", "state", "sessions");
9
+
10
+ /**
11
+ * .omc/state/sessions/ 디렉토리를 읽어 팀 상태를 반환한다.
12
+ * @returns {{ agents: Array<{name: string, status: string, progress: number}>, dagLevel: number, totalProgress: number } | null}
13
+ */
14
+ export async function getMissionBoardState(sessionsDir = SESSIONS_DIR) {
15
+ let entries;
16
+ try {
17
+ entries = await readdir(sessionsDir);
18
+ } catch {
19
+ return null;
20
+ }
21
+
22
+ const jsonFiles = entries.filter((f) => f.endsWith(".json"));
23
+ if (jsonFiles.length === 0) return null;
24
+
25
+ const agents = [];
26
+ for (const file of jsonFiles) {
27
+ let data;
28
+ try {
29
+ const raw = await readFile(join(sessionsDir, file), "utf8");
30
+ data = JSON.parse(raw);
31
+ } catch {
32
+ continue;
33
+ }
34
+
35
+ const name = data.name ?? file.replace(/\.json$/, "");
36
+ const status = data.status ?? "idle";
37
+ const progress = typeof data.progress === "number" ? data.progress : 0;
38
+ agents.push({ name, status, progress });
39
+ }
40
+
41
+ if (agents.length === 0) return null;
42
+
43
+ const totalProgress =
44
+ agents.length > 0
45
+ ? Math.round(
46
+ agents.reduce((sum, a) => sum + a.progress, 0) / agents.length,
47
+ )
48
+ : 0;
49
+
50
+ return {
51
+ agents,
52
+ // TODO: derive dagLevel from real mission dependency metadata instead of hardcoding 0.
53
+ dagLevel: 0,
54
+ totalProgress,
55
+ };
56
+ }