triflux 10.7.1 → 10.8.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.
- package/CLAUDE.md +212 -0
- package/hub/cli-adapter-base.mjs +0 -72
- package/hub/lib/spawn-trace.mjs +1 -32
- package/hub/server.mjs +12 -0
- package/hub/team/conductor.mjs +13 -1
- package/hub/team/remote-session.mjs +2 -1
- package/hub/team/tui.mjs +218 -59
- package/package.json +21 -56
- package/references/hosts.json +46 -0
- package/scripts/__tests__/spawn-trace.test.mjs +1 -3
- package/scripts/headless-guard.mjs +6 -4
- package/skills/tfx-workspace/async-tests/run-tests.sh +203 -0
- package/skills/tfx-workspace/evals/evals.json +79 -0
- package/skills/tfx-workspace/iteration-1/benchmark.json +524 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/eval_metadata.json +11 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/grading.json +25 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/outputs/analysis.md +154 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/grading.json +25 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/outputs/analysis.md +126 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/eval_metadata.json +11 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/grading.json +25 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/outputs/analysis.md +119 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/grading.json +25 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/outputs/analysis.md +115 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/eval_metadata.json +10 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/grading.json +20 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/outputs/analysis.md +86 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/grading.json +20 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/outputs/analysis.md +81 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/eval_metadata.json +12 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/grading.json +30 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/outputs/analysis.md +316 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/grading.json +30 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/outputs/analysis.md +352 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/review.html +1325 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/eval_metadata.json +12 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/grading.json +30 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/outputs/analysis.md +97 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/grading.json +30 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/outputs/analysis.md +94 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/eval_metadata.json +12 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/grading.json +30 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/outputs/analysis.md +209 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/grading.json +30 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/outputs/analysis.md +193 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-2/benchmark.json +144 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/eval_metadata.json +13 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/grading.json +35 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/outputs/analysis.md +382 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/grading.json +35 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/outputs/analysis.md +333 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-2/review.html +1325 -0
- package/skills/tfx-workspace/skill-snapshot/tfx-auto/SKILL.md +217 -0
- package/skills/tfx-workspace/skill-snapshot/tfx-auto-codex/SKILL.md +77 -0
- package/skills/tfx-workspace/skill-snapshot/tfx-codex/SKILL.md +65 -0
- package/skills/tfx-workspace/skill-snapshot/tfx-doctor/SKILL.md +94 -0
- package/skills/tfx-workspace/skill-snapshot/tfx-gemini/SKILL.md +82 -0
- package/skills/tfx-workspace/skill-snapshot/tfx-hub/SKILL.md +133 -0
- package/skills/tfx-workspace/skill-snapshot/tfx-multi/SKILL.md +426 -0
- package/skills/tfx-workspace/skill-snapshot/tfx-setup/SKILL.md +101 -0
- package/.claude-plugin/marketplace.json +0 -34
- package/.claude-plugin/plugin.json +0 -22
- package/config/mcp-registry.json +0 -29
- package/tui/codex-profile.mjs +0 -457
- package/tui/core.mjs +0 -266
- package/tui/doctor.mjs +0 -375
- package/tui/gemini-profile.mjs +0 -299
- package/tui/monitor-data.mjs +0 -152
- package/tui/monitor.mjs +0 -339
- package/tui/setup.mjs +0 -533
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
|
+
| `sandbox = "elevated"` | `--full-auto` |
|
|
59
|
+
| `approval_mode = "full-auto"` | `--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>
|
package/hub/cli-adapter-base.mjs
CHANGED
|
@@ -152,78 +152,6 @@ export function appendWarnings(stderr, warnings = []) {
|
|
|
152
152
|
return [stderr, text].filter(Boolean).join("\n");
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
// ── Circuit breaker factory ─────────────────────────────────────
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* @deprecated Use per-account circuit breakers in account-broker.mjs instead.
|
|
159
|
-
* Kept for backward compatibility with code that hasn't migrated yet.
|
|
160
|
-
*/
|
|
161
|
-
export function createCircuitBreaker(opts = {}) {
|
|
162
|
-
const state = {
|
|
163
|
-
failures: [],
|
|
164
|
-
maxFailures: opts.maxFailures ?? 3,
|
|
165
|
-
windowMs: opts.windowMs ?? 10 * 60_000,
|
|
166
|
-
openedAt: 0,
|
|
167
|
-
trialInFlight: false,
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
function pruneFailures(now = Date.now()) {
|
|
171
|
-
state.failures = state.failures.filter(
|
|
172
|
-
(stamp) => now - stamp < state.windowMs,
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function reset() {
|
|
177
|
-
state.failures = [];
|
|
178
|
-
state.openedAt = 0;
|
|
179
|
-
state.trialInFlight = false;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function recordFailure(isHalfOpen, now = Date.now()) {
|
|
183
|
-
pruneFailures(now);
|
|
184
|
-
state.failures = [...state.failures, now];
|
|
185
|
-
state.trialInFlight = false;
|
|
186
|
-
if (isHalfOpen || state.failures.length >= state.maxFailures) {
|
|
187
|
-
state.openedAt = now;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function getState(now = Date.now()) {
|
|
192
|
-
pruneFailures(now);
|
|
193
|
-
const withinWindow =
|
|
194
|
-
state.openedAt && now - state.openedAt < state.windowMs;
|
|
195
|
-
const current = withinWindow
|
|
196
|
-
? "open"
|
|
197
|
-
: state.openedAt
|
|
198
|
-
? "half-open"
|
|
199
|
-
: "closed";
|
|
200
|
-
return {
|
|
201
|
-
state: current,
|
|
202
|
-
failures: [...state.failures],
|
|
203
|
-
maxFailures: state.maxFailures,
|
|
204
|
-
windowMs: state.windowMs,
|
|
205
|
-
openedAt: state.openedAt || null,
|
|
206
|
-
trialInFlight: state.trialInFlight,
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function canExecute() {
|
|
211
|
-
const circuit = getState();
|
|
212
|
-
if (circuit.state === "open") return { allowed: false, halfOpen: false };
|
|
213
|
-
if (circuit.state === "half-open" && state.trialInFlight)
|
|
214
|
-
return { allowed: false, halfOpen: true };
|
|
215
|
-
const halfOpen = circuit.state === "half-open";
|
|
216
|
-
if (halfOpen) state.trialInFlight = true;
|
|
217
|
-
return { allowed: true, halfOpen };
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function clearTrial() {
|
|
221
|
-
state.trialInFlight = false;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return { getState, recordFailure, reset, canExecute, clearTrial };
|
|
225
|
-
}
|
|
226
|
-
|
|
227
155
|
// ── Broker-integrated execution ─────────────────────────────────
|
|
228
156
|
|
|
229
157
|
/**
|
package/hub/lib/spawn-trace.mjs
CHANGED
|
@@ -2,18 +2,11 @@ import * as childProcess from "node:child_process";
|
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
3
|
import { createWriteStream, mkdirSync } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
|
-
import {
|
|
5
|
+
import { join } from "node:path";
|
|
6
6
|
|
|
7
7
|
const LOG_DIR = join(homedir(), ".triflux", "logs");
|
|
8
8
|
const DEDUPE_WINDOW_MS = 5_000;
|
|
9
9
|
const RATE_WINDOW_MS = 1_000;
|
|
10
|
-
const WINDOWS_TERMINAL_COMMANDS = new Set([
|
|
11
|
-
"wt",
|
|
12
|
-
"wt.exe",
|
|
13
|
-
"windowsterminal.exe",
|
|
14
|
-
]);
|
|
15
|
-
|
|
16
|
-
export const MAX_WT_TABS = 8;
|
|
17
10
|
export const MAX_SPAWN_PER_SEC = resolvePositiveInteger(
|
|
18
11
|
process.env.TRIFLUX_MAX_SPAWN_RATE,
|
|
19
12
|
10,
|
|
@@ -30,7 +23,6 @@ let traceSequence = 0;
|
|
|
30
23
|
const recentSpawnTimes = [];
|
|
31
24
|
const dedupeEntries = new Map();
|
|
32
25
|
const activeChildren = new Map();
|
|
33
|
-
const activeWtChildren = new Set();
|
|
34
26
|
|
|
35
27
|
function resolvePositiveInteger(...values) {
|
|
36
28
|
for (const value of values) {
|
|
@@ -149,14 +141,6 @@ function getCwd(options) {
|
|
|
149
141
|
return options.cwd || process.cwd();
|
|
150
142
|
}
|
|
151
143
|
|
|
152
|
-
function getCommandBasename(command) {
|
|
153
|
-
return basename(String(command || "")).toLowerCase();
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function isWindowsTerminalSpawn(command) {
|
|
157
|
-
return WINDOWS_TERMINAL_COMMANDS.has(getCommandBasename(command));
|
|
158
|
-
}
|
|
159
|
-
|
|
160
144
|
function createPolicyError(reasonCode, message, meta = {}) {
|
|
161
145
|
const error = new Error(message);
|
|
162
146
|
error.code = "TRIFLUX_SPAWN_BLOCKED";
|
|
@@ -212,14 +196,6 @@ function enforceGuards(command, args, options) {
|
|
|
212
196
|
);
|
|
213
197
|
}
|
|
214
198
|
|
|
215
|
-
if (isWindowsTerminalSpawn(command) && activeWtChildren.size >= MAX_WT_TABS) {
|
|
216
|
-
return createPolicyError(
|
|
217
|
-
"wt_tab_cap",
|
|
218
|
-
`spawn-trace Windows Terminal cap exceeded (${MAX_WT_TABS})`,
|
|
219
|
-
{ maxWtTabs: MAX_WT_TABS },
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
199
|
recentSpawnTimes.push(now);
|
|
224
200
|
if (dedupeKey) {
|
|
225
201
|
dedupeEntries.set(dedupeKey, now);
|
|
@@ -234,9 +210,6 @@ function trackChild(child, meta) {
|
|
|
234
210
|
}
|
|
235
211
|
|
|
236
212
|
activeChildren.set(child, meta);
|
|
237
|
-
if (meta.isWindowsTerminalSpawn) {
|
|
238
|
-
activeWtChildren.add(child);
|
|
239
|
-
}
|
|
240
213
|
|
|
241
214
|
let finalized = false;
|
|
242
215
|
const finalize = (event, payload = {}) => {
|
|
@@ -246,7 +219,6 @@ function trackChild(child, meta) {
|
|
|
246
219
|
|
|
247
220
|
finalized = true;
|
|
248
221
|
activeChildren.delete(child);
|
|
249
|
-
activeWtChildren.delete(child);
|
|
250
222
|
|
|
251
223
|
appendTrace({
|
|
252
224
|
event,
|
|
@@ -381,7 +353,6 @@ export function spawn(command, args, options) {
|
|
|
381
353
|
args: argsList,
|
|
382
354
|
cwd: getCwd(normalizedOptions),
|
|
383
355
|
reason: getReason(normalizedOptions),
|
|
384
|
-
isWindowsTerminalSpawn: isWindowsTerminalSpawn(command),
|
|
385
356
|
});
|
|
386
357
|
}
|
|
387
358
|
|
|
@@ -432,7 +403,6 @@ export function execFile(file, args, options, callback) {
|
|
|
432
403
|
args: normalized.argsList,
|
|
433
404
|
cwd: getCwd(normalized.options),
|
|
434
405
|
reason: getReason(normalized.options),
|
|
435
|
-
isWindowsTerminalSpawn: isWindowsTerminalSpawn(file),
|
|
436
406
|
});
|
|
437
407
|
}
|
|
438
408
|
|
|
@@ -521,7 +491,6 @@ export default {
|
|
|
521
491
|
spawn,
|
|
522
492
|
execFile,
|
|
523
493
|
execFileSync,
|
|
524
|
-
MAX_WT_TABS,
|
|
525
494
|
MAX_SPAWN_PER_SEC,
|
|
526
495
|
MAX_TOTAL_DESCENDANTS,
|
|
527
496
|
};
|
package/hub/server.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
ListToolsRequestSchema,
|
|
22
22
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
23
23
|
import { createModuleLogger } from "../scripts/lib/logger.mjs";
|
|
24
|
+
import { reloadBroker } from "./account-broker.mjs";
|
|
24
25
|
import { createAdaptiveEngine } from "./adaptive.mjs";
|
|
25
26
|
import { createAssignCallbackServer } from "./assign-callbacks.mjs";
|
|
26
27
|
import { DelegatorService } from "./delegator/index.mjs";
|
|
@@ -706,6 +707,17 @@ export async function startHub({
|
|
|
706
707
|
return writeJson(res, 200, getQosStatsPayload());
|
|
707
708
|
}
|
|
708
709
|
|
|
710
|
+
if (path === "/broker/reload" && req.method === "POST") {
|
|
711
|
+
const result = reloadBroker();
|
|
712
|
+
if (!result.ok) {
|
|
713
|
+
return writeJson(res, 200, { ok: false, error: result.error });
|
|
714
|
+
}
|
|
715
|
+
const accounts = result.broker
|
|
716
|
+
? [...result.broker.snapshot()].length
|
|
717
|
+
: 0;
|
|
718
|
+
return writeJson(res, 200, { ok: true, accounts });
|
|
719
|
+
}
|
|
720
|
+
|
|
709
721
|
if (path.startsWith("/bridge")) {
|
|
710
722
|
const isBridgeStatusGet =
|
|
711
723
|
path === "/bridge/status" && req.method === "GET";
|
package/hub/team/conductor.mjs
CHANGED
|
@@ -332,7 +332,11 @@ export function createConductor(opts = {}) {
|
|
|
332
332
|
`restart_${session.restarts + 1}/${maxRestarts}`,
|
|
333
333
|
);
|
|
334
334
|
session.restarts += 1;
|
|
335
|
-
|
|
335
|
+
const backoffMs = Math.min(1000 * 2 ** (session.restarts - 1), 30_000);
|
|
336
|
+
session._respawnTimer = setTimeout(() => {
|
|
337
|
+
session._respawnTimer = null;
|
|
338
|
+
void respawnSession(session);
|
|
339
|
+
}, backoffMs);
|
|
336
340
|
} else {
|
|
337
341
|
transition(session, STATES.DEAD, `maxRestarts(${maxRestarts})_exceeded`);
|
|
338
342
|
emitter.emit("dead", { sessionId: session.id, reason });
|
|
@@ -681,6 +685,10 @@ export function createConductor(opts = {}) {
|
|
|
681
685
|
if (!session) return;
|
|
682
686
|
if (TERMINAL_STATES.has(session.state)) return;
|
|
683
687
|
|
|
688
|
+
if (session._respawnTimer) {
|
|
689
|
+
clearTimeout(session._respawnTimer);
|
|
690
|
+
session._respawnTimer = null;
|
|
691
|
+
}
|
|
684
692
|
eventLog.append("kill", { session: id, reason });
|
|
685
693
|
await cleanupChild(session);
|
|
686
694
|
transition(session, STATES.FAILED, reason);
|
|
@@ -744,6 +752,10 @@ export function createConductor(opts = {}) {
|
|
|
744
752
|
const cleanups = [...sessions.values()]
|
|
745
753
|
.filter((s) => !TERMINAL_STATES.has(s.state))
|
|
746
754
|
.map(async (s) => {
|
|
755
|
+
if (s._respawnTimer) {
|
|
756
|
+
clearTimeout(s._respawnTimer);
|
|
757
|
+
s._respawnTimer = null;
|
|
758
|
+
}
|
|
747
759
|
s.probe?.stop();
|
|
748
760
|
await cleanupChild(s);
|
|
749
761
|
if (!TERMINAL_STATES.has(s.state)) {
|
|
@@ -118,7 +118,8 @@ function probeRemoteEnvViaPwsh(host) {
|
|
|
118
118
|
|
|
119
119
|
function probeRemoteEnvViaPosix(host) {
|
|
120
120
|
const script = [
|
|
121
|
-
"
|
|
121
|
+
'[ -f "$HOME/.zshenv" ] && . "$HOME/.zshenv" 2>/dev/null || true',
|
|
122
|
+
"export PATH=/opt/homebrew/bin:/opt/homebrew/sbin:$HOME/.local/bin:$HOME/.nvm/versions/node/$(ls $HOME/.nvm/versions/node 2>/dev/null | sort -V | tail -1)/bin:$PATH 2>/dev/null",
|
|
122
123
|
"echo shell=$(basename $SHELL)",
|
|
123
124
|
"echo home=$HOME",
|
|
124
125
|
"command -v claude >/dev/null 2>&1 && echo claude=$(command -v claude) || echo claude=notfound",
|