triflux 7.1.4 → 7.2.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 (73) hide show
  1. package/.claude-plugin/marketplace.json +31 -31
  2. package/.claude-plugin/plugin.json +22 -23
  3. package/bin/triflux.mjs +18 -5
  4. package/hooks/keyword-rules.json +393 -361
  5. package/hub/bridge.mjs +799 -786
  6. package/hub/delegator/contracts.mjs +37 -38
  7. package/hub/delegator/schema/delegator-tools.schema.json +250 -250
  8. package/hub/delegator/service.mjs +307 -302
  9. package/hub/intent.mjs +108 -11
  10. package/hub/lib/process-utils.mjs +20 -0
  11. package/hub/pipe.mjs +589 -589
  12. package/hub/pipeline/gates/confidence.mjs +1 -1
  13. package/hub/pipeline/gates/selfcheck.mjs +2 -4
  14. package/hub/pipeline/state.mjs +191 -187
  15. package/hub/pipeline/transitions.mjs +124 -120
  16. package/hub/public/dashboard.html +355 -349
  17. package/hub/quality/deslop.mjs +5 -3
  18. package/hub/reflexion.mjs +5 -1
  19. package/hub/research.mjs +6 -1
  20. package/hub/router.mjs +791 -782
  21. package/hub/server.mjs +893 -822
  22. package/hub/store.mjs +807 -778
  23. package/hub/team/agent-map.json +10 -0
  24. package/hub/team/ansi.mjs +3 -4
  25. package/hub/team/cli/commands/control.mjs +43 -43
  26. package/hub/team/cli/commands/interrupt.mjs +36 -36
  27. package/hub/team/cli/commands/kill.mjs +3 -3
  28. package/hub/team/cli/commands/send.mjs +37 -37
  29. package/hub/team/cli/commands/start/index.mjs +18 -8
  30. package/hub/team/cli/commands/start/parse-args.mjs +3 -1
  31. package/hub/team/cli/commands/start/start-headless.mjs +4 -1
  32. package/hub/team/cli/commands/status.mjs +87 -87
  33. package/hub/team/cli/commands/stop.mjs +1 -1
  34. package/hub/team/cli/commands/task.mjs +1 -1
  35. package/hub/team/cli/index.mjs +41 -39
  36. package/hub/team/cli/manifest.mjs +29 -28
  37. package/hub/team/cli/services/hub-client.mjs +37 -0
  38. package/hub/team/cli/services/state-store.mjs +26 -12
  39. package/hub/team/dashboard.mjs +11 -4
  40. package/hub/team/handoff.mjs +12 -0
  41. package/hub/team/headless.mjs +202 -200
  42. package/hub/team/native-supervisor.mjs +386 -346
  43. package/hub/team/nativeProxy.mjs +680 -692
  44. package/hub/team/staleState.mjs +361 -369
  45. package/hub/team/tui-viewer.mjs +27 -3
  46. package/hub/team/tui.mjs +1 -0
  47. package/hub/token-mode.mjs +114 -24
  48. package/hub/workers/delegator-mcp.mjs +1059 -1057
  49. package/hud/colors.mjs +88 -0
  50. package/hud/constants.mjs +78 -0
  51. package/hud/hud-qos-status.mjs +206 -1872
  52. package/hud/providers/claude.mjs +309 -0
  53. package/hud/providers/codex.mjs +151 -0
  54. package/hud/providers/gemini.mjs +320 -0
  55. package/hud/renderers.mjs +424 -0
  56. package/hud/terminal.mjs +140 -0
  57. package/hud/utils.mjs +271 -0
  58. package/package.json +1 -2
  59. package/scripts/__tests__/keyword-detector.test.mjs +234 -234
  60. package/scripts/headless-guard-fast.sh +21 -0
  61. package/scripts/headless-guard.mjs +26 -6
  62. package/scripts/lib/keyword-rules.mjs +166 -168
  63. package/scripts/setup.mjs +720 -690
  64. package/scripts/tfx-route-post.mjs +424 -424
  65. package/scripts/tfx-route.sh +1663 -1650
  66. package/scripts/tmp-cleanup.mjs +74 -0
  67. package/skills/tfx-auto/SKILL.md +279 -278
  68. package/skills/tfx-auto-codex/SKILL.md +98 -77
  69. package/skills/tfx-codex/SKILL.md +65 -65
  70. package/skills/tfx-gemini/SKILL.md +83 -82
  71. package/skills/tfx-hub/SKILL.md +205 -136
  72. package/skills/tfx-multi/SKILL.md +11 -5
  73. package/.mcp.json +0 -8
