triflux 10.9.19 → 10.9.21

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 (113) hide show
  1. package/CLAUDE.md +212 -0
  2. package/hub/lib/bash-path.mjs +73 -0
  3. package/hub/team/dashboard-open.mjs +1 -68
  4. package/hub/team/native-supervisor.mjs +9 -2
  5. package/hub/team/psmux.mjs +5 -13
  6. package/hub/team/session.mjs +6 -26
  7. package/hub/team/swarm-hypervisor.mjs +205 -27
  8. package/hub/team/synapse-http.mjs +1 -0
  9. package/hub/team/tui-core.mjs +292 -0
  10. package/hub/team/tui-lite.mjs +20 -154
  11. package/hub/team/tui-synapse.mjs +213 -0
  12. package/hub/team/tui-widgets.mjs +262 -0
  13. package/hub/team/tui.mjs +159 -255
  14. package/hub/workers/delegator-mcp.mjs +2 -2
  15. package/package.json +21 -62
  16. package/references/hosts.json +46 -0
  17. package/scripts/__tests__/keyword-detector.test.mjs +4 -4
  18. package/scripts/cross-review-gate.mjs +13 -0
  19. package/scripts/remote-spawn.mjs +11 -46
  20. package/scripts/session-spawn-helper.mjs +8 -21
  21. package/scripts/test-tfx-route-no-claude-native.mjs +4 -2
  22. package/scripts/tfx-route.sh +13 -0
  23. package/skills/tfx-deep-interview/SKILL.md +6 -6
  24. package/skills/tfx-deep-interview/SKILL.md.tmpl +6 -6
  25. package/skills/tfx-index/SKILL.md +1 -1
  26. package/skills/tfx-index/SKILL.md.tmpl +1 -1
  27. package/skills/tfx-interview/SKILL.md +9 -9
  28. package/skills/tfx-interview/SKILL.md.tmpl +9 -9
  29. package/skills/tfx-plan/SKILL.md +1 -1
  30. package/skills/tfx-plan/SKILL.md.tmpl +1 -1
  31. package/skills/tfx-research/SKILL.md +1 -1
  32. package/skills/tfx-research/SKILL.md.tmpl +1 -1
  33. package/skills/tfx-workspace/async-tests/run-tests.sh +203 -0
  34. package/skills/tfx-workspace/evals/evals.json +79 -0
  35. package/skills/tfx-workspace/iteration-1/benchmark.json +524 -0
  36. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/eval_metadata.json +11 -0
  37. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/grading.json +25 -0
  38. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/outputs/analysis.md +154 -0
  39. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/timing.json +5 -0
  40. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/grading.json +25 -0
  41. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/outputs/analysis.md +126 -0
  42. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/timing.json +5 -0
  43. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/eval_metadata.json +11 -0
  44. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/grading.json +25 -0
  45. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/outputs/analysis.md +119 -0
  46. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/timing.json +5 -0
  47. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/grading.json +25 -0
  48. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/outputs/analysis.md +115 -0
  49. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/timing.json +5 -0
  50. package/skills/tfx-workspace/iteration-1/hub-start-sequence/eval_metadata.json +10 -0
  51. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/grading.json +20 -0
  52. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/outputs/analysis.md +86 -0
  53. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/timing.json +5 -0
  54. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/grading.json +20 -0
  55. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/outputs/analysis.md +81 -0
  56. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/timing.json +5 -0
  57. package/skills/tfx-workspace/iteration-1/multi-team-creation/eval_metadata.json +12 -0
  58. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/grading.json +30 -0
  59. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/outputs/analysis.md +316 -0
  60. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/timing.json +5 -0
  61. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/grading.json +30 -0
  62. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/outputs/analysis.md +352 -0
  63. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/timing.json +5 -0
  64. package/skills/tfx-workspace/iteration-1/review.html +1325 -0
  65. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/eval_metadata.json +12 -0
  66. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/grading.json +30 -0
  67. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/outputs/analysis.md +97 -0
  68. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/timing.json +5 -0
  69. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/grading.json +30 -0
  70. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/outputs/analysis.md +94 -0
  71. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/timing.json +5 -0
  72. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/eval_metadata.json +12 -0
  73. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/grading.json +30 -0
  74. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/outputs/analysis.md +209 -0
  75. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/timing.json +5 -0
  76. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/grading.json +30 -0
  77. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/outputs/analysis.md +193 -0
  78. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/timing.json +5 -0
  79. package/skills/tfx-workspace/iteration-2/benchmark.json +144 -0
  80. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/eval_metadata.json +13 -0
  81. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/grading.json +35 -0
  82. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/outputs/analysis.md +382 -0
  83. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/timing.json +5 -0
  84. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/grading.json +35 -0
  85. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/outputs/analysis.md +333 -0
  86. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/timing.json +5 -0
  87. package/skills/tfx-workspace/iteration-2/review.html +1325 -0
  88. package/skills/tfx-workspace/skill-snapshot/tfx-auto/SKILL.md +217 -0
  89. package/skills/tfx-workspace/skill-snapshot/tfx-auto-codex/SKILL.md +77 -0
  90. package/skills/tfx-workspace/skill-snapshot/tfx-codex/SKILL.md +65 -0
  91. package/skills/tfx-workspace/skill-snapshot/tfx-doctor/SKILL.md +94 -0
  92. package/skills/tfx-workspace/skill-snapshot/tfx-gemini/SKILL.md +82 -0
  93. package/skills/tfx-workspace/skill-snapshot/tfx-hub/SKILL.md +133 -0
  94. package/skills/tfx-workspace/skill-snapshot/tfx-multi/SKILL.md +426 -0
  95. package/skills/tfx-workspace/skill-snapshot/tfx-setup/SKILL.md +101 -0
  96. package/.claude-plugin/marketplace.json +0 -34
  97. package/.claude-plugin/plugin.json +0 -22
  98. package/config/mcp-registry.json +0 -29
  99. package/scripts/__tests__/release-governance.test.mjs +0 -148
  100. package/scripts/release/bump-version.mjs +0 -77
  101. package/scripts/release/check-sync.mjs +0 -51
  102. package/scripts/release/lib.mjs +0 -303
  103. package/scripts/release/prepare.mjs +0 -85
  104. package/scripts/release/publish.mjs +0 -87
  105. package/scripts/release/verify.mjs +0 -81
  106. package/scripts/release/version-manifest.json +0 -26
  107. package/tui/codex-profile.mjs +0 -457
  108. package/tui/core.mjs +0 -266
  109. package/tui/doctor.mjs +0 -375
  110. package/tui/gemini-profile.mjs +0 -299
  111. package/tui/monitor-data.mjs +0 -152
  112. package/tui/monitor.mjs +0 -339
  113. package/tui/setup.mjs +0 -598
