triflux 10.9.27 → 10.9.29

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 (83) hide show
  1. package/CLAUDE.md +212 -0
  2. package/hub/account-broker.mjs +1 -1
  3. package/hub/server.mjs +4 -3
  4. package/hub/team/conductor.mjs +4 -0
  5. package/hub/team/synapse-registry.mjs +1 -1
  6. package/package.json +21 -62
  7. package/references/hosts.json +46 -0
  8. package/scripts/headless-guard.mjs +3 -1
  9. package/scripts/lib/mcp-filter.mjs +17 -0
  10. package/scripts/tfx-route.sh +10 -4
  11. package/skills/tfx-workspace/async-tests/run-tests.sh +203 -0
  12. package/skills/tfx-workspace/evals/evals.json +79 -0
  13. package/skills/tfx-workspace/iteration-1/benchmark.json +524 -0
  14. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/eval_metadata.json +11 -0
  15. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/grading.json +25 -0
  16. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/outputs/analysis.md +154 -0
  17. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/timing.json +5 -0
  18. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/grading.json +25 -0
  19. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/outputs/analysis.md +126 -0
  20. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/timing.json +5 -0
  21. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/eval_metadata.json +11 -0
  22. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/grading.json +25 -0
  23. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/outputs/analysis.md +119 -0
  24. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/timing.json +5 -0
  25. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/grading.json +25 -0
  26. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/outputs/analysis.md +115 -0
  27. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/timing.json +5 -0
  28. package/skills/tfx-workspace/iteration-1/hub-start-sequence/eval_metadata.json +10 -0
  29. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/grading.json +20 -0
  30. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/outputs/analysis.md +86 -0
  31. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/timing.json +5 -0
  32. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/grading.json +20 -0
  33. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/outputs/analysis.md +81 -0
  34. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/timing.json +5 -0
  35. package/skills/tfx-workspace/iteration-1/multi-team-creation/eval_metadata.json +12 -0
  36. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/grading.json +30 -0
  37. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/outputs/analysis.md +316 -0
  38. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/timing.json +5 -0
  39. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/grading.json +30 -0
  40. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/outputs/analysis.md +352 -0
  41. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/timing.json +5 -0
  42. package/skills/tfx-workspace/iteration-1/review.html +1325 -0
  43. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/eval_metadata.json +12 -0
  44. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/grading.json +30 -0
  45. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/outputs/analysis.md +97 -0
  46. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/timing.json +5 -0
  47. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/grading.json +30 -0
  48. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/outputs/analysis.md +94 -0
  49. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/timing.json +5 -0
  50. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/eval_metadata.json +12 -0
  51. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/grading.json +30 -0
  52. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/outputs/analysis.md +209 -0
  53. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/timing.json +5 -0
  54. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/grading.json +30 -0
  55. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/outputs/analysis.md +193 -0
  56. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/timing.json +5 -0
  57. package/skills/tfx-workspace/iteration-2/benchmark.json +144 -0
  58. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/eval_metadata.json +13 -0
  59. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/grading.json +35 -0
  60. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/outputs/analysis.md +382 -0
  61. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/timing.json +5 -0
  62. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/grading.json +35 -0
  63. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/outputs/analysis.md +333 -0
  64. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/timing.json +5 -0
  65. package/skills/tfx-workspace/iteration-2/review.html +1325 -0
  66. package/skills/tfx-workspace/skill-snapshot/tfx-auto/SKILL.md +217 -0
  67. package/skills/tfx-workspace/skill-snapshot/tfx-auto-codex/SKILL.md +77 -0
  68. package/skills/tfx-workspace/skill-snapshot/tfx-codex/SKILL.md +65 -0
  69. package/skills/tfx-workspace/skill-snapshot/tfx-doctor/SKILL.md +94 -0
  70. package/skills/tfx-workspace/skill-snapshot/tfx-gemini/SKILL.md +82 -0
  71. package/skills/tfx-workspace/skill-snapshot/tfx-hub/SKILL.md +133 -0
  72. package/skills/tfx-workspace/skill-snapshot/tfx-multi/SKILL.md +426 -0
  73. package/skills/tfx-workspace/skill-snapshot/tfx-setup/SKILL.md +101 -0
  74. package/.claude-plugin/marketplace.json +0 -34
  75. package/.claude-plugin/plugin.json +0 -22
  76. package/config/mcp-registry.json +0 -29
  77. package/tui/codex-profile.mjs +0 -457
  78. package/tui/core.mjs +0 -266
  79. package/tui/doctor.mjs +0 -375
  80. package/tui/gemini-profile.mjs +0 -299
  81. package/tui/monitor-data.mjs +0 -152
  82. package/tui/monitor.mjs +0 -339
  83. package/tui/setup.mjs +0 -598