@@ -22,6 +22,14 @@ if (!SESSION) {
22
22
  process.exit(1);
23
23
  }
24
24
 
25
+ // ── psmux 존재 확인 ──
26
+ try {
27
+ execFileSync("psmux", ["--version"], { encoding: "utf8", timeout: 2000 });
28
+ } catch {
29
+ process.stderr.write("ERROR: psmux not found or not executable. Install psmux before running tui-viewer.\n");
30
+ process.exit(1);
31
+ }
32
+
25
33
  const tui = createLogDashboard({ refreshMs: 0 });
26
34
  const startTime = Date.now();
27
35
  tui.setStartTime(startTime);
@@ -38,6 +46,7 @@ function listPanes() {
38
46
  return { index: parseInt(index, 10), title: title || "", pid };
39
47
  });
40
48
  } catch {
49
+ // psmux 미설치 또는 세션 없음 — 빈 목록 반환
41
50
  return [];
42
51
  }
43
52
  }
@@ -49,6 +58,7 @@ function capturePane(paneIdx, lines = 5) {
49
58
  "capture-pane", "-t", `${SESSION}:0.${paneIdx}`, "-p",
50
59
  ], { encoding: "utf8", timeout: 2000 }).trim().split("\n").slice(-lines).join("\n");
51
60
  } catch {
61
+ // pane 캡처 실패 (pane 종료 또는 세션 소멸) — 빈 문자열 반환
52
62
  return "";
53
63
  }
54
64
  }
@@ -62,6 +72,7 @@ function checkResultFile(paneName) {
62
72
  if (content.trim().length === 0) return null;
63
73
  return processHandoff(content, { exitCode: 0, resultFile });
64
74
  } catch {
75
+ // result 파일 파싱 실패 — null 반환하여 진행 중으로 처리
65
76
  return null;
66
77
  }
67
78
  }