package/hub/team/tui.mjs CHANGED
@@ -29,19 +29,40 @@ import {
29
29
  wcswidth,
30
30
  } from "./ansi.mjs";
31
31
  import { resolveAttachCommand } from "./session.mjs";
32
+ import {
33
+ clamp,
34
+ cliColor,
35
+ countStatuses,
36
+ FALLBACK_COLUMNS,
37
+ FALLBACK_ROWS,
38
+ formatTokens,
39
+ loadVersion,
40
+ normalizeWorkerState as coreNormalizeWorkerState,
41
+ runtimeStatus,
42
+ sanitizeFiles,
43
+ sanitizeFindings,
44
+ sanitizeOneLine,
45
+ sanitizeTextBlock,
46
+ statusColor,
47
+ stripCodeBlocks,
48
+ wrapLine,
49
+ wrapText as wrapTextAll,
50
+ } from "./tui-core.mjs";
51
+ import {
52
+ createPanelResizer,
53
+ createSearchState,
54
+ createTokenTracker,
55
+ createVimMotion,
56
+ } from "./tui-widgets.mjs";
57
+ import {
58
+ createMetricsCollector,
59
+ createSynapseEventStream,
60
+ renderMetricsTier1,
61
+ } from "./tui-synapse.mjs";
32
62
 
33
- // package.json에서 동적 로드 (실패 시 fallback)
34
- let VERSION = "7.x";
35
- try {
36
- const { createRequire } = await import("node:module");
37
- const require = createRequire(import.meta.url);
38
- VERSION = require("../../package.json").version;
39
- } catch {
40
- /* fallback */
41
- }
63
+ const VERSION = await loadVersion();
42
64
 
43
- const FALLBACK_COLUMNS = 100;
44
- const FALLBACK_ROWS = 30;
65
+ // FALLBACK_COLUMNS, FALLBACK_ROWS → tui-core.mjs에서 import
45
66
  const MIN_CARD_WIDTH = 28;
46
67
  const ATTACH_SESSION_NAME_PATTERN = /^[a-zA-Z0-9_.-]+$/u;
47
68
  const DEFAULT_ATTACH_TAB_TTL_MS = 30_000;