package/CLAUDE.md ADDED
@@ -0,0 +1,212 @@
1
+ <!-- prompt-hygiene:ignore line_count_warning -->
2
+ # triflux — Claude Code 운영 가이드
3
+
4
+ <core-systems>
5
+ ## 핵심 스킬 시스템 (항상 인지)
6
+
7
+ 이 프로젝트는 3개의 스킬 시스템을 동시에 사용한다. 어떤 작업이든 해당 시스템의 스킬이 있는지 먼저 확인한다.
8
+
9
+ | 시스템 | 접두사 | 용도 | 스킬 수 |
10
+ |--------|--------|------|---------|
11
+ | **triflux** | `/tfx-*` | CLI 라우팅, 멀티모델 오케스트레이션, 스웜, 원격 실행 | ~40개 |
12
+ | **gstack** | `/` (접두사 없음) | QA, ship, investigate, design, review, checkpoint | ~35개 |
13
+ | **omc** | `/oh-my-claudecode:*` | autopilot, ralph, team, ultrawork, ccg | ~25개 |
14
+
15
+ 스킬을 모르면 자연어 라우팅(아래)으로 자동 매핑된다.
16
+ 세션 종료 전 메모리 파일이 3개+ 변경됐으면 `/memory-hygiene` 제안을 검토한다.
17
+ </core-systems>
18
+
19
+ <psmux-wt>
20
+ ## psmux/WT 규칙
21
+
22
+ psmux 세션·WT 패인을 생성/조작/정리할 때 `tfx-psmux-rules` 스킬을 참조한다.
23
+ WT 프리징 방지: exit → sleep 2 → kill 순서. 바로 kill하지 않는다.
24
+
25
+ ### wt.exe → wt-manager 경유
26
+
27
+ safety-guard가 `wt.exe`, `wt new-tab`, `wt split-pane`, `Start-Process wt`를 차단한다.
28
+ `hub/team/wt-manager.mjs`의 API를 사용한다.
29
+
30
+ | 용도 | API |
31
+ |------|-----|
32
+ | 새 탭 | `createTab({ title, command, profile, cwd })` |
33
+ | 패인 분할 | `splitPane({ direction: 'H'\|'V', title, command })` |
34
+ | 다중 배치 | `applySplitLayout([{ title, command, direction }])` |
35
+ | 탭 정리 | `closeTab(title)` / `closeStale({ olderThanMs, titlePattern })` |
36
+
37
+ 차단과 대안은 항상 쌍으로 존재해야 한다. 차단만 추가하고 대안을 안 만들면 데드락.
38
+
39
+ ### psmux에서 Codex 실행
40
+
41
+ | 방식 | 동작 | 이유 |
42
+ |------|------|------|
43
+ | `codex` (interactive) | 불가 | psmux에서 TTY를 못 잡음 |
44
+ | `codex < prompt.md` | 불가 | "stdin is not a terminal" |
45
+ | `codex exec "$(cat prompt.md)" -s danger-full-access --dangerously-bypass-approvals-and-sandbox` | 사용 | 유일한 안전 경로 |
46
+
47
+ `codex exec`는 config.toml `approval_mode`를 무시하므로 `--dangerously-bypass-approvals-and-sandbox` 필수.
48
+ `-s` 유효값: read-only, workspace-write, danger-full-access.
49
+ </psmux-wt>
50
+
51
+ <codex-config>
52
+ ## Codex config.toml
53
+
54
+ config.toml에 이미 설정된 값은 CLI 플래그로 중복 지정하지 않는다.
55
+
56
+ | config.toml에 있으면 | CLI에서 생략 |
57
+ |---------------------|-------------|
58
+ | `approval_mode = "auto"` | `-a`, `--full-auto` |
59
+ | `sandbox = "workspace-write"` | `-s`, `--full-auto` |
60
+
61
+ 안전 패턴: config.toml에 기본값을 두고, CLI에서는 `--profile` 선택만 한다.
62
+ </codex-config>
63
+
64
+ <account-broker>
65
+ ## AccountBroker (계정 브로커)
66
+
67
+ conductor, headless, swarm-hypervisor가 하나의 AccountBroker 싱글턴을 공유한다.
68
+
69
+ | 항목 | 설명 |
70
+ |------|------|
71
+ | 계정별 CircuitBreaker | 장애 격리 — 한 계정 오류가 다른 계정에 전파되지 않음 |
72
+ | busy 플래그 | 동일 계정 이중 임대(double-lease) 방지 |
73
+ | `/broker/reload` | 장시간 세션 중 accounts.json 핫리로드 |
74
+ | EventEmitter 이벤트 | `lease`, `release`, `cooldown`, `tierFallback`, `circuitOpen`, `circuitClose`, `noAvailableAccounts` — HUD 연동용 |
75
+ </account-broker>
76
+
77
+ <remote>
78
+ ## 원격 실행
79
+
80
+ ### 스킬 구분
81
+
82
+ | 스킬 | 대상 | 방식 |
83
+ |------|------|------|
84
+ | tfx-codex-swarm | 로컬 전용 | 로컬 worktree + psmux |
85
+ | tfx-remote-spawn | Claude Code 원격 | SSH → Claude Code 세션 → 내부 tfx 라우팅 |
86
+
87
+ codex를 SSH 너머로 직접 실행하지 않는다. config.toml 충돌 + TTY 문제.
88
+ 원격에서 codex가 필요하면: remote-spawn → Claude Code → Claude가 내부에서 codex 호출.
89
+
90
+ ### SSH 패턴
91
+
92
+ hosts.json `os` 필드로 대상 셸을 판단한다. safety-guard도 이 필드를 참조.
93
+
94
+ | 대상 OS | 셸 | 패턴 |
95
+ |---------|-----|------|
96
+ | windows | PowerShell | scp + `pwsh -File` 필수. `$var` → `$env:VAR`, `2>/dev/null` → `2>$null` |
97
+ | darwin | zsh | 인라인 가능. brew PATH 주의 (`/opt/homebrew/bin`) |
98
+ | linux | bash | 인라인 가능. 표준 POSIX |
99
+
100
+ - `~` → `$HOME` 변환은 모든 OS 공통
101
+ </remote>
102
+
103
+ <headless-retrieval>
104
+ ## Headless 결과 회수
105
+
106
+ background로 실행한 headless 결과는 **반드시 task-notification 완료 후** 읽는다.
107
+
108
+ | 패턴 | 올바름 | 이유 |
109
+ |------|--------|------|
110
+ | task-notification 후 output 파일 읽기 | YES | 프로세스 종료 = 워커 전부 완료 |
111
+ | task-notification 전 output 파일 tail | NO | 시작 메시지만 보이고 "실패"로 오진 |
112
+ | psmux capture-pane으로 중간 체크 | NO | 워커 진행 중이면 빈 화면일 수 있음 |
113
+
114
+ 완료 마커: `=== HEADLESS_COMPLETE succeeded=N failed=N total=N ===`
115
+ 워커 상세: `$TMPDIR/tfx-headless/{sessionName}-worker-N.txt`
116
+ </headless-retrieval>
117
+
118
+ <cross-review>
119
+ ## 교차 검증
120
+
121
+ - Claude 작성 코드 → Codex 리뷰
122
+ - Codex 작성 코드 → Claude 리뷰
123
+ - 동일 모델 self-approve 하지 않는다
124
+ - git commit 전 미검증 파일 감지 시 nudge
125
+ </cross-review>
126
+
127
+ <session-context>
128
+ ## 맥락 이탈 판단
129
+
130
+ 현재 세션 맥락과 무관한 요청이 감지되면 psmux 격리를 제안한다.
131
+
132
+ | 확신도 | 신호 | 행동 |
133
+ |--------|------|------|
134
+ | 확실 | "새 탭", "별도로", "새 세션" | 바로 psmux spawn |
135
+ | 높음 | 다른 프로젝트/스택 언급 | 분리 제안 |
136
+ | 중간 | 작업 유형 전환 | 분리 제안 + 현재 세션 옵션 |
137
+ | 낮음 | 현재 작업 연장 | 세션 유지 |
138
+ </session-context>
139
+
140
+ <routing>
141
+ ## 자연어 → 스킬 라우팅
142
+
143
+ 사용자가 스킬명을 모르더라도 자연어로 요청하면 아래 규칙에 따라 적절한 스킬을 호출한다.
144
+
145
+ ### 행동 유형 → 스킬 매핑
146
+
147
+ | 의도 | 자연어 신호 | 스킬 |
148
+ |------|-----------|------|
149
+ | 구현/수정 | 만들어, 고쳐, 구현해, 짜줘, 수정해, 바꿔 | tfx-auto |
150
+ | 리뷰 | 봐줘, 리뷰해, 검토해, 괜찮아? | tfx-review |
151
+ | 분석 | 분석해, 어떻게 돌아가?, 구조가 뭐야 | tfx-analysis |
152
+ | 계획 | 계획, 어떻게 하지, 설계해 | tfx-plan |
153
+ | 검색 | 찾아, 어디있어, 파일 찾아 | tfx-find |
154
+ | 리서치 (빠른) | 검색해줘, 찾아봐, 공식문서, 이거 뭐야 | tfx-research |
155
+ | 리서치 (자율) | 자율 리서치, 검색하고 정리해, research and plan | tfx-autoresearch |
156
+ | 테스트 | 테스트, 검증, 돌려봐, QA | tfx-qa |
157
+ | 정리 | 정리해, 슬롭 제거, 클린업 | tfx-prune |
158
+ | 토론 | 뭐가 나을까, 비교해, A vs B | tfx-debate |
159
+
160
+ ### 깊이 수정자
161
+
162
+ | 수정자 | 신호 | 효과 |
163
+ |--------|------|------|
164
+ | 기본 | (없음), 빠르게, 간단히 | Light 스킬 |
165
+ | 깊이 | 제대로, 꼼꼼히, 철저히 | Deep 스킬 (tfx-deep-*). 예외: tfx-deep-interview는 Gemini 단독 |
166
+ | 합의 | 3자, 교차, 다각도 | consensus 프로토콜 |
167
+ | 반복 | 끝까지, 멈추지마, ralph | persist 모드 |
168
+ | 자율 | 알아서, 자동으로, autopilot | autopilot 모드 |
169
+
170
+ ### CLI 라우팅
171
+
172
+ headless-guard가 `codex exec` / `gemini -y -p` 직접 호출을 차단한다. tfx 스킬 경유 필수.
173
+
174
+ **Layer 1 — Light** (tfx-route.sh → 단일 CLI)
175
+
176
+ | 스킬 | CLI | 용도 |
177
+ |------|-----|------|
178
+ | tfx-auto | 자동 | 통합 진입점 |
179
+ | tfx-codex | Codex | Codex 전용 |
180
+ | tfx-gemini | Gemini | Gemini 전용 |
181
+ | tfx-autopilot | Codex→검증 | 단일 파일, 5분 이내 |
182
+ | tfx-autoroute | 자동 승격 | 실패→더 강한 모델 |
183
+
184
+ **Layer 2 — Deep** (headless 3-CLI 합의)
185
+
186
+ tfx-deep-review, tfx-deep-qa, tfx-deep-plan, tfx-deep-research, tfx-consensus, tfx-debate, tfx-panel, tfx-fullcycle, tfx-persist
187
+
188
+ **Layer 3 — Remote/병렬**
189
+
190
+ | 스킬 | 용도 |
191
+ |------|------|
192
+ | tfx-multi | 2+개 태스크 headless 병렬 |
193
+ | tfx-swarm | PRD별 worktree + 다중 모델(Codex/Gemini/Claude) + 다중 기기(로컬+원격) |
194
+ | tfx-remote-spawn | Claude Code 원격 세션 (SSH, setup 필수) |
195
+
196
+ **Claude 네이티브** (CLI 불필요): tfx-find, tfx-forge, tfx-prune, tfx-index, tfx-setup, tfx-doctor, tfx-hooks, tfx-hub
197
+
198
+ 자원 우선순위: remote-spawn > swarm > multi > Light > 로컬 단독
199
+
200
+ ### 충돌 해소
201
+
202
+ - ralph = persist alias
203
+ - "auto" 단독 → tfx-auto. "알아서 해" → tfx-autopilot
204
+ - "코드에서 찾아" → tfx-find. "알아봐" → tfx-research
205
+ - 복합 의도: "구현하고 리뷰까지" → tfx-auto → cross-review hook
206
+
207
+ ### Q-Learning 동적 라우팅 (실험적)
208
+
209
+ - `TRIFLUX_DYNAMIC_ROUTING=true` 또는 `1` 설정 시 Q-Learning 기반 동적 스킬 라우팅 활성화
210
+ - `routing-weights.json` + Q-table로 스킬 선택 최적화
211
+ - 기본 비활성
212
+ </routing>
@@ -77,7 +77,7 @@ function persistState(stateMap) {
77
77
  }