@@ -69,9 +80,22 @@ function checkResultFile(paneName) {
69
80
  // ── 메인 폴링 ──
70
81
  const POLL_MS = 1000;
71
82
  const workerState = new Map(); // paneName → { paneIdx, done }
83
+ let emptyPollCount = 0;
72
84
 
73
85
  function poll() {
74
86
  const panes = listPanes();
87
+
88
+ // 세션 사망 감지: 워커 pane이 없는 상태가 지속되면 자동 종료
89
+ if (!panes.some(p => p.index !== 0)) {
90
+ emptyPollCount++;
91
+ const threshold = workerState.size === 0 ? 15 : 10;
92
+ if (emptyPollCount >= threshold) {
93
+ tui.close(); clearInterval(timer); clearInterval(doneCheck); process.exit(0);
94
+ }
95
+ } else {
96
+ emptyPollCount = 0;
97
+ }
98
+
75
99
  // pane 0 = 대시보드 (자기 자신), pane 1+ = 워커
76
100
  for (const pane of panes) {
77
101
  if (pane.index === 0) continue; // 자기 자신 건너뜀
@@ -111,7 +135,7 @@ function poll() {
111
135
  // 완료 감지: (1) result 파일 존재, (2) 셸 프롬프트 복귀, (3) "tokens used" 텍스트
112
136
  const resultFile = join(RESULT_DIR, `${SESSION}-${paneName}.txt`);
113
137
  let resultSize = 0;
114
- try { resultSize = statSync(resultFile).size; } catch {}
138
+ try { resultSize = statSync(resultFile).size; } catch { /* 파일 미존재 — size 0 유지 */ }
115
139
 
116
140
  const shellReturned = /^(PS\s|>|\$)\s*/.test(lastLine) && lines.length > 2;
117
141
  const tokensLine = lines.find(l => /tokens?\s+used/i.test(l));
@@ -152,5 +176,5 @@ const doneCheck = setInterval(() => {
152
176
  }
153
177
  }, 2000);
154
178
 
155
- process.on("SIGINT", () => { tui.close(); clearInterval(timer); process.exit(0); });
156
- setTimeout(() => { tui.close(); clearInterval(timer); process.exit(0); }, 10 * 60 * 1000);
179
+ process.on("SIGINT", () => { tui.close(); clearInterval(timer); clearInterval(doneCheck); process.exit(0); });
180
+ setTimeout(() => { tui.close(); clearInterval(timer); clearInterval(doneCheck); process.exit(0); }, 10 * 60 * 1000);
package/hub/team/tui.mjs CHANGED
@@ -131,6 +131,7 @@ export function createLogDashboard(opts = {}) {
131
131
  render,
132
132
  getWorkers() { return new Map(workers); },
133
133
  getFrameCount() { return frameCount; },
134
+ getPipelineState() { return { ...pipeline }; },
134
135
  close() {
135
136
  if (closed) return;
136
137
  if (timer) clearInterval(timer);
@@ -20,39 +20,105 @@ export const COMPACT_RULES = [
20
20
  { from: ['pending', '대기'], to: '⏸', type: 'symbol' },
21
21
 
22
22
  // ── 약어 (긴 매칭 우선 정렬) ──
23
- { from: ['configuration', '설정'], to: 'cfg', type: 'abbrev' },
24
- { from: ['implementation', '구현'], to: 'impl', type: 'abbrev' },
25
- { from: ['architecture', '아키텍처'], to: 'arch', type: 'abbrev' },
26
- { from: ['dependency', '의존성'], to: 'dep', type: 'abbrev' },
27
- { from: ['function', '함수'], to: 'fn', type: 'abbrev' },
28
- { from: ['parameter', '파라미터'], to: 'param', type: 'abbrev' },
29
- { from: ['repository', '저장소'], to: 'repo', type: 'abbrev' },
30
- { from: ['environment', '환경'], to: 'env', type: 'abbrev' },
31
- { from: ['variable', '변수'], to: 'var', type: 'abbrev' },
32
- { from: ['directory', '디렉토리'], to: 'dir', type: 'abbrev' },
23
+ { from: ['configuration', '설정'], to: 'cfg', type: 'abbrev' },
24
+ { from: ['implementation', '구현'], to: 'impl', type: 'abbrev' },
25
+ { from: ['architecture', '아키텍처'], to: 'arch', type: 'abbrev' },
26
+ { from: ['dependency', '의존성'], to: 'dep', type: 'abbrev' },
27
+ { from: ['function', '함수'], to: 'fn', type: 'abbrev' },
28
+ { from: ['parameter', '파라미터'], to: 'param', type: 'abbrev' },
29
+ { from: ['repository', '저장소'], to: 'repo', type: 'abbrev' },
30
+ { from: ['environment', '환경'], to: 'env', type: 'abbrev' },
31
+ { from: ['variable', '변수'], to: 'var', type: 'abbrev' },
32
+ { from: ['directory', '디렉토리'], to: 'dir', type: 'abbrev' },
33
+
34
+ // ── 한국어 동사/명령형 약어 ──
35
+ { from: ['구현해'], to: 'impl', type: 'abbrev' },
36
+ { from: ['확인해'], to: 'check', type: 'abbrev' },
37
+ { from: ['수정해'], to: 'fix', type: 'abbrev' },
38
+ { from: ['테스트'], to: 'test', type: 'abbrev' },
39
+ { from: ['리뷰'], to: 'review', type: 'abbrev' },
40
+ { from: ['분석'], to: 'analyze', type: 'abbrev' },
41
+ { from: ['설계'], to: 'design', type: 'abbrev' },
42
+ { from: ['문서화'], to: 'docs', type: 'abbrev' },
33
43
  ];
34
44
 
35
- // ── 내부: 정렬된 치환 빌드 (가장 매칭 우선) ──
45
+ /** @type {Array<{ from: string[], to: string, type: 'symbol'|'abbrev' }>} */
46
+ export const REVIEW_RULES = [
47
+ { from: ['looks good to me', 'lgtm'], to: '✓lgtm', type: 'abbrev' },
48
+ { from: ['needs changes', '수정 필요'], to: '✗chg', type: 'abbrev' },
49
+ { from: ['nitpick', '사소한'], to: 'nit', type: 'abbrev' },
50
+ { from: ['blocking', '블로킹'], to: 'blk', type: 'abbrev' },
51
+ { from: ['suggestion', '제안'], to: 'sug', type: 'abbrev' },
52
+ { from: ['question', '질문'], to: 'q', type: 'abbrev' },
53
+ { from: ['approved', '승인'], to: '✓apv', type: 'abbrev' },
54
+ { from: ['request changes', '변경 요청'], to: '✗req', type: 'abbrev' },
55
+ ];
56
+
57
+ /** @type {Array<{ from: string[], to: string, type: 'symbol'|'abbrev' }>} */
58
+ export const DESIGN_RULES = [
59
+ { from: ['component', '컴포넌트'], to: 'cmp', type: 'abbrev' },
60
+ { from: ['interface', '인터페이스'], to: 'iface', type: 'abbrev' },
61
+ { from: ['abstraction', '추상화'], to: 'abs', type: 'abbrev' },
62
+ { from: ['pattern', '패턴'], to: 'ptn', type: 'abbrev' },
63
+ { from: ['dependency injection', '의존성 주입'], to: 'di', type: 'abbrev' },
64
+ { from: ['single responsibility', '단일 책임'], to: 'srp', type: 'abbrev' },
65
+ { from: ['open closed', '개방 폐쇄'], to: 'ocp', type: 'abbrev' },
66
+ { from: ['inheritance', '상속'], to: 'inh', type: 'abbrev' },
67
+ ];
68
+
69
+ /** @type {Array<{ from: string[], to: string, type: 'symbol'|'abbrev' }>} */
70
+ export const DOCS_RULES = [
71
+ { from: ['description', '설명'], to: 'desc', type: 'abbrev' },
72
+ { from: ['example', '예시'], to: 'ex', type: 'abbrev' },
73
+ { from: ['reference', '참조'], to: 'ref', type: 'abbrev' },
74
+ { from: ['introduction', '소개'], to: 'intro', type: 'abbrev' },
75
+ { from: ['deprecated', '사용 중단'], to: 'dep', type: 'abbrev' },
76
+ { from: ['optional', '선택적'], to: 'opt', type: 'abbrev' },
77
+ { from: ['required', '필수'], to: 'req', type: 'abbrev' },
78
+ { from: ['returns', '반환'], to: 'ret', type: 'abbrev' },
79
+ ];
80
+
81
+ // ── 프로필 맵 ──
36
82
 
37
- /** @type {Array<{ pattern: RegExp, to: string }>} */
38
- const _compactPairs = [];
83
+ /** @type {Record<string, Array<{ from: string[], to: string, type: 'symbol'|'abbrev' }>>} */
84
+ const PROFILE_MAP = {
85
+ default: COMPACT_RULES,
86
+ review: [...COMPACT_RULES, ...REVIEW_RULES],
87
+ design: [...COMPACT_RULES, ...DESIGN_RULES],
88
+ docs: [...COMPACT_RULES, ...DOCS_RULES],
89
+ };
39
90
 
40
- for (const rule of COMPACT_RULES) {
41
- for (const keyword of rule.from) {
42
- _compactPairs.push({ pattern: keyword, to: rule.to, len: keyword.length });
91
+ // ── 내부: 정렬된 치환 쌍 빌드 헬퍼 ──
92
+
93
+ /**
94
+ * 규칙 배열로부터 패턴 쌍을 빌드하고 긴 패턴 우선 정렬
95
+ * @param {Array<{ from: string[], to: string }>} rules
96
+ * @returns {Array<{ pattern: string, to: string, len: number }>}
97
+ */
98
+ function buildCompactPairs(rules) {
99
+ const pairs = [];
100
+ for (const rule of rules) {
101
+ for (const keyword of rule.from) {
102
+ pairs.push({ pattern: keyword, to: rule.to, len: keyword.length });
103
+ }
43
104
  }
105
+ pairs.sort((a, b) => b.len - a.len);
106
+ return pairs;
44
107
  }
45
- // greedy: 긴 패턴 먼저
46
- _compactPairs.sort((a, b) => b.len - a.len);
47
108
 
48
- /** @type {Array<{ pattern: RegExp, to: string }>} */
49
- const _expandPairs = [];
50
-
51
- for (const rule of COMPACT_RULES) {
52
- // expand 시 첫 번째 from 값(영어 우선)으로 복원
53
- _expandPairs.push({ symbol: rule.to, restore: rule.from[0] });
109
+ /**
110
+ * 규칙 배열로부터 확장 쌍을 빌드
111
+ * @param {Array<{ from: string[], to: string }>} rules
112
+ * @returns {Array<{ symbol: string, restore: string }>}
113
+ */
114
+ function buildExpandPairs(rules) {
115
+ return rules.map((rule) => ({ symbol: rule.to, restore: rule.from[0] }));
54
116
  }
55
117
 
118
+ // 기본 프로필 쌍 (기존 동작 유지)
119
+ const _compactPairs = buildCompactPairs(COMPACT_RULES);
120
+ const _expandPairs = buildExpandPairs(COMPACT_RULES);
121
+
56
122
  // ── 코드 블록 보호 유틸 ──
57
123
 
58
124
  const CODE_BLOCK_RE = /```[\s\S]*?```/g;
@@ -114,6 +180,29 @@ export function compactify(text) {
114
180
  });
115
181
  }
116
182
 
183
+ /**
184
+ * 텍스트를 compact 모드로 변환 (도메인 프로필 선택 가능)
185
+ * @param {string} text
186
+ * @param {'default'|'review'|'design'|'docs'} [profile='default'] — 도메인 프로필
187
+ * @returns {string}
188
+ */
189
+ export function applyCompactRules(text, profile = 'default') {
190
+ if (!text || typeof text !== 'string') return text ?? '';
191
+
192
+ const rules = PROFILE_MAP[profile] ?? COMPACT_RULES;
193
+ const pairs = buildCompactPairs(rules);
194
+
195
+ return withCodeProtection(text, (segment) => {
196
+ let result = segment;
197
+ for (const { pattern, to } of pairs) {
198
+ const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
199
+ const re = new RegExp(escaped, 'gi');
200
+ result = result.replace(re, to);
201
+ }
202
+ return result;
203
+ });
204
+ }
205
+
117
206
  /**
118
207
  * compact 텍스트를 원래 형태로 복원 (best-effort)
119
208
  * @param {string} text
@@ -121,6 +210,7 @@ export function compactify(text) {
121
210
  */
122
211
  export function expand(text) {
123
212
  if (!text || typeof text !== 'string') return text ?? '';
213
+ _compactMode = false;
124
214
 
125
215
  return withCodeProtection(text, (segment) => {
126
216
  let result = segment;