@@ -308,96 +329,7 @@ function _autoColumnCount(totalCols, workerCount) {
308
329
  return 1;
309
330
  }
310
331
 
311
- // ── 문자열 유틸 ──────────────────────────────────────────────────────────
312
- function clamp(value, min, max) {
313
- return Math.min(max, Math.max(min, value));
314
- }
315
-
316
- function stripCodeBlocks(text) {
317
- return (
318
- String(text || "")
319
- .replace(/\r/g, "")
320
- // fenced code blocks
321
- .replace(/```[\s\S]*?(?:```|$)/g, "\n")
322
- .replace(/^\s*```.*$/gm, "")
323
- // indented code blocks (4+ spaces or tab at line start)
324
- .replace(/^(?: {4}|\t).+$/gm, "")
325
- // shell prompts: PS C:\...>, >, $
326
- .replace(/^(?:PS\s+\S[^\n]*?>|>\s+|\$\s+)[^\n]*/gm, "")
327
- .trim()
328
- );
329
- }
330
-
331
- function sanitizeTextBlock(text, rawMode = false) {
332
- const normalized = rawMode
333
- ? String(text || "").replace(/\r/g, "")
334
- : stripCodeBlocks(text);
335
- return normalized
336
- .split("\n")
337
- .map((line) => line.trim())
338
- .filter(Boolean)
339
- .filter((line) => line !== "--- HANDOFF ---")
340
- .join("\n")
341
- .trim();
342
- }
343
-
344
- function sanitizeOneLine(text, fallback = "") {
345
- const normalized = sanitizeTextBlock(text).replace(/\s+/g, " ").trim();
346
- return normalized || fallback;
347
- }
348
-
349
- function sanitizeFiles(files) {
350
- if (!files) return [];
351
- const raw = Array.isArray(files) ? files : String(files).split(",");
352
- return raw.map((e) => sanitizeOneLine(e)).filter(Boolean);
353
- }
354
-
355
- function sanitizeFindings(findings) {
356
- if (!findings) return [];
357
- const raw = Array.isArray(findings)
358
- ? findings
359
- : sanitizeTextBlock(findings).split("\n");
360
- return raw.map((e) => sanitizeOneLine(e)).filter(Boolean);
361
- }
362
-
363
- function normalizeTokens(tokens) {
364
- if (tokens === null || tokens === undefined) return "";
365
- if (typeof tokens === "number" && Number.isFinite(tokens)) return tokens;
366
- const raw = sanitizeOneLine(tokens);
367
- if (!raw) return "";
368
- const match = raw.match(/(\d+(?:[.,]\d+)?\s*[kKmM]?)/);
369
- return match ? match[1].replace(/\s+/g, "").toLowerCase() : raw;
370
- }
371
-
372
- function formatTokens(tokens) {
373
- if (tokens === null || tokens === undefined || tokens === "") return "n/a";
374
- if (typeof tokens === "number" && Number.isFinite(tokens)) {
375
- if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}m`;
376
- if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}k`;
377
- return `${tokens}`;
378
- }
379
- return String(tokens);
380
- }
381
-
382
- // ── 색상 헬퍼 ─────────────────────────────────────────────────────────────
383
- function cliColor(cli) {
384
- if (cli === "gemini") return FG.gemini;
385
- if (cli === "claude") return FG.claude;
386
- if (cli === "codex") return FG.codex;
387
- return FG.white;
388
- }
389
-
390
- function runtimeStatus(st) {
391
- return st?.handoff?.status || st?.status || "pending";
392
- }
393
-
394
- function statusColor(status) {
395
- if (status === "ok" || status === "completed") return MOCHA.ok;
396
- if (status === "partial") return MOCHA.partial;
397
- if (status === "failed") return MOCHA.fail;
398
- if (status === "running" || status === "in_progress") return MOCHA.executing;
399
- return FG.muted;
400
- }
332
+ // 텍스트/상태/색상 유틸은 tui-core.mjs에서 import (위 참조)
401
333
 
402
334
  // ── MOCHA RGB (gradual fade 보간용) ──
403
335
  const MOCHA_RGB = {
@@ -505,37 +437,7 @@ function dedupeRole(role, name, cli) {
505
437
  return r;
506
438
  }
507
439
 
508
- // ── 텍스트 래핑 ──────────────────────────────────────────────────────────
509
- function wrapLine(text, width) {
510
- const limit = Math.max(8, width);
511
- const source = String(text || "").trim();
512
- if (!source) return [""];
513
- const words = source.split(/\s+/);
514
- const lines = [];
515
- let current = "";
516
- for (const word of words) {
517
- const candidate = current ? `${current} ${word}` : word;
518
- if (wcswidth(candidate) <= limit) {
519
- current = candidate;
520
- continue;
521
- }
522
- if (current) {
523
- lines.push(current);
524
- current = "";
525
- }
526
- if (wcswidth(word) <= limit) {
527
- current = word;
528
- continue;
529
- }
530
- let offset = 0;
531
- while (offset < word.length) {
532
- lines.push(word.slice(offset, offset + limit));
533
- offset += limit;
534
- }
535
- }
536
- if (current) lines.push(current);
537
- return lines.length > 0 ? lines : [source.slice(0, limit)];
538
- }
440
+ // wrapLine, wrapTextAll tui-core.mjs에서 import
539
441
 
540
442
  function _wrapText(
541
443
  text,
@@ -557,16 +459,6 @@ function _wrapText(
557
459
  ];
558
460
  }
559
461
 
560
- // 스크롤 없이 전체 줄 반환 (focus pane용)
561
- function wrapTextAll(text, width, rawMode = false) {
562
- const input = sanitizeTextBlock(text, rawMode);
563
- if (!input) return [];
564
- return input
565
- .split("\n")
566
- .flatMap((line) => wrapLine(line, width))
567
- .filter(Boolean);
568
- }
569
-
570
462
  // ── virtual row buffer ────────────────────────────────────────────────────
571
463
  class RowBuffer {
572
464
  constructor() {
@@ -600,22 +492,7 @@ class RowBuffer {
600
492
  }
601
493
  }
602
494
 
603
- // ── 상태 집계 ─────────────────────────────────────────────────────────────
604
- function countStatuses(names, workers) {
605
- let ok = 0,
606
- partial = 0,
607
- failed = 0,
608
- running = 0;
609
- for (const name of names) {
610
- const st = workers.get(name);
611
- const s = runtimeStatus(st);
612
- if (s === "ok" || s === "completed") ok++;
613
- else if (s === "partial") partial++;
614
- else if (s === "failed") failed++;
615
- else if (s === "running" || s === "in_progress") running++;
616
- }
617
- return { ok, partial, failed, running };
618
- }
495
+ // countStatuses tui-core.mjs에서 import
619
496
 
620
497
  // ── Tier1: 상단 고정 1행 ─────────────────────────────────────────────────
621
498
  function phaseColor(phase, time = Date.now()) {
@@ -646,7 +523,7 @@ function buildTier1(
646
523
  width,
647
524
  );
648
525
  const keysHint = color(
649
- "Tab:focus • j/k/↑↓:nav • f:followr:rawl:tabn:recent1-9:jump",
526
+ "Tab:focus • j/k:nav • gg/G:jump/:search • n/N:nextH/L:resizef:followl:tab",
650
527
  MOCHA.subtext,
651
528
  );
652
529
  const hintWidth = wcswidth(stripAnsi(keysHint));
@@ -1000,7 +877,7 @@ function buildSummaryBar(
1000
877
  );
1001
878
  const keysLine = truncate(
1002
879
  color(
1003
- "Tab:focus • j/k/↑↓:nav • f:followr:rawl:tabn:recent1-9:jump",
880
+ "Tab:focus • j/k:nav • gg/G:jump/:search • n/N:nextH/L:resizef:followl:tab",
1004
881
  MOCHA.subtext,
1005
882
  ),
1006
883
  width - 4,
@@ -1067,87 +944,9 @@ function _joinColumns(blocks, gap = GRID_GAP) {
1067
944
 
1068
945
  // ── normalizeWorkerState ──────────────────────────────────────────────────
1069
946
  function normalizeWorkerState(existing, state) {
1070
- const nextHandoff =
1071
- state.handoff === undefined
1072
- ? existing.handoff
1073
- : {
1074
- ...(existing.handoff || {}),
1075
- ...(state.handoff || {}),
1076
- verdict:
1077
- state.handoff?.verdict !== undefined
1078
- ? sanitizeOneLine(state.handoff.verdict)
1079
- : existing.handoff?.verdict,
1080
- files_changed:
1081
- state.handoff?.files_changed !== undefined
1082
- ? sanitizeFiles(state.handoff.files_changed)
1083
- : existing.handoff?.files_changed,
1084
- confidence:
1085
- state.handoff?.confidence !== undefined
1086
- ? sanitizeOneLine(state.handoff.confidence)
1087
- : existing.handoff?.confidence,
1088
- status:
1089
- state.handoff?.status !== undefined
1090
- ? sanitizeOneLine(state.handoff.status)
1091
- : existing.handoff?.status,
1092
- };
1093
-
1094
- return {
1095
- ...existing,
1096
- ...state,
1097
- cli:
1098
- state.cli !== undefined
1099
- ? sanitizeOneLine(state.cli, existing.cli || "codex")
1100
- : existing.cli || "codex",
1101
- role:
1102
- state.role !== undefined ? sanitizeOneLine(state.role) : existing.role,
1103
- status:
1104
- state.status !== undefined
1105
- ? sanitizeOneLine(state.status, existing.status || "pending")
1106
- : existing.status || "pending",
1107
- snapshot:
1108
- state.snapshot !== undefined
1109
- ? sanitizeTextBlock(state.snapshot)
1110
- : existing.snapshot,
1111
- summary:
1112
- state.summary !== undefined
1113
- ? sanitizeTextBlock(state.summary)
1114
- : existing.summary,
1115
- detail:
1116
- state.detail !== undefined
1117
- ? sanitizeTextBlock(state.detail)
1118
- : existing.detail,
1119
- findings:
1120
- state.findings !== undefined
1121
- ? sanitizeFindings(state.findings)
1122
- : existing.findings,
1123
- files_changed:
1124
- state.files_changed !== undefined
1125
- ? sanitizeFiles(state.files_changed)
1126
- : existing.files_changed,
1127
- confidence:
1128
- state.confidence !== undefined
1129
- ? sanitizeOneLine(state.confidence)
1130
- : existing.confidence,
1131
- tokens:
1132
- state.tokens !== undefined
1133
- ? normalizeTokens(state.tokens)
1134
- : existing.tokens,
1135
- progress:
1136
- state.progress !== undefined
1137
- ? clamp(Number(state.progress) || 0, 0, 1)
1138
- : existing.progress,
1139
- handoff: nextHandoff,
1140
- _prevStatus:
1141
- state.status !== undefined &&
1142
- sanitizeOneLine(state.status) !== existing.status
1143
- ? existing.status
1144
- : existing._prevStatus,
1145
- _statusChangedAt:
1146
- state.status !== undefined &&
1147
- sanitizeOneLine(state.status) !== existing.status
1148
- ? Date.now()
1149
- : existing._statusChangedAt || 0,
1150
- };
947
+ return coreNormalizeWorkerState(existing || { cli: "codex", status: "pending" }, state, {
948
+ trackChanges: true,
949
+ });
1151
950
  }
1152
951
 
1153
952
  // ── createLogDashboard ────────────────────────────────────────────────────
@@ -1208,6 +1007,25 @@ export function createLogDashboard(opts = {}) {
1208
1007
  let inputAttached = false;
1209
1008
  let rawModeEnabled = false;
1210
1009
 
1010
+ // UX 위젯 (ISSUE-14)
1011
+ const tokenTracker = createTokenTracker();
1012
+ const searchState = createSearchState();
1013
+ const vimMotion = createVimMotion();
1014
+ const panelResizer = createPanelResizer({
1015
+ initialRatio: focus === "detail" ? 0.2 : 0.3,
1016
+ });
1017
+
1018
+ // Synapse 실시간 관제 (Phase 3)
1019
+ const synapseMetrics = deps.synapseMetrics || createMetricsCollector();
1020
+ const synapseStream = deps.synapseStream || createSynapseEventStream({
1021
+ onEvent(event) {
1022
+ synapseMetrics.ingest(event);
1023
+ },
1024
+ fetchImpl: deps.synapseFetch,
1025
+ });
1026
+ // Synapse 자동 시작 (옵트인: deps.enableSynapse)
1027
+ if (deps.enableSynapse) synapseStream.start();
1028
+
1211
1029
  // virtual row buffer (altScreen 전용)
1212
1030
  const rowBuf = new RowBuffer();
1213
1031
 
@@ -1298,6 +1116,7 @@ export function createLogDashboard(opts = {}) {
1298
1116
  function doClose() {
1299
1117
  if (closed) return;
1300
1118
  if (timer) clearInterval(timer);
1119
+ synapseStream.stop();
1301
1120
  if (inputAttached && typeof input?.off === "function")
1302
1121
  input.off("data", handleInput);
1303
1122
  if (rawModeEnabled && typeof input?.setRawMode === "function")
@@ -1312,6 +1131,14 @@ export function createLogDashboard(opts = {}) {
1312
1131
  const key = String(chunk);
1313
1132
  if (key === "\u0003") { doClose(); return; }
1314
1133
 
1134
+ // 검색 모드 활성 중: 키를 검색 상태에 위임
1135
+ if (searchState.active) {
1136
+ if (searchState.handleKey(key)) {
1137
+ render();
1138
+ return;
1139
+ }
1140
+ }
1141
+
1315
1142
  // Help overlay: 아무 키나 누르면 닫기
1316
1143
  if (helpOverlay) {
1317
1144
  helpOverlay = false;
@@ -1379,20 +1206,33 @@ export function createLogDashboard(opts = {}) {
1379
1206
  }
1380
1207
  }
1381
1208
 
1382
- // g: focus pane 상단 점프
1383
- if (key === "g") {
1384
- followTail = false;
1385
- detailScrollOffset = 0;
1209
+ // vim 모션: gg(첫 워커/상단), G(마지막 워커/하단)
1210
+ const motion = vimMotion.handleKey(key);
1211
+ if (motion === "gg") {
1212
+ if (focus === "detail") {
1213
+ followTail = false;
1214
+ detailScrollOffset = 0;
1215
+ } else {
1216
+ const names = visibleWorkerNames();
1217
+ if (names.length > 0) setSelectedWorker(names[0]);
1218
+ }
1386
1219
  render();
1387
1220
  return;
1388
1221
  }
1389
- // G: focus pane 하단 점프
1390
- if (key === "G") {
1391
- followTail = true;
1392
- detailScrollOffset = 0;
1222
+ if (motion === "G") {
1223
+ if (focus === "detail") {
1224
+ followTail = true;
1225
+ detailScrollOffset = 0;
1226
+ } else {
1227
+ const names = visibleWorkerNames();
1228
+ if (names.length > 0) setSelectedWorker(names[names.length - 1]);
1229
+ }
1393
1230
  render();
1394
1231
  return;
1395
1232
  }
1233
+ // 단일 g는 vimMotion 대기 → return하지 않음 (이전 동작 유지)
1234
+ if (key === "g") return;
1235
+
1396
1236
  // PgUp/PgDn: 페이지 단위 스크롤
1397
1237
  const pageSize = Math.max(1, Math.floor(getViewportRows() / 2));
1398
1238
  if (key === "\x1b[5~") {
@@ -1424,9 +1264,52 @@ export function createLogDashboard(opts = {}) {
1424
1264
  render();
1425
1265
  return;
1426
1266
  }
1427
- // n: 가장 최근 상태 변경 워커로 이동
1267
+ // /: 검색 모드 활성화 (vim 패턴)
1268
+ if (key === "/") {
1269
+ searchState.activate();
1270
+ render();
1271
+ return;
1272
+ }
1273
+ // n: 검색 결과 다음 / 최근 변경 워커
1428
1274
  if (key === "n") {
1429
- selectMostRecentChangedWorker();
1275
+ if (searchState.query) {
1276
+ const names = visibleWorkerNames();
1277
+ const idx = names.indexOf(selectedWorker);
1278
+ const match = searchState.findMatch(names, idx, 1);
1279
+ if (match >= 0) {
1280
+ setSelectedWorker(names[match]);
1281
+ render();
1282
+ } else {
1283
+ showFlash(`검색 결과 없음: ${searchState.query}`);
1284
+ }
1285
+ } else {
1286
+ selectMostRecentChangedWorker();
1287
+ }
1288
+ return;
1289
+ }
1290
+ // N: 검색 결과 이전
1291
+ if (key === "N") {
1292
+ if (searchState.query) {
1293
+ const names = visibleWorkerNames();
1294
+ const idx = names.indexOf(selectedWorker);
1295
+ const match = searchState.findMatch(names, idx, -1);
1296
+ if (match >= 0) {
1297
+ setSelectedWorker(names[match]);
1298
+ render();
1299
+ }
1300
+ }
1301
+ return;
1302
+ }
1303
+ // H: rail 축소 (패널 리사이즈)
1304
+ if (key === "H") {
1305
+ panelResizer.shrinkRail();
1306
+ render();
1307
+ return;
1308
+ }
1309
+ // L: rail 확대 (패널 리사이즈)
1310
+ if (key === "L") {
1311
+ panelResizer.expandRail();
1312
+ render();
1430
1313
  return;
1431
1314
  }
1432
1315
  // h/?: 도움말 오버레이 토글
@@ -1576,6 +1459,16 @@ export function createLogDashboard(opts = {}) {
1576
1459
  truncate(` ${color("▸", MOCHA.green)} ${flashMessage}`, totalCols),
1577
1460
  );
1578
1461
  }
1462
+ // Synapse 실시간 메트릭 (활성 시)
1463
+ if (synapseStream.isRunning) {
1464
+ const metricsSnap = synapseMetrics.snapshot();
1465
+ tier1.push(truncate(renderMetricsTier1(metricsSnap, totalCols), totalCols));
1466
+ }
1467
+ // 검색 프롬프트 (/ 모드)
1468
+ const searchPrompt = searchState.renderPrompt(totalCols);
1469
+ if (searchPrompt) {
1470
+ tier1.push(truncate(searchPrompt, totalCols));
1471
+ }
1579
1472
 
1580
1473
  // 레이아웃 결정
1581
1474
  let effectiveLayout = layoutHint;
@@ -1614,13 +1507,11 @@ export function createLogDashboard(opts = {}) {
1614
1507
  return [...tier1, ...summaryBar, ...focusPane];
1615
1508
  }
1616
1509
 
1617
- // 좌우 분할: Left Rail (30%) | Right Focus (70%)
1618
- // 목업: Tier2 Left Rail + Tier3 Focus 나란히 렌더링
1510
+ // 좌우 분할: Left Rail | Right Focus (H/L로 비율 조정 가능)
1619
1511
  const GAP = 1; // rail과 focus 사이 구분선
1620
- const railRatio = focus === "detail" ? 0.2 : 0.3;
1621
1512
  const railWidth = Math.max(
1622
1513
  MIN_CARD_WIDTH,
1623
- Math.floor(totalCols * railRatio),
1514
+ Math.floor(totalCols * panelResizer.ratio),
1624
1515
  );
1625
1516
  const focusWidth = totalCols - railWidth - GAP;
1626
1517
  const bodyHeight = Math.max(6, totalRows - tier1.length - 1); // -1 for status bar
@@ -1777,6 +1668,8 @@ export function createLogDashboard(opts = {}) {
1777
1668
  ? existing._logSec
1778
1669
  : (explicitElapsed ?? nowElapsedSec());
1779
1670
  workers.set(paneName, merged);
1671
+ // 토큰 히스토리 추적 (스파크라인용)
1672
+ if (merged.tokens !== undefined) tokenTracker.record(paneName, merged.tokens);
1780
1673
  ensureSelectedWorker(visibleWorkerNames());
1781
1674
  // follow-tail: 새 데이터 → 자동 scroll 재계산
1782
1675
  if (followTail) detailScrollOffset = 0;
@@ -1855,6 +1748,17 @@ export function createLogDashboard(opts = {}) {
1855
1748
  return attachToSession(w);
1856
1749
  },
1857
1750
 
1751
+ // Synapse 관제 API
1752
+ startSynapse() {
1753
+ synapseStream.start();
1754
+ },
1755
+ stopSynapse() {
1756
+ synapseStream.stop();
1757
+ },
1758
+ getSynapseMetrics() {
1759
+ return synapseMetrics.snapshot();
1760
+ },
1761
+
1858
1762
  close() {
1859
1763
  doClose();
1860
1764
  },
@@ -11,7 +11,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
11
11
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
12
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
13
  import * as z from "zod";
14
-
14
+ import { resolveBashExecutable } from "../lib/bash-path.mjs";
15
15
  import { CodexMcpWorker } from "./codex-mcp.mjs";
16
16
  import { GeminiWorker } from "./gemini-worker.mjs";
17
17
 
@@ -440,7 +440,7 @@ export class DelegatorMcpWorker {
440
440
  options.bashCommand ||
441
441
  this.env.TFX_DELEGATOR_BASH_COMMAND ||
442
442
  this.env.BASH_BIN ||
443
- "bash";
443
+ resolveBashExecutable();
444
444
 
445
445
  this.codexWorker = new CodexMcpWorker({
446
446
  command:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "10.9.19",
3
+ "version": "10.9.21",
4
4
  "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,75 +13,26 @@
13
13
  "tfx-doctor-tui": "bin/tfx-doctor-tui.mjs",
14
14
  "tfx-setup-tui": "bin/tfx-setup-tui.mjs"
15
15
  },
16
+ "engines": {
17
+ "node": ">=18.0.0"
18
+ },
19
+ "dependencies": {
20
+ "@triflux/core": "10.0.1",
21
+ "@triflux/remote": "^10.0.0-alpha.1"
22
+ },
16
23
  "files": [
17
24
  "bin",
18
- "tui",
19
- "hub",
20
- "config",
21
25
  "skills",
22
- "!skills/tfx-workspace",
23
- "!**/failure-reports",
24
- "scripts",
25
26
  "hooks",
26
27
  "hud",
28
+ "scripts",
29
+ "hub",
27
30
  "mesh",
28
- ".claude-plugin",
31
+ "references",
32
+ "CLAUDE.md",
29
33
  "README.md",
30
- "README.ko.md",
31
34
  "LICENSE"
32
35
  ],
33
- "workspaces": [
34
- "packages/core",
35
- "packages/remote",
36
- "packages/triflux"
37
- ],
38
- "scripts": {
39
- "pack": "node scripts/pack.mjs all",
40
- "pack:core": "node scripts/pack.mjs core",
41
- "pack:remote": "node scripts/pack.mjs remote",
42
- "setup": "node scripts/setup.mjs",
43
- "preinstall": "node scripts/preinstall.mjs",
44
- "postinstall": "node scripts/setup.mjs",
45
- "lint": "biome check bin config hooks hub hud mesh scripts tests .claude-plugin .github package.json package-lock.json biome.json",
46
- "lint:fix": "biome check --write bin config hooks hub hud mesh scripts tests .claude-plugin .github package.json package-lock.json biome.json",
47
- "health": "npm test && npm run lint",
48
- "test": "node scripts/test-lock.mjs --test --test-force-exit --test-concurrency=8 \"tests/**/*.test.mjs\" \"scripts/__tests__/**/*.test.mjs\"",
49
- "test:unit": "node scripts/test-lock.mjs --test --test-force-exit --test-concurrency=8 tests/unit/**/*.test.mjs",
50
- "test:integration": "node scripts/test-lock.mjs --test --test-force-exit --test-concurrency=8 tests/integration/**/*.test.mjs",
51
- "test:route-smoke": "node scripts/test-lock.mjs --test scripts/test-tfx-route-no-claude-native.mjs",
52
- "test:contract": "node scripts/test-lock.mjs --test --test-force-exit --test-concurrency=8 tests/contract/**/*.test.mjs",
53
- "test:coverage": "node --experimental-test-coverage --test-coverage-lines=60 --test-coverage-functions=60 --test --test-force-exit --test-concurrency=8 \"tests/**/*.test.mjs\"",
54
- "gen:skill-docs": "node scripts/gen-skill-docs.mjs",
55
- "gen:skill-manifest": "node scripts/gen-skill-manifest.mjs",
56
- "release:check-sync": "node scripts/release/check-sync.mjs",
57
- "release:check-sync:fix": "node scripts/release/check-sync.mjs --fix",
58
- "release:bump": "node scripts/release/bump-version.mjs",
59
- "release:prepare": "node scripts/release/prepare.mjs",
60
- "release:publish": "node scripts/release/publish.mjs",
61
- "release:verify": "node scripts/release/verify.mjs"
62
- },
63
- "engines": {
64
- "node": ">=18.0.0"
65
- },
66
- "repository": {
67
- "type": "git",
68
- "url": "git+https://github.com/tellang/triflux.git"
69
- },
70
- "homepage": "https://github.com/tellang/triflux#readme",
71
- "author": "tellang",
72
- "license": "MIT",
73
- "dependencies": {
74
- "@modelcontextprotocol/sdk": "^1.27.1",
75
- "better-sqlite3": "^12.6.2",
76
- "pino": "^10.3.1",
77
- "pino-pretty": "^13.1.3",
78
- "systray2": "^2.1.4",
79
- "zod": "^4.0.0"
80
- },
81
- "devDependencies": {
82
- "@biomejs/biome": "^2.0.0",
83
- "knip": "^6.3.0"
84
- },
85
36
  "keywords": [
86
37
  "claude-code",
87
38
  "plugin",
@@ -92,5 +43,13 @@
92
43
  "multi-model",
93
44
  "triflux",
94
45
  "tfx"
95
- ]
46
+ ],
47
+ "author": "tellang",
48
+ "license": "MIT",
49
+ "homepage": "https://github.com/tellang/triflux#readme",
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "git+https://github.com/tellang/triflux.git",
53
+ "directory": "packages/triflux"
54
+ }
96
55
  }