@triflux/remote 10.0.1 → 10.1.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.
@@ -42,7 +42,7 @@ body{background:#0d1117;color:#c9d1d9;font-family:system-ui,-apple-system,sans-s
42
42
  .token-item .calls{font-size:.78rem;color:#8b949e;margin-top:2px}
43
43
 
44
44
  .chart-wrap{position:relative;width:100%;height:160px}
45
- .chart-wrap canvas{width:100%!important;height:100%!important}
45
+ .chart-wrap canvas{width:100%;height:100%}
46
46
  .chart-legend{display:flex;gap:14px;margin-top:8px;font-size:.78rem;color:#8b949e}
47
47
  .legend-dot{width:8px;height:8px;border-radius:50%;display:inline-block;margin-right:4px;vertical-align:middle}
48
48
 
@@ -152,7 +152,7 @@ body{background:#0d1117;color:#c9d1d9;font-family:system-ui,-apple-system,sans-s
152
152
  function remain(rt) {
153
153
  if (!rt) return '';
154
154
  var ts = typeof rt === 'number' && rt < 1e10 ? rt * 1000 : new Date(rt).getTime();
155
- if (!isFinite(ts)) return '';
155
+ if (!Number.isFinite(ts)) return '';
156
156
  var ms = ts - Date.now();
157
157
  if (ms <= 0) return '만료됨';
158
158
  var h = Math.floor(ms / 3600000);
@@ -199,7 +199,7 @@ body{background:#0d1117;color:#c9d1d9;font-family:system-ui,-apple-system,sans-s
199
199
  var s = '';
200
200
 
201
201
  // Claude
202
- var cl = d.claude && d.claude.data;
202
+ var cl = d.claude?.data;
203
203
  if (cl) {
204
204
  s += quotaBlock('Claude (c)', '#d19a66', [
205
205
  ['5시간', cl.fiveHourPercent || 0],
@@ -208,16 +208,16 @@ body{background:#0d1117;color:#c9d1d9;font-family:system-ui,-apple-system,sans-s
208
208
  }
209
209
 
210
210
  // Codex
211
- var cx2 = d.codex && d.codex.buckets && d.codex.buckets.codex;
211
+ var cx2 = d.codex?.buckets?.codex;
212
212
  if (cx2) {
213
213
  s += quotaBlock('Codex (x)', '#ffffff', [
214
214
  ['Primary', cx2.primary ? cx2.primary.used_percent || 0 : 0],
215
215
  ['Secondary', cx2.secondary ? cx2.secondary.used_percent || 0 : 0]
216
- ], cx2.primary && cx2.primary.resets_at);
216
+ ], cx2.primary?.resets_at);
217
217
  }
218
218
 
219
219
  // Gemini
220
- var gm = d.gemini && d.gemini.buckets;
220
+ var gm = d.gemini?.buckets;
221
221
  if (gm && gm.length > 0) {
222
222
  var frac = gm[0].remainingFraction;
223
223
  s += quotaBlock('Gemini (g)', '#61afef', [
@@ -330,7 +330,7 @@ body{background:#0d1117;color:#c9d1d9;font-family:system-ui,-apple-system,sans-s
330
330
  gauge(data.aimd ? data.aimd.batchSize : 0);
331
331
  renderQuota(data);
332
332
 
333
- cachedEvents = (data.aimd && data.aimd.events) || [];
333
+ cachedEvents = (data.aimd?.events) || [];
334
334
  drawEvents(cachedEvents);
335
335
 
336
336
  renderTokens(data.accumulator);
package/hub/server.mjs CHANGED
@@ -418,6 +418,29 @@ export async function startHub({
418
418
  const adaptiveEngine = createAdaptiveEngine({ repoRoot: PROJECT_ROOT, fingerprintService });
419
419
  adaptiveEngine.startSession();
420
420
 
421
+ // safety-guard → reflexion 패널티 승격 + 적응형 규칙 유지보수
422
+ const projectSlug = PROJECT_ROOT.split(/[\\/]/).pop();
423
+ try {
424
+ const { promotePenalties } = await import("./promote-penalties.mjs");
425
+ const result = promotePenalties(store, { projectSlug });
426
+ if (result.promoted > 0) {
427
+ console.log(`[reflexion] ${result.promoted} penalties promoted to adaptive rules`);
428
+ }
429
+ } catch { /* promote-penalties 실패는 Hub 시작을 막지 않음 */ }
430
+
431
+ // stale adaptive_rules 정리 (30일 초과 + confidence 0.2 미만)
432
+ try {
433
+ const pruned = store.pruneStaleRules();
434
+ if (pruned > 0) console.log(`[reflexion] ${pruned} stale adaptive rules pruned`);
435
+ } catch { /* prune 실패 무시 */ }
436
+
437
+ // adaptive rule confidence decay (7일 이상 미관측 규칙 -0.1 감소)
438
+ try {
439
+ const { decayRules } = await import("./reflexion.mjs");
440
+ const decay = decayRules(store, adaptiveEngine.sessionCount?.() || 1, projectSlug);
441
+ if (decay.deleted.length > 0) console.log(`[reflexion] ${decay.deleted.length} low-confidence rules removed`);
442
+ } catch { /* decay 실패 무시 */ }
443
+
421
444
  // Delegator MCP resident service 초기화
422
445
  const delegatorWorker = createDelegatorWorker({ cwd: PROJECT_ROOT });
423
446
  try {
@@ -15,23 +15,35 @@ export { createMemoryStore };
15
15
  function ensureAdaptiveRulesSchema(db) {
16
16
  const schemaKey = 'adaptive_rules_schema_version';
17
17
  const currentVersion = db.prepare('SELECT value FROM _meta WHERE key = ?').pluck().get(schemaKey);
18
- if (currentVersion === '1') return;
19
- db.exec(`
20
- CREATE TABLE IF NOT EXISTS adaptive_rules (
21
- project_slug TEXT NOT NULL,
22
- pattern TEXT NOT NULL,
23
- confidence REAL NOT NULL DEFAULT 0.5,
24
- hit_count INTEGER NOT NULL DEFAULT 1,
25
- last_seen_ms INTEGER NOT NULL,
26
- created_ms INTEGER NOT NULL,
27
- PRIMARY KEY (project_slug, pattern)
28
- );
29
- CREATE INDEX IF NOT EXISTS idx_adaptive_rules_last_seen
30
- ON adaptive_rules(project_slug, last_seen_ms DESC);
31
- CREATE INDEX IF NOT EXISTS idx_adaptive_rules_confidence
32
- ON adaptive_rules(project_slug, confidence DESC);
33
- `);
34
- db.prepare('INSERT OR REPLACE INTO _meta (key, value) VALUES (?, ?)').run(schemaKey, '1');
18
+ if (currentVersion === '2') return;
19
+ if (!currentVersion) {
20
+ // 신규 생성
21
+ db.exec(`
22
+ CREATE TABLE IF NOT EXISTS adaptive_rules (
23
+ project_slug TEXT NOT NULL,
24
+ pattern TEXT NOT NULL,
25
+ confidence REAL NOT NULL DEFAULT 0.5,
26
+ hit_count INTEGER NOT NULL DEFAULT 1,
27
+ last_seen_ms INTEGER NOT NULL,
28
+ created_ms INTEGER NOT NULL,
29
+ error_message TEXT,
30
+ solution TEXT,
31
+ context TEXT,
32
+ PRIMARY KEY (project_slug, pattern)
33
+ );
34
+ CREATE INDEX IF NOT EXISTS idx_adaptive_rules_last_seen
35
+ ON adaptive_rules(project_slug, last_seen_ms DESC);
36
+ CREATE INDEX IF NOT EXISTS idx_adaptive_rules_confidence
37
+ ON adaptive_rules(project_slug, confidence DESC);
38
+ `);
39
+ } else {
40
+ // v1 → v2 마이그레이션: 컬럼 추가
41
+ const cols = db.pragma('table_info(adaptive_rules)').map(c => c.name);
42
+ if (!cols.includes('error_message')) db.exec('ALTER TABLE adaptive_rules ADD COLUMN error_message TEXT');
43
+ if (!cols.includes('solution')) db.exec('ALTER TABLE adaptive_rules ADD COLUMN solution TEXT');
44
+ if (!cols.includes('context')) db.exec('ALTER TABLE adaptive_rules ADD COLUMN context TEXT');
45
+ }
46
+ db.prepare('INSERT OR REPLACE INTO _meta (key, value) VALUES (?, ?)').run(schemaKey, '2');
35
47
  }
36
48
 
37
49
  function attachAdaptiveRuleStore(store) {
@@ -40,15 +52,20 @@ function attachAdaptiveRuleStore(store) {
40
52
  const statements = {
41
53
  upsertRule: store.db.prepare(`
42
54
  INSERT INTO adaptive_rules (
43
- project_slug, pattern, confidence, hit_count, last_seen_ms, created_ms
55
+ project_slug, pattern, confidence, hit_count, last_seen_ms, created_ms,
56
+ error_message, solution, context
44
57
  ) VALUES (
45
- @project_slug, @pattern, @confidence, @hit_count, @last_seen_ms, @created_ms
58
+ @project_slug, @pattern, @confidence, @hit_count, @last_seen_ms, @created_ms,
59
+ @error_message, @solution, @context
46
60
  )
47
61
  ON CONFLICT(project_slug, pattern) DO UPDATE SET
48
62
  confidence = excluded.confidence,
49
63
  hit_count = excluded.hit_count,
50
64
  last_seen_ms = excluded.last_seen_ms,
51
- created_ms = MIN(adaptive_rules.created_ms, excluded.created_ms)`),
65
+ created_ms = MIN(adaptive_rules.created_ms, excluded.created_ms),
66
+ error_message = COALESCE(excluded.error_message, adaptive_rules.error_message),
67
+ solution = COALESCE(excluded.solution, adaptive_rules.solution),
68
+ context = COALESCE(excluded.context, adaptive_rules.context)`),
52
69
  getRule: store.db.prepare('SELECT * FROM adaptive_rules WHERE project_slug = ? AND pattern = ?'),
53
70
  updateRule: store.db.prepare(`
54
71
  UPDATE adaptive_rules SET
@@ -93,6 +110,20 @@ function attachAdaptiveRuleStore(store) {
93
110
  return statements.pruneRules.run(cutoff, minConfidence).changes;
94
111
  };
95
112
 
113
+ // listAdaptiveRules: decayRules/getActiveAdaptiveRules에서 사용
114
+ const listStmt = store.db.prepare('SELECT * FROM adaptive_rules WHERE project_slug = ? ORDER BY confidence DESC');
115
+ const listAllStmt = store.db.prepare('SELECT * FROM adaptive_rules ORDER BY confidence DESC');
116
+ const deleteRuleStmt = store.db.prepare('DELETE FROM adaptive_rules WHERE project_slug = ? AND pattern = ?');
117
+
118
+ store.listAdaptiveRules = function listAdaptiveRules(projectSlug) {
119
+ if (projectSlug) return listStmt.all(projectSlug).map(clone);
120
+ return listAllStmt.all().map(clone);
121
+ };
122
+
123
+ store.deleteAdaptiveRule = function deleteAdaptiveRule(projectSlug, pattern) {
124
+ return deleteRuleStmt.run(projectSlug, pattern).changes > 0;
125
+ };
126
+
96
127
  return store;
97
128
  }
98
129
 
package/hub/store.mjs CHANGED
@@ -551,7 +551,7 @@ export function createStore(dbPath, options = {}) {
551
551
  ttl_ms: clampDuration(patch.ttl_ms ?? current.ttl_ms, current.ttl_ms || nextTimeout),
552
552
  timeout_ms: nextTimeout,
553
553
  deadline_ms: (() => {
554
- if (Object.prototype.hasOwnProperty.call(patch, 'deadline_ms')) {
554
+ if (Object.hasOwn(patch, 'deadline_ms')) {
555
555
  return patch.deadline_ms == null ? null : Math.trunc(Number(patch.deadline_ms));
556
556
  }
557
557
  if (isTerminal) return null;
@@ -560,23 +560,23 @@ export function createStore(dbPath, options = {}) {
560
560
  })(),
561
561
  trace_id: patch.trace_id ?? current.trace_id,
562
562
  correlation_id: patch.correlation_id ?? current.correlation_id,
563
- last_message_id: Object.prototype.hasOwnProperty.call(patch, 'last_message_id')
563
+ last_message_id: Object.hasOwn(patch, 'last_message_id')
564
564
  ? patch.last_message_id
565
565
  : current.last_message_id,
566
- result_json: Object.prototype.hasOwnProperty.call(patch, 'result')
566
+ result_json: Object.hasOwn(patch, 'result')
567
567
  ? (patch.result == null ? null : JSON.stringify(patch.result))
568
568
  : (current.result == null ? null : JSON.stringify(current.result)),
569
- error_json: Object.prototype.hasOwnProperty.call(patch, 'error')
569
+ error_json: Object.hasOwn(patch, 'error')
570
570
  ? (patch.error == null ? null : JSON.stringify(patch.error))
571
571
  : (current.error == null ? null : JSON.stringify(current.error)),
572
572
  updated_at_ms: now,
573
- started_at_ms: Object.prototype.hasOwnProperty.call(patch, 'started_at_ms')
573
+ started_at_ms: Object.hasOwn(patch, 'started_at_ms')
574
574
  ? patch.started_at_ms
575
575
  : (nextStatus === 'running' ? (current.started_at_ms || now) : current.started_at_ms),
576
- completed_at_ms: Object.prototype.hasOwnProperty.call(patch, 'completed_at_ms')
576
+ completed_at_ms: Object.hasOwn(patch, 'completed_at_ms')
577
577
  ? patch.completed_at_ms
578
578
  : (isTerminal ? (current.completed_at_ms || now) : current.completed_at_ms),
579
- last_retry_at_ms: Object.prototype.hasOwnProperty.call(patch, 'last_retry_at_ms')
579
+ last_retry_at_ms: Object.hasOwn(patch, 'last_retry_at_ms')
580
580
  ? patch.last_retry_at_ms
581
581
  : current.last_retry_at_ms,
582
582
  };
@@ -661,7 +661,7 @@ export function createStore(dbPath, options = {}) {
661
661
  started_at_ms: null,
662
662
  last_retry_at_ms: Date.now(),
663
663
  result: patch.result ?? null,
664
- error: Object.prototype.hasOwnProperty.call(patch, 'error') ? patch.error : current.error,
664
+ error: Object.hasOwn(patch, 'error') ? patch.error : current.error,
665
665
  last_message_id: null,
666
666
  });
667
667
  },
@@ -1,6 +1,6 @@
1
- import { BOLD, DIM, GREEN, RESET, AMBER } from "../../../shared.mjs";
1
+ import { DIM, GREEN, RESET, AMBER } from "../../../shared.mjs";
2
2
  import { runHeadlessInteractive, resolveCliType } from "../../../headless.mjs";
3
- import { ok, warn } from "../../render.mjs";
3
+ import { ok, } from "../../render.mjs";
4
4
  import { buildTasks } from "../../services/task-model.mjs";
5
5
  import { clearTeamState } from "../../services/state-store.mjs";
6
6
 
@@ -10,7 +10,7 @@ export async function startHeadlessTeam({ sessionId, task, lead, agents, subtask
10
10
  ? assigns.map((a, i) => ({ cli: resolveCliType(a.cli), prompt: a.prompt, role: a.role || `worker-${i + 1}`, mcp: mcpProfile, model }))
11
11
  : subtasks.map((subtask, i) => ({ cli: resolveCliType(agents[i] || agents[0]), prompt: subtask, role: `worker-${i + 1}`, mcp: mcpProfile, model }));
12
12
 
13
- const startedAt = Date.now();
13
+ const _startedAt = Date.now();
14
14
  ok(`headless ${assignments.length}워커 시작`);
15
15
 
16
16
  const handle = await runHeadlessInteractive(sessionId, assignments, {
@@ -46,6 +46,8 @@ export async function startHeadlessTeam({ sessionId, task, lead, agents, subtask
46
46
  const failed = results.filter((r) => !r.matched || r.exitCode !== 0);
47
47
 
48
48
  ok(`헤드리스 완료: ${succeeded.length}성공 / ${failed.length}실패 / ${results.length}전체`);
49
+ // 머신 파싱용 완료 마커 — background task에서 결과 회수 시점 판단에 사용
50
+ console.log(`=== HEADLESS_COMPLETE succeeded=${succeeded.length} failed=${failed.length} total=${results.length} ===`);
49
51
 
50
52
  if (failed.length > 0) {
51
53
  for (const r of failed) console.log(` ${AMBER}✗${RESET} ${r.paneName} (${r.cli}) exit=${r.exitCode}`);
@@ -154,7 +154,7 @@ export async function ensureHubAlive(maxRetries = 3) {
154
154
  }
155
155
  // 다음 재시도 전 대기: 1초, 2초, 4초 (마지막 시도 후에는 대기 없음)
156
156
  if (i < maxRetries - 1) {
157
- const backoffMs = Math.pow(2, i) * 1000; // i=0: 1초, i=1: 2초, i=2: 4초
157
+ const backoffMs = 2 ** i * 1000; // i=0: 1초, i=1: 2초, i=2: 4초
158
158
  await new Promise((resolve) => setTimeout(resolve, backoffMs));
159
159
  }
160
160
  }
@@ -14,7 +14,7 @@ import { homedir } from 'node:os';
14
14
  import { mkdirSync, createWriteStream, readFileSync, copyFileSync } from 'node:fs';
15
15
  import { EventEmitter } from 'node:events';
16
16
 
17
- import { killProcess, IS_WINDOWS } from '@triflux/core/hub/platform.mjs';
17
+ import { killProcess, } from '@triflux/core/hub/platform.mjs';
18
18
  import { createEventLog } from './event-log.mjs';
19
19
  import { createHealthProbe } from './health-probe.mjs';
20
20
  import { createRemoteProbe } from './remote-probe.mjs';
@@ -15,7 +15,7 @@ function sanitizeWindowTitle(value, fallback = "triflux") {
15
15
  }
16
16
 
17
17
  function sanitizeSessionName(value) {
18
- return String(value || "").replace(/[^a-zA-Z0-9_\-]/g, "") || "tfx-session";
18
+ return String(value || "").replace(/[^a-zA-Z0-9_-]/g, "") || "tfx-session";
19
19
  }
20
20
 
21
21
  function sanitizeWorkingDirectory(value) {
@@ -33,8 +33,8 @@ const CLI_BRAND = {
33
33
  gemini: { emoji: "\u{1F535}", label: "Gemini", ansi: "\x1b[38;5;39m" }, // 🔵 geminiBlue
34
34
  claude: { emoji: "\u{1F7E0}", label: "Claude", ansi: "\x1b[38;2;232;112;64m" }, // 🟠 claudeOrange
35
35
  };
36
- const ANSI_RESET = "\x1b[0m";
37
- const ANSI_DIM = "\x1b[2m";
36
+ const _ANSI_RESET = "\x1b[0m";
37
+ const _ANSI_DIM = "\x1b[2m";
38
38
 
39
39
  /** 에이전트 역할명 → CLI 타입 매핑 (단일 소스: agent-map.json) */
40
40
  const _require = createRequire(import.meta.url);
@@ -478,7 +478,7 @@ async function awaitAll(sessionName, dispatches, timeoutSec, safeProgress, progr
478
478
  }
479
479
 
480
480
  let completion;
481
- if (stallOpts && stallOpts.enabled) {
481
+ if (stallOpts?.enabled) {
482
482
  // 하이브리드 stall detection 모드
483
483
  try {
484
484
  const stallPollCb = safeProgress && progressIntervalSec > 0
@@ -809,7 +809,7 @@ function atomicWriteSync(filePath, data) {
809
809
  }
810
810
 
811
811
  function sanitizeSessionName(value) {
812
- return String(value || "").replace(/[^a-zA-Z0-9_\-]/g, "") || "tfx-session";
812
+ return String(value || "").replace(/[^a-zA-Z0-9_-]/g, "") || "tfx-session";
813
813
  }
814
814
 
815
815
  function sanitizeWindowTitle(value, fallback = "triflux") {
@@ -50,7 +50,7 @@ export function decomposeTask(taskDescription, agentCount) {
50
50
  * @returns {string}
51
51
  */
52
52
  export function buildLeadPrompt(taskDescription, config) {
53
- const { agentId, hubUrl, teammateMode = "tmux", workers = [] } = config;
53
+ const { agentId, teammateMode = "tmux", workers = [] } = config;
54
54
 
55
55
  const roster = workers
56
56
  .map((w, i) => `${i + 1}. ${w.agentId} (${w.cli}) — ${w.subtask}`)
@@ -92,7 +92,7 @@ ${roster}
92
92
  export function buildPrompt(subtask, config) {
93
93
  const { cli, agentId, hubUrl } = config;
94
94
 
95
- const hubBase = hubUrl.replace("/mcp", "");
95
+ const _hubBase = hubUrl.replace("/mcp", "");
96
96
 
97
97
  const bridgePath = "node hub/bridge.mjs";
98
98
 
@@ -196,7 +196,12 @@ function ensureCaptureHelper() {
196
196
  "}",
197
197
  "",
198
198
  "# Force UTF-8 encoding — CP949 등 non-UTF-8 codepage에서 Gemini stdout 캡처 실패 방지",
199
+ "# InputEncoding: pipe-pane에서 읽어들이는 바이트 해석",
200
+ "# OutputEncoding: 네이티브 명령(gemini/codex CLI)의 stdout 디코딩",
201
+ "# $OutputEncoding: PowerShell 파이프라인 기본 인코딩 (BOM 없는 UTF-8)",
199
202
  "[Console]::InputEncoding = [System.Text.Encoding]::UTF8",
203
+ "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8",
204
+ "$OutputEncoding = [System.Text.UTF8Encoding]::new($false)",
200
205
  "",
201
206
  "$reader = [Console]::In",
202
207
  "while (($line = $reader.ReadLine()) -ne $null) {",
@@ -514,6 +519,23 @@ export function createPsmuxSession(sessionName, opts = {}) {
514
519
  psmuxExec(["select-pane", "-t", pane, "-T", toPaneTitle(index)]);
515
520
  });
516
521
 
522
+ // CP949 등 non-UTF-8 codepage 환경에서 CLI stdout 깨짐 방지:
523
+ // pane 생성 직후 UTF-8 인코딩을 강제 설정한다.
524
+ // chcp 65001: 프로세스 코드 페이지를 UTF-8로 전환
525
+ // OutputEncoding: 네이티브 명령(gemini/codex) stdout 디코딩
526
+ // $OutputEncoding: PowerShell 파이프라인 기본 인코딩
527
+ if (IS_WINDOWS) {
528
+ const encodingInit = [
529
+ "chcp 65001 > $null",
530
+ "[Console]::InputEncoding = [System.Text.Encoding]::UTF8",
531
+ "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8",
532
+ "$OutputEncoding = [System.Text.UTF8Encoding]::new($false)",
533
+ ].join("; ");
534
+ panes.forEach((paneId) => {
535
+ sendLiteralToPane(paneId, encodingInit, true);
536
+ });
537
+ }
538
+
517
539
  return { sessionName, panes };
518
540
  }
519
541
 
@@ -857,15 +879,55 @@ export function startCapture(sessionName, paneNameOrTarget) {
857
879
  * @param {string} cmd
858
880
  * @returns {string}
859
881
  */
882
+ // Windows: PowerShell의 bash가 WSL을 가리킬 수 있음. Git Bash를 명시적으로 사용.
883
+ // WSL bash에서는 /c/Users/... 경로가 유효하지 않아 exit 127 발생.
884
+ // psmux pane 용 (PowerShell → bash 실행): & "path" 형식
885
+ // Node.js execSync 용 (직접 실행): "path" 형식
886
+ const _gitBashPath = (() => {
887
+ if (!IS_WINDOWS) return null;
888
+ const candidates = [
889
+ "C:\\Program Files\\Git\\usr\\bin\\bash.exe",
890
+ "C:\\Program Files (x86)\\Git\\usr\\bin\\bash.exe",
891
+ ];
892
+ for (const p of candidates) {
893
+ if (existsSync(p)) return p;
894
+ }
895
+ return null;
896
+ })();
897
+ // PowerShell call operator(&)가 필요: & "C:\...\bash.exe" -c '...'
898
+ const GIT_BASH_BIN_PS = _gitBashPath ? `& "${_gitBashPath}"` : "bash";
899
+ // Node.js execSync에서 직접 실행할 때는 따옴표만
900
+ const GIT_BASH_BIN_EXEC = _gitBashPath ? `"${_gitBashPath}"` : "bash";
901
+
902
+ // CLI 바이너리 절대 경로 캐시 (세션 중 1회만 resolve)
903
+ const _cliPathCache = new Map();
904
+ function resolveCliAbsPath(name) {
905
+ if (_cliPathCache.has(name)) return _cliPathCache.get(name);
906
+ try {
907
+ const resolved = childProcess
908
+ .execSync(`${GIT_BASH_BIN_EXEC} -c "which ${name}"`, { encoding: "utf8", timeout: 3000 })
909
+ .trim();
910
+ if (resolved) _cliPathCache.set(name, resolved);
911
+ return resolved || name;
912
+ } catch {
913
+ return name;
914
+ }
915
+ }
916
+
860
917
  function wrapCliForBash(cmd) {
861
918
  const trimmed = cmd.trimStart();
862
919
  // PowerShell 구문(Clear-Host, Get-Content 등) 또는 completion token이 포함되면 PowerShell 직통
863
920
  if (/Clear-Host|Get-Content|__TRIFLUX_DONE__/i.test(trimmed)) return cmd;
864
- const isCli = /\b(codex|gemini)\b/u.test(trimmed);
865
- if (!isCli) return cmd;
921
+ const cliMatch = trimmed.match(/^(codex|gemini)\b/u);
922
+ if (!cliMatch) return cmd;
923
+ // Node.js 측에서 절대 경로 resolve → psmux pane의 bash PATH 불일치 문제 해결 (exit 127)
924
+ const absPath = resolveCliAbsPath(cliMatch[1]);
925
+ const resolved = absPath !== cliMatch[1]
926
+ ? trimmed.replace(new RegExp(`^${cliMatch[1]}\\b`), absPath)
927
+ : trimmed;
866
928
  // 단일 따옴표 이스케이프: ' → '\''
867
- const escaped = trimmed.replace(/'/g, "'\\''");
868
- return `bash -c '${escaped}'`;
929
+ const escaped = resolved.replace(/'/g, "'\\''");
930
+ return `${GIT_BASH_BIN_PS} -c '${escaped}'`;
869
931
  }
870
932
 
871
933
  export function dispatchCommand(sessionName, paneNameOrTarget, commandText) {
@@ -1230,7 +1292,7 @@ export function captureWorkerOutput(sessionName, workerName, lines = 50) {
1230
1292
 
1231
1293
  // ─── CLI 진입점 ───
1232
1294
 
1233
- if (process.argv[1] && process.argv[1].endsWith("psmux.mjs")) {
1295
+ if (process.argv[1]?.endsWith("psmux.mjs")) {
1234
1296
  (async () => {
1235
1297
  const [, , cmd, ...args] = process.argv;
1236
1298
 
@@ -14,7 +14,7 @@ import { detectInputWait, PROBE_DEFAULTS } from './health-probe.mjs';
14
14
  const COMPLETION_TOKEN_RE = /__TRIFLUX_DONE__/;
15
15
 
16
16
  /** 프롬프트 idle 패턴 (Claude Code 프롬프트 복귀) */
17
- const PROMPT_IDLE_RE = /(\u276f|\u2795|>\s*$)/;
17
+ const _PROMPT_IDLE_RE = /(\u276f|\u2795|>\s*$)/;
18
18
 
19
19
  /**
20
20
  * SSH 경유로 원격 psmux capture-pane 실행.
@@ -40,7 +40,7 @@ const PROMPT_IDLE_PATTERNS = Object.freeze([
40
40
  ]);
41
41
 
42
42
  /** @deprecated shellQuoteForHost(value, os) 사용 권장 — OS-aware 쿼팅 */
43
- function shellQuote(value, os) {
43
+ function _shellQuote(value, os) {
44
44
  if (os) return shellQuoteForHost(value, os);
45
45
  return `'${String(value).replace(/'/g, "'\\''")}'`;
46
46
  }
@@ -481,7 +481,7 @@ export function configureTeammateKeybindings(sessionName, opts = {}) {
481
481
  if (taskListCommand) {
482
482
  const escaped = taskListCommand.replace(/'/g, "'\\''");
483
483
  try {
484
- tmux(`bind-key -T root -n C-t if-shell -F '${cond}' \"display-popup -E '${escaped}'\" \"send-keys C-t\"`);
484
+ tmux(`bind-key -T root -n C-t if-shell -F '${cond}' "display-popup -E '${escaped}'" "send-keys C-t"`);
485
485
  } catch {
486
486
  tmux(`bind-key -T root -n C-t if-shell -F '${cond}' 'display-message "tfx multi tasks 명령으로 태스크 확인"' 'send-keys C-t'`);
487
487
  }
@@ -13,7 +13,6 @@
13
13
  import { EventEmitter } from 'node:events';
14
14
  import { join } from 'node:path';
15
15
  import { mkdirSync, readFileSync, existsSync } from 'node:fs';
16
- import { execSync } from 'node:child_process';
17
16
 
18
17
  import { createConductor, STATES } from './conductor.mjs';
19
18
  import { createSwarmLocks } from './swarm-locks.mjs';
@@ -68,9 +67,9 @@ export function createSwarmHypervisor(opts) {
68
67
  logsDir,
69
68
  maxRestarts = 2,
70
69
  graceMs = 10_000,
71
- integrationTimeoutMs = 60_000,
70
+ _integrationTimeoutMs = 60_000,
72
71
  probeOpts = {},
73
- deps = {},
72
+ _deps = {},
74
73
  } = opts;
75
74
 
76
75
  if (!workdir) throw new Error('workdir is required');
@@ -415,7 +414,7 @@ export function createSwarmHypervisor(opts) {
415
414
  */
416
415
  function detectChangedFiles(shardName, worker) {
417
416
  // Best-effort: parse output log for file paths
418
- const outPath = join(logsDir, shardName);
417
+ const _outPath = join(logsDir, shardName);
419
418
  try {
420
419
  const snap = worker.conductor.getSnapshot();
421
420
  for (const session of snap) {
@@ -57,7 +57,7 @@ function resolveSshUser(hostsData, host) {
57
57
  * @returns {string|null}
58
58
  */
59
59
  function resolveHostFromSessionName(sessionName) {
60
- if (!sessionName || !sessionName.startsWith(SESSION_PREFIX)) return null;
60
+ if (!sessionName?.startsWith(SESSION_PREFIX)) return null;
61
61
  const rest = sessionName.slice(SESSION_PREFIX.length);
62
62
  const dashIdx = rest.indexOf("-");
63
63
  if (dashIdx === -1) return rest || null;
@@ -162,7 +162,7 @@ export function createRemoteAdapter(opts = {}) {
162
162
  const loadHosts = deps.loadHostsJson || loadHostsJson;
163
163
  const setIntervalFn = deps.setInterval || setInterval;
164
164
  const clearIntervalFn = deps.clearInterval || clearInterval;
165
- const nowFn = deps.now || Date.now;
165
+ const _nowFn = deps.now || Date.now;
166
166
 
167
167
  const emitter = new EventEmitter();
168
168
  let hostsData = loadHosts(hostsJsonPath);
@@ -175,7 +175,7 @@ export function createRemoteAdapter(opts = {}) {
175
175
  function handleStateChange({ sessionId, from, to, reason }) {
176
176
  const snapshots = conductor.getSnapshot();
177
177
  const entry = snapshots.find((s) => s.id === sessionId);
178
- if (!entry || !entry.remote) return;
178
+ if (!entry?.remote) return;
179
179
 
180
180
  const watcherStatus = getWatcherSession(sessionId);
181
181
  const workerData = buildWorkerData(entry, watcherStatus, hostsData);
@@ -11,7 +11,6 @@ import { createLogDashboard } from "./tui.mjs";
11
11
  import { createLiteDashboard } from "./tui-lite.mjs";
12
12
  import { openHeadlessDashboardTarget } from "./dashboard-open.mjs";
13
13
  import { processHandoff } from "./handoff.mjs";
14
- import { statusBadge } from "./ansi.mjs";
15
14
 
16
15
 
17
16
  // ── CLI 인자 파싱 ──
package/hub/team/tui.mjs CHANGED
@@ -5,7 +5,6 @@
5
5
  import {
6
6
  RESET,
7
7
  FG,
8
- BG,
9
8
  MOCHA,
10
9
  color,
11
10
  dim,
@@ -18,7 +17,6 @@ import {
18
17
  wcswidth,
19
18
  progressBar,
20
19
  statusBadge,
21
- STATUS_ICON,
22
20
  altScreenOn,
23
21
  altScreenOff,
24
22
  clearScreen,
@@ -27,7 +25,6 @@ import {
27
25
  cursorShow,
28
26
  moveTo,
29
27
  clearLine,
30
- clearToEnd,
31
28
  } from "./ansi.mjs";
32
29
 
33
30
  import { execFile as _execFile } from "node:child_process";
@@ -52,7 +49,7 @@ const SPINNER_FRAMES = [...SPINNER_FRAMES_RAW, ...[...SPINNER_FRAMES_RAW].revers
52
49
  const SPINNER_CYCLE_MS = 2000;
53
50
  const SPINNER_BASE_COLOR = { r: 203, g: 166, b: 247 }; // Catppuccin Mocha mauve
54
51
  const SPINNER_SHIMMER = { r: 171, g: 43, b: 63 }; // Claude shimmer #ab2b3f
55
- let spinnerStart = Date.now();
52
+ const spinnerStart = Date.now();
56
53
  let spinnerTick = 0;
57
54
 
58
55
  function lerpRgb(a, b, t) {
@@ -119,7 +116,7 @@ function activityWave(tick, count = 4) {
119
116
  const GRID_GAP = 2;
120
117
  const DEFAULT_DETAIL_LINES = 10;
121
118
  // Tier1 상단 고정 행 수
122
- const TIER1_ROWS = 2;
119
+ const _TIER1_ROWS = 2;
123
120
 
124
121
  const SUMMARY_KEYS = [
125
122
  "status", "lead_action", "verdict", "files_changed",
@@ -128,14 +125,14 @@ const SUMMARY_KEYS = [
128
125
 
129
126
  // ── 레이아웃 브레이크포인트 ──────────────────────────────────────────────
130
127
  // 80-119: 28col rail, 120-159: 36col rail, 160+: 균등
131
- function resolveRailWidth(totalCols, columnCount) {
128
+ function _resolveRailWidth(totalCols, columnCount) {
132
129
  if (columnCount <= 1) return totalCols;
133
130
  if (totalCols >= 160) return Math.floor((totalCols - GRID_GAP * (columnCount - 1)) / columnCount);
134
131
  if (totalCols >= 120) return Math.min(36, Math.floor((totalCols - GRID_GAP * (columnCount - 1)) / columnCount));
135
132
  return Math.min(28, Math.floor((totalCols - GRID_GAP * (columnCount - 1)) / columnCount));
136
133
  }
137
134
 
138
- function autoColumnCount(totalCols, workerCount) {
135
+ function _autoColumnCount(totalCols, workerCount) {
139
136
  if (workerCount <= 1) return 1;
140
137
  if (totalCols >= 160) return Math.min(workerCount, 3);
141
138
  if (totalCols >= 120) return Math.min(workerCount, 2);
@@ -154,7 +151,7 @@ function stripCodeBlocks(text) {
154
151
  .replace(/```[\s\S]*?(?:```|$)/g, "\n")
155
152
  .replace(/^\s*```.*$/gm, "")
156
153
  // indented code blocks (4+ spaces or tab at line start)
157
- .replace(/^(?: |\t).+$/gm, "")
154
+ .replace(/^(?: {4}|\t).+$/gm, "")
158
155
  // shell prompts: PS C:\...>, >, $
159
156
  .replace(/^(?:PS\s+\S[^\n]*?>|>\s+|\$\s+)[^\n]*/gm, "")
160
157
  .trim();
@@ -275,7 +272,7 @@ function gradientBorderFn(topRgb, bottomRgb) {
275
272
  }
276
273
 
277
274
  // Effect 3: Flash-fade border — 상태 변경 시 백색 플래시 → 페이드아웃
278
- function flashFadeBorderColor(currentStatus, prevStatus, changedAt) {
275
+ function _flashFadeBorderColor(currentStatus, prevStatus, changedAt) {
279
276
  const elapsed = Date.now() - (changedAt || 0);
280
277
  if (elapsed >= FADE_DURATION_MS || !prevStatus) return null;
281
278
  const statusRgb = statusToRgb(currentStatus);
@@ -319,11 +316,11 @@ function dedupeRole(role, name, cli) {
319
316
  r = r.replace(new RegExp(esc(cli), "gi"), "").trim();
320
317
  r = r.replace(new RegExp(esc(name), "gi"), "").trim();
321
318
  // CLI indicator emojis 제거
322
- r = r.replace(/[⚪⚫🔴🟠🟡🟢🔵🟣🟤⭕🔘]/g, "").trim();
319
+ r = r.replace(/[⚪⚫🔴🟠🟡🟢🔵🟣🟤⭕🔘]/gu, "").trim();
323
320
  // 빈 괄호 제거 + 중첩 괄호 정리
324
321
  r = r.replace(/\(\s*\)/g, "").trim();
325
322
  r = r.replace(/^\(([^()]+)\)$/, "$1").trim();
326
- r = r.replace(/^\s*[•·\-]\s*/, "").trim();
323
+ r = r.replace(/^\s*[•·-]\s*/, "").trim();
327
324
  return r;
328
325
  }
329
326
 
@@ -350,7 +347,7 @@ function wrapLine(text, width) {
350
347
  return lines.length > 0 ? lines : [source.slice(0, limit)];
351
348
  }
352
349
 
353
- function wrapText(text, width, maxLines = DEFAULT_DETAIL_LINES, rawMode = false) {
350
+ function _wrapText(text, width, maxLines = DEFAULT_DETAIL_LINES, rawMode = false) {
354
351
  if (maxLines <= 0) return [];
355
352
  const input = sanitizeTextBlock(text, rawMode);
356
353
  if (!input) return [];
@@ -465,7 +462,7 @@ function buildWorkerRail(name, st, opts = {}) {
465
462
  selected = false,
466
463
  focused = false, // rail 포커스 여부
467
464
  previousSelected = false,
468
- rawMode = false,
465
+ _rawMode = false,
469
466
  compact = false,
470
467
  time = Date.now(),
471
468
  } = opts;
@@ -713,7 +710,7 @@ function buildHelpOverlay(width, height) {
713
710
  }
714
711
 
715
712
  // ── joinColumns ───────────────────────────────────────────────────────────
716
- function joinColumns(blocks, gap = GRID_GAP) {
713
+ function _joinColumns(blocks, gap = GRID_GAP) {
717
714
  const maxHeight = Math.max(...blocks.map((b) => b.length));
718
715
  return Array.from({ length: maxHeight }, (_, rowIdx) =>
719
716
  blocks
@@ -1418,7 +1415,7 @@ export function renderConductorTier(snapshot, cols = 100) {
1418
1415
  const dataLines = [];
1419
1416
  if (!snapshot || snapshot.length === 0) {
1420
1417
  const emptyMsg = color('(no sessions)', FG.muted);
1421
- const emptyPad = clip(stripAnsi(emptyMsg) === '(no sessions)' ? emptyMsg : emptyMsg, inner);
1418
+ const _emptyPad = clip(stripAnsi(emptyMsg) === '(no sessions)' ? emptyMsg : emptyMsg, inner);
1422
1419
  dataLines.push(`${borderSeq}│${RESET} ${padRight(emptyMsg, inner - 2)} ${borderSeq}│${RESET}`);
1423
1420
  } else {
1424
1421
  for (const s of snapshot) {
@@ -173,7 +173,7 @@ export async function pruneWorktree({ worktreePath, branchName, rootDir = proces
173
173
  try {
174
174
  await git(['worktree', 'remove', worktreePath, ...(forceFlag ? [forceFlag] : [])], rootDir);
175
175
  break;
176
- } catch (err) {
176
+ } catch (_err) {
177
177
  if (attempt === 2) {
178
178
  // Last resort: rm the directory and prune
179
179
  try { await rm(worktreePath, { recursive: true, force: true }); } catch { /* ignore */ }
@@ -30,7 +30,6 @@ const {
30
30
  buildPromptHint,
31
31
  getCodexMcpConfig,
32
32
  getGeminiAllowedServers,
33
- resolveMcpProfile,
34
33
  SUPPORTED_MCP_PROFILES,
35
34
  } = await import(pathToFileURL(mcpFilterPath).href);
36
35
  const SERVER_INFO = { name: 'triflux-delegator', version: '1.0.0' };
@@ -198,7 +197,7 @@ function joinInstructions(...values) {
198
197
  .trim();
199
198
  }
200
199
 
201
- function loadAvailableServersFromSearchEngineCache(cwd = process.cwd()) {
200
+ function _loadAvailableServersFromSearchEngineCache(cwd = process.cwd()) {
202
201
  const cacheFile = resolve(cwd, ...SEARCH_ENGINE_CACHE_PATH);
203
202
  if (!existsSync(cacheFile)) return null;
204
203
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@triflux/remote",
3
- "version": "10.0.1",
3
+ "version": "10.1.0",
4
4
  "description": "triflux remote — team mode, psmux, MCP workers, SQLite store.",
5
5
  "type": "module",
6
6
  "main": "hub/index.mjs",