78
78
  mkdirSync(AUTH_BASE_PATH, { recursive: true });
79
79
  writeFileSync(STATE_PERSIST_PATH, JSON.stringify({ ts: now, entries }));
80
- } catch { /* best-effort */ }
80
+ } catch (err) { try { console.error("[account-broker] persistState failed:", err.message); } catch {} }
81
81
  }
82
82
 
83
83
  function loadPersistedState() {
package/hub/server.mjs CHANGED
@@ -80,6 +80,7 @@ const AIMD_WINDOW_MS = 30 * 60 * 1000;
80
80
  const AIMD_INITIAL_BATCH_SIZE = 3;
81
81
  const AIMD_MIN_BATCH_SIZE = 1;
82
82
  const AIMD_MAX_BATCH_SIZE = 10;
83
+ const SYNAPSE_VALID_OPS = new Set(["checkout", "rebase", "cherry-pick", "reset", "stash-pop", "worktree-remove"]);
83
84
  const HUB_IDLE_TIMEOUT_DEFAULT_MS = 0; // 0 = 영구 실행 (idle shutdown 비활성). TFX_HUB_IDLE_TIMEOUT_MS 환경변수로 오버라이드 가능
84
85
  const HUB_IDLE_SWEEP_DEFAULT_MS = 60 * 1000;
85
86
  const STATIC_CONTENT_TYPES = Object.freeze({
@@ -809,7 +810,7 @@ export async function startHub({
809
810
  if (path === "/synapse/heartbeat" && req.method === "POST") {
810
811
  try {
811
812
  const body = await parseBody(req);
812
- const { sessionId, partial } = body || {};
813
+ const { sessionId, ...partial } = body || {};
813
814
  const ok = synapseRegistry.heartbeat(sessionId, partial);
814
815
  if (!ok) {
815
816
  throw new Error("heartbeat failed");
@@ -839,14 +840,13 @@ export async function startHub({
839
840
  }
840
841
 
841
842
  if (path === "/synapse/preflight" && req.method === "POST") {
842
- const VALID_OPS = new Set(["checkout", "rebase", "cherry-pick", "reset", "stash-pop", "worktree-remove"]);
843
843
  try {
844
844
  const body = await parseBody(req);
845
845
  const { op, args = {}, sessionContext = {} } = body;
846
846
  if (!op || typeof op !== "string") {
847
847
  return writeJson(res, 400, { ok: false, error: "op 필수" });
848
848
  }
849
- if (!VALID_OPS.has(op)) {
849
+ if (!SYNAPSE_VALID_OPS.has(op)) {
850
850
  return writeJson(res, 400, { ok: false, error: `invalid op: ${op}` });
851
851
  }
852
852
  const result = gitPreflight.check(op, args, sessionContext);
@@ -1814,6 +1814,7 @@ const QUOTA_CACHE_PATH = join(CACHE_DIR, "broker-quota-cache.json");
1814
1814
  async function checkSingleAccountQuota(acct) {
1815
1815
  try {
1816
1816
  const authPath = join(PID_DIR, acct.authFile);
1817
+ if (!authPath.startsWith(PID_DIR + sep)) return { id: acct.id, status: "path_blocked" };
1817
1818
  if (!existsSync(authPath)) return { id: acct.id, status: "no_auth" };
1818
1819
  const auth = JSON.parse(readFileSync(authPath, "utf8"));
1819
1820
  if (acct.provider === "codex") {
@@ -733,6 +733,9 @@ export function createConductor(opts = {}) {
733
733
  if (code === 0) {
734
734
  transition(session, STATES.COMPLETED, `exit_${code}`);
735
735
  emitter.emit("completed", { sessionId: session.id });
736
+ if (typeof session.config.onCompleted === "function") {
737
+ session.config.onCompleted({ sessionId: session.id });
738
+ }
736
739
  maybeAutoShutdown();
737
740
  } else {
738
741
  handleFailure(session, `remote_exit_${code}`);
@@ -891,6 +894,7 @@ export function createConductor(opts = {}) {
891
894
  }
892
895
 
893
896
  if (!session.child) return false;
897
+ if (!session.child.stdin?.writable) return false;
894
898
  try {
895
899
  session.child.stdin.write(`${text}\n`);
896
900
  eventLog.append("stdin", { session: id, text: text.slice(0, 100) });
@@ -152,7 +152,7 @@ export function createSynapseRegistry(opts = {}) {
152
152
  const staled = { ...current, status: "stale" };
153
153
  sessions.set(sessionId, staled);
154
154
  schedulePersist();
155
- setImmediate(() => notifyStale(staled));
155
+ setImmediate(() => { if (!destroyed) notifyStale(staled); });
156
156
  }
157
157
  }, intervalFor(session));
158
158
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "10.9.27",
3
+ "version": "10.9.29",
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
  }
@@ -0,0 +1,46 @@
1
+ {
2
+ "hosts": {
3
+ "ultra4": {
4
+ "description": "Windows 데스크탑 (22코어/64GB RAM)",
5
+ "aliases": ["울트라", "울트라4", "데스크탑"],
6
+ "default_dir": "~/Desktop/Projects/cli/triflux",
7
+ "tailscale": {
8
+ "ip": "100.110.136.64",
9
+ "dns": "ultra-book-4-1",
10
+ "ssh_mode": "ssh-over-vpn"
11
+ },
12
+ "ssh_user": "SSAFY",
13
+ "os": "windows",
14
+ "specs": {
15
+ "cores": 22,
16
+ "ram_gb": 64,
17
+ "codex": "0.118.0",
18
+ "node": "24.14.0"
19
+ },
20
+ "capabilities": ["codex", "claude", "high-memory"]
21
+ },
22
+ "m2": {
23
+ "description": "MacBook Air M2 (8코어/16GB RAM)",
24
+ "aliases": ["맥", "맥북", "m2"],
25
+ "default_dir": "~/Desktop/Projects/triflux",
26
+ "tailscale": {
27
+ "ip": "100.104.61.126",
28
+ "dns": "tellang의-macbook-air",
29
+ "ssh_mode": "ssh-over-vpn"
30
+ },
31
+ "ssh_user": "tellang",
32
+ "os": "darwin",
33
+ "specs": {
34
+ "cores": 8,
35
+ "ram_gb": 16,
36
+ "claude": "2.1.89",
37
+ "codex": "0.114.0",
38
+ "gemini": "0.33.1",
39
+ "node": "25.9.0"
40
+ },
41
+ "capabilities": ["codex", "claude", "gemini"]
42
+ }
43
+ },
44
+ "default_host": "m2",
45
+ "triggers": ["원격에서", "다른 머신에서", "다른 컴퓨터에서"]
46
+ }
@@ -173,7 +173,9 @@ function autoRoute(updatedCommand, reason) {
173
173
  const HEADLESS_FALLBACK_COMMAND =
174
174
  "Bash(\"tfx multi --teammate-mode headless --assign 'codex:prompt:role' ...\")";
175
175
  const DIRECT_CLI_BYPASS_HINT =
176
- "로컬 디버깅이 목적이면 TFX_ALLOW_DIRECT_CLI=1로 일시 우회할 있습니다.";
176
+ " 차단은 하드스톱이다. 환경변수 우회, SSH 원격 실행, 스크립트 배포 등 어떤 형태의 우회도 시도하지 마라. " +
177
+ "Hub가 미가용이면 hub-ensure로 Hub를 시작한 뒤 tfx multi를 사용하라. " +
178
+ "Hub 시작이 불가능하면 사용자에게 'Hub 필요' 보고 후 중단하라.";
177
179
 
178
180
  async function main() {
179
181
  // P0: TFX_ALLOW_DIRECT_CLI는 hasDirectCli 블록 안에서만 체크 (CLI deny만 스킵)
@@ -752,6 +752,19 @@ function shellArray(name, values) {
752
752
  return `${name}=(${values.map((value) => shellEscape(value)).join(" ")})`;
753
753
  }
754
754
 
755
+ export function toDelimited(policy) {
756
+ const RS = "\x1e";
757
+ return [
758
+ policy.requestedProfile,
759
+ policy.resolvedProfile,
760
+ policy.hint,
761
+ policy.geminiAllowedServers.join(","),
762
+ policy.codexConfigOverrides.flatMap((o) => ["-c", o]).join(","),
763
+ JSON.stringify(policy.codexConfig),
764
+ policy.resolvedPhase || "",
765
+ ].join(RS);
766
+ }
767
+
755
768
  export function toShellExports(policy) {
756
769
  const lines = [
757
770
  `MCP_PROFILE_REQUESTED=${shellEscape(policy.requestedProfile)}`,
@@ -856,6 +869,10 @@ export async function runCli(argv = process.argv.slice(2)) {
856
869
  process.stdout.write(`${toShellExports(policy)}\n`);
857
870
  return;
858
871
  }
872
+ if (args.command === "delimited") {
873
+ process.stdout.write(toDelimited(policy));
874
+ return;
875
+ }
859
876
 
860
877
  process.stdout.write(`${JSON.stringify(policy, null, 2)}\n`);
861
878
  }
@@ -64,6 +64,7 @@ track_worker_pid() {
64
64
  }
65
65
 
66
66
  cleanup_workers() {
67
+ _codex_config_swap "restore" 2>/dev/null || true
67
68
  deregister_agent 2>/dev/null || true
68
69
  [[ ! -f "$_PID_TRACK" ]] && return
69
70
  while IFS= read -r pid; do
@@ -1203,7 +1204,7 @@ resolve_mcp_policy() {
1203
1204
  fi
1204
1205
 
1205
1206
  local -a cmd=(
1206
- "$NODE_BIN" "$filter_script" shell
1207
+ "$NODE_BIN" "$filter_script" delimited
1207
1208
  "--agent" "$AGENT_TYPE"
1208
1209
  "--profile" "$MCP_PROFILE"
1209
1210
  "--available" "$available_servers"
@@ -1213,13 +1214,18 @@ resolve_mcp_policy() {
1213
1214
  [[ -n "$TFX_SEARCH_TOOL" ]] && cmd+=("--search-tool" "$TFX_SEARCH_TOOL")
1214
1215
  [[ -n "$TFX_WORKER_INDEX" ]] && cmd+=("--worker-index" "$TFX_WORKER_INDEX")
1215
1216
 
1216
- local shell_exports
1217
- if ! shell_exports="$("${cmd[@]}")"; then
1217
+ local _raw
1218
+ if ! _raw="$("${cmd[@]}")"; then
1218
1219
  echo "[tfx-route] ERROR: MCP 정책 계산 실패" >&2
1219
1220
  return 1
1220
1221
  fi
1221
1222
 
1222
- eval "$shell_exports"
1223
+ local _gemini_servers _codex_flags _phase
1224
+ IFS=$'\x1e' read -r MCP_PROFILE_REQUESTED MCP_RESOLVED_PROFILE MCP_HINT \
1225
+ _gemini_servers _codex_flags CODEX_CONFIG_JSON _phase <<< "$_raw"
1226
+ IFS=',' read -r -a GEMINI_ALLOWED_SERVERS <<< "$_gemini_servers"
1227
+ IFS=',' read -r -a CODEX_CONFIG_FLAGS <<< "$_codex_flags"
1228
+ [[ -n "$_phase" ]] && MCP_PIPELINE_PHASE="$_phase"
1223
1229
  }
1224
1230
 
1225
1231
  get_claude_model() {