triflux 10.9.32 → 10.10.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 +159 -0
- package/hooks/keyword-rules.json +39 -0
- package/hub/cli-adapter-base.mjs +0 -1
- package/hub/team/conductor.mjs +15 -3
- package/hub/team/execution-mode.mjs +98 -0
- package/hub/team/swarm-cli.mjs +11 -5
- package/hub/team/swarm-hypervisor.mjs +57 -4
- package/hub/team/swarm-planner.mjs +12 -0
- package/hub/workers/codex-app-server-worker.mjs +0 -1
- package/package.json +21 -62
- package/references/cli-parameter-reference.md +240 -0
- package/references/codex-plugin-cc-analysis.md +706 -0
- package/references/codex-plugin-cc-code-patterns.md +468 -0
- package/references/hosts.json +46 -0
- package/scripts/__tests__/release-governance.test.mjs +56 -0
- package/scripts/release/lib.mjs +39 -4
- package/scripts/release/prepare.mjs +68 -7
- package/skills/tfx-analysis/SKILL.md +153 -60
- package/skills/tfx-auto/SKILL.md +82 -4
- package/skills/tfx-auto-codex/SKILL.md +25 -90
- package/skills/tfx-autopilot/SKILL.md +26 -97
- package/skills/tfx-autoresearch/SKILL.md +5 -117
- package/skills/tfx-autoroute/SKILL.md +23 -170
- package/skills/tfx-codex/SKILL.md +21 -66
- package/skills/tfx-deep-analysis/SKILL.md +12 -216
- package/skills/tfx-deep-interview/SKILL.md +7 -187
- package/skills/tfx-deep-plan/SKILL.md +6 -271
- package/skills/tfx-deep-qa/SKILL.md +8 -149
- package/skills/tfx-deep-research/SKILL.md +8 -199
- package/skills/tfx-deep-review/SKILL.md +16 -162
- package/skills/tfx-fullcycle/SKILL.md +23 -268
- package/skills/tfx-gemini/SKILL.md +21 -74
- package/skills/tfx-multi/SKILL.md +23 -167
- package/skills/tfx-persist/SKILL.md +25 -257
- package/skills/tfx-plan/SKILL.md +156 -42
- package/skills/tfx-qa/SKILL.md +118 -83
- package/skills/tfx-research/SKILL.md +154 -93
- package/skills/tfx-review/SKILL.md +181 -13
- package/skills/tfx-ship/SKILL.md +324 -0
- package/skills/tfx-swarm/SKILL.md +25 -210
- 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 -330
- package/tui/setup.mjs +0 -598
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# triflux — Claude Code 운영 가이드
|
|
2
|
+
|
|
3
|
+
<core-systems>
|
|
4
|
+
## 핵심 스킬 시스템 (항상 인지)
|
|
5
|
+
|
|
6
|
+
이 프로젝트는 3개의 스킬 시스템을 동시에 사용한다. 어떤 작업이든 해당 시스템의 스킬이 있는지 먼저 확인한다.
|
|
7
|
+
|
|
8
|
+
| 시스템 | 접두사 | 용도 | 스킬 수 |
|
|
9
|
+
|--------|--------|------|---------|
|
|
10
|
+
| **triflux** | `/tfx-*` | CLI 라우팅, 멀티모델 오케스트레이션, 스웜, 원격 실행 | ~40개 |
|
|
11
|
+
| **gstack** | `/` (접두사 없음) | QA, ship, investigate, design, review, checkpoint | ~35개 |
|
|
12
|
+
| **omc** | `/oh-my-claudecode:*` | autopilot, ralph, team, ultrawork, ccg | ~25개 |
|
|
13
|
+
|
|
14
|
+
스킬을 모르면 자연어 라우팅(`.claude/rules/tfx-routing.md`)으로 자동 매핑된다.
|
|
15
|
+
세션 종료 전 메모리 파일이 3개+ 변경됐으면 `/memory-hygiene` 제안을 검토한다.
|
|
16
|
+
</core-systems>
|
|
17
|
+
|
|
18
|
+
<psmux-wt>
|
|
19
|
+
## psmux/WT 규칙
|
|
20
|
+
|
|
21
|
+
psmux 세션·WT 패인을 생성/조작/정리할 때 `tfx-psmux-rules` 스킬을 참조한다.
|
|
22
|
+
WT 프리징 방지: exit → sleep 2 → kill 순서. 바로 kill하지 않는다.
|
|
23
|
+
|
|
24
|
+
### wt.exe → wt-manager 경유
|
|
25
|
+
|
|
26
|
+
safety-guard가 `wt.exe`, `wt new-tab`, `wt split-pane`, `Start-Process wt`를 차단한다.
|
|
27
|
+
`hub/team/wt-manager.mjs`의 API를 사용한다.
|
|
28
|
+
|
|
29
|
+
| 용도 | API |
|
|
30
|
+
|------|-----|
|
|
31
|
+
| 새 탭 | `createTab({ title, command, profile, cwd })` |
|
|
32
|
+
| 패인 분할 | `splitPane({ direction: 'H'\|'V', title, command })` |
|
|
33
|
+
| 다중 배치 | `applySplitLayout([{ title, command, direction }])` |
|
|
34
|
+
| 탭 정리 | `closeTab(title)` / `closeStale({ olderThanMs, titlePattern })` |
|
|
35
|
+
|
|
36
|
+
차단과 대안은 항상 쌍으로 존재해야 한다. 차단만 추가하고 대안을 안 만들면 데드락.
|
|
37
|
+
|
|
38
|
+
### raw `psmux kill-session` → psmux wrapper 경유
|
|
39
|
+
|
|
40
|
+
safety-guard가 raw `psmux kill-session`을 차단한다.
|
|
41
|
+
세션 정리는 `hub/team/psmux.mjs` 공개 API 또는 internal wrapper로 우회한다.
|
|
42
|
+
|
|
43
|
+
| 용도 | API / 래퍼 |
|
|
44
|
+
|------|------------|
|
|
45
|
+
| 세션 조회 | `listSessions({ filterTitle?, olderThanMs? })` |
|
|
46
|
+
| title prefix / regex kill | `killSessionByTitle(titlePattern)` |
|
|
47
|
+
| stale idle 세션 정리 | `pruneStale({ olderThanMs, dryRun })` |
|
|
48
|
+
| Bash 훅 우회용 래퍼 | `node hub/team/psmux.mjs --internal kill-by-title <prefix\|/regex/>` |
|
|
49
|
+
|
|
50
|
+
### psmux에서 Codex 실행
|
|
51
|
+
|
|
52
|
+
| 방식 | 동작 | 이유 |
|
|
53
|
+
|------|------|------|
|
|
54
|
+
| `codex` (interactive) | 불가 | psmux에서 TTY를 못 잡음 |
|
|
55
|
+
| `codex < prompt.md` | 불가 | "stdin is not a terminal" |
|
|
56
|
+
| `codex exec "$(cat prompt.md)" -s danger-full-access --dangerously-bypass-approvals-and-sandbox` | 사용 | 유일한 안전 경로 |
|
|
57
|
+
|
|
58
|
+
`codex exec`는 config.toml `approval_mode`를 무시하므로 `--dangerously-bypass-approvals-and-sandbox` 필수.
|
|
59
|
+
`-s` 유효값: read-only, workspace-write, danger-full-access.
|
|
60
|
+
</psmux-wt>
|
|
61
|
+
|
|
62
|
+
<codex-config>
|
|
63
|
+
## Codex config.toml
|
|
64
|
+
|
|
65
|
+
config.toml에 이미 설정된 값은 CLI 플래그로 중복 지정하지 않는다.
|
|
66
|
+
|
|
67
|
+
| config.toml에 있으면 | CLI에서 생략 |
|
|
68
|
+
|---------------------|-------------|
|
|
69
|
+
| `approval_mode = "auto"` | `-a`, `--full-auto` |
|
|
70
|
+
| `sandbox = "workspace-write"` | `-s`, `--full-auto` |
|
|
71
|
+
|
|
72
|
+
안전 패턴: config.toml에 기본값을 두고, CLI에서는 `--profile` 선택만 한다.
|
|
73
|
+
</codex-config>
|
|
74
|
+
|
|
75
|
+
<account-broker>
|
|
76
|
+
## AccountBroker (계정 브로커)
|
|
77
|
+
|
|
78
|
+
conductor, headless, swarm-hypervisor가 하나의 AccountBroker 싱글턴을 공유한다.
|
|
79
|
+
|
|
80
|
+
| 항목 | 설명 |
|
|
81
|
+
|------|------|
|
|
82
|
+
| 계정별 CircuitBreaker | 장애 격리 — 한 계정 오류가 다른 계정에 전파되지 않음 |
|
|
83
|
+
| busy 플래그 | 동일 계정 이중 임대(double-lease) 방지 |
|
|
84
|
+
| `/broker/reload` | 장시간 세션 중 accounts.json 핫리로드 |
|
|
85
|
+
| EventEmitter 이벤트 | `lease`, `release`, `cooldown`, `tierFallback`, `circuitOpen`, `circuitClose`, `noAvailableAccounts` — HUD 연동용 |
|
|
86
|
+
</account-broker>
|
|
87
|
+
|
|
88
|
+
<remote>
|
|
89
|
+
## 원격 실행
|
|
90
|
+
|
|
91
|
+
### 스킬 구분
|
|
92
|
+
|
|
93
|
+
| 스킬 | 대상 | 방식 |
|
|
94
|
+
|------|------|------|
|
|
95
|
+
| tfx-codex-swarm | 로컬 전용 | 로컬 worktree + psmux |
|
|
96
|
+
| tfx-remote-spawn | Claude Code 원격 | SSH → Claude Code 세션 → 내부 tfx 라우팅 |
|
|
97
|
+
|
|
98
|
+
codex를 SSH 너머로 직접 실행하지 않는다. config.toml 충돌 + TTY 문제.
|
|
99
|
+
원격에서 codex가 필요하면: remote-spawn → Claude Code → Claude가 내부에서 codex 호출.
|
|
100
|
+
|
|
101
|
+
### SSH 패턴
|
|
102
|
+
|
|
103
|
+
hosts.json `os` 필드로 대상 셸을 판단한다. safety-guard도 이 필드를 참조.
|
|
104
|
+
|
|
105
|
+
| 대상 OS | 셸 | 패턴 |
|
|
106
|
+
|---------|-----|------|
|
|
107
|
+
| windows | PowerShell | scp + `pwsh -File` 필수. `$var` → `$env:VAR`, `2>/dev/null` → `2>$null` |
|
|
108
|
+
| darwin | zsh | 인라인 가능. brew PATH 주의 (`/opt/homebrew/bin`) |
|
|
109
|
+
| linux | bash | 인라인 가능. 표준 POSIX |
|
|
110
|
+
|
|
111
|
+
- `~` → `$HOME` 변환은 모든 OS 공통
|
|
112
|
+
</remote>
|
|
113
|
+
|
|
114
|
+
<headless-retrieval>
|
|
115
|
+
## Headless 결과 회수
|
|
116
|
+
|
|
117
|
+
background로 실행한 headless 결과는 **반드시 task-notification 완료 후** 읽는다.
|
|
118
|
+
|
|
119
|
+
| 패턴 | 올바름 | 이유 |
|
|
120
|
+
|------|--------|------|
|
|
121
|
+
| task-notification 후 output 파일 읽기 | YES | 프로세스 종료 = 워커 전부 완료 |
|
|
122
|
+
| task-notification 전 output 파일 tail | NO | 시작 메시지만 보이고 "실패"로 오진 |
|
|
123
|
+
| psmux capture-pane으로 중간 체크 | NO | 워커 진행 중이면 빈 화면일 수 있음 |
|
|
124
|
+
|
|
125
|
+
완료 마커: `=== HEADLESS_COMPLETE succeeded=N failed=N total=N ===`
|
|
126
|
+
워커 상세: `$TMPDIR/tfx-headless/{sessionName}-worker-N.txt`
|
|
127
|
+
</headless-retrieval>
|
|
128
|
+
|
|
129
|
+
<cross-review>
|
|
130
|
+
## 교차 검증
|
|
131
|
+
|
|
132
|
+
- Claude 작성 코드 → Codex 리뷰
|
|
133
|
+
- Codex 작성 코드 → Claude 리뷰
|
|
134
|
+
- 동일 모델 self-approve 하지 않는다
|
|
135
|
+
- git commit 전 미검증 파일 감지 시 nudge
|
|
136
|
+
</cross-review>
|
|
137
|
+
|
|
138
|
+
<session-context>
|
|
139
|
+
## 맥락 이탈 판단
|
|
140
|
+
|
|
141
|
+
현재 세션 맥락과 무관한 요청이 감지되면 psmux 격리를 제안한다.
|
|
142
|
+
|
|
143
|
+
| 확신도 | 신호 | 행동 |
|
|
144
|
+
|--------|------|------|
|
|
145
|
+
| 확실 | "새 탭", "별도로", "새 세션" | 바로 psmux spawn |
|
|
146
|
+
| 높음 | 다른 프로젝트/스택 언급 | 분리 제안 |
|
|
147
|
+
| 중간 | 작업 유형 전환 | 분리 제안 + 현재 세션 옵션 |
|
|
148
|
+
| 낮음 | 현재 작업 연장 | 세션 유지 |
|
|
149
|
+
</session-context>
|
|
150
|
+
|
|
151
|
+
## 세부 규칙은 `.claude/rules/` 참조
|
|
152
|
+
|
|
153
|
+
| 파일 | 내용 |
|
|
154
|
+
|------|------|
|
|
155
|
+
| `.claude/rules/tfx-routing.md` | 자연어 → 스킬 라우팅, CLI 라우팅 Layer 1~3, 충돌 해소 |
|
|
156
|
+
| `.claude/rules/tfx-execution-skill-map.md` | tfx-auto / multi / swarm 실행 엔진 매핑, 격리 기준, 안티패턴 |
|
|
157
|
+
| `.claude/rules/tfx-update-logic.md` | triflux / OMC / gstack / Codex / Gemini 업데이트 로직 |
|
|
158
|
+
|
|
159
|
+
Claude Code는 `.claude/rules/*.md` 를 자동 로드한다. Codex CLI는 `@import` 미지원이므로 필요 시 `AGENTS.md` 를 독립 유지한다.
|
package/hooks/keyword-rules.json
CHANGED
|
@@ -664,6 +664,45 @@
|
|
|
664
664
|
"exclusive": false,
|
|
665
665
|
"state": null,
|
|
666
666
|
"mcp_route": null
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
"id": "tfx-ship",
|
|
670
|
+
"patterns": [
|
|
671
|
+
{
|
|
672
|
+
"source": "\\btfx[\\s-]?ship\\b",
|
|
673
|
+
"flags": "i"
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
"source": "/ship\\b",
|
|
677
|
+
"flags": "i"
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
"source": "\\brelease\\b",
|
|
681
|
+
"flags": "i"
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
"source": "\\bpublish\\b",
|
|
685
|
+
"flags": "i"
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
"source": "배포(?!자|사|장|처)",
|
|
689
|
+
"flags": ""
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
"source": "릴리[즈스]",
|
|
693
|
+
"flags": ""
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
"source": "쉽\\s*(?:해|하자|가자|쳐)",
|
|
697
|
+
"flags": ""
|
|
698
|
+
}
|
|
699
|
+
],
|
|
700
|
+
"skill": "tfx-ship",
|
|
701
|
+
"priority": 3,
|
|
702
|
+
"supersedes": ["gstack-ship"],
|
|
703
|
+
"exclusive": false,
|
|
704
|
+
"state": null,
|
|
705
|
+
"mcp_route": null
|
|
667
706
|
}
|
|
668
707
|
]
|
|
669
708
|
}
|
package/hub/cli-adapter-base.mjs
CHANGED
package/hub/team/conductor.mjs
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
getConductorRegistry,
|
|
29
29
|
} from "./conductor-registry.mjs";
|
|
30
30
|
import { createEventLog } from "./event-log.mjs";
|
|
31
|
+
import { buildSpawnSpecForMode, MODES } from "./execution-mode.mjs";
|
|
31
32
|
import { createHealthProbe } from "./health-probe.mjs";
|
|
32
33
|
import { buildLauncher } from "./launcher-template.mjs";
|
|
33
34
|
import {
|
|
@@ -490,6 +491,14 @@ export function createConductor(opts = {}) {
|
|
|
490
491
|
let recentOutput = "";
|
|
491
492
|
|
|
492
493
|
const spawnCwd = session.config.workdir || launcher.cwd || undefined;
|
|
494
|
+
const spawnSpec = buildSpawnSpecForMode(MODES.HEADLESS, {
|
|
495
|
+
cli: session.config.agent,
|
|
496
|
+
prompt: session.config.prompt,
|
|
497
|
+
profile: session.config.profile,
|
|
498
|
+
model: session.config.model,
|
|
499
|
+
mcpServers: session.config.mcpServers,
|
|
500
|
+
resolveCommand: opts.deps?.resolveCliExecutable,
|
|
501
|
+
});
|
|
493
502
|
|
|
494
503
|
// #90 branch guard: shard spawn cwd가 main 브랜치면 즉시 abort.
|
|
495
504
|
// swarm shard는 반드시 shard 전용 worktree 브랜치에서 실행되어야 한다.
|
|
@@ -524,8 +533,8 @@ export function createConductor(opts = {}) {
|
|
|
524
533
|
|
|
525
534
|
let child;
|
|
526
535
|
try {
|
|
527
|
-
child = spawnFn(
|
|
528
|
-
shell:
|
|
536
|
+
child = spawnFn(spawnSpec.command, spawnSpec.args, {
|
|
537
|
+
shell: false,
|
|
529
538
|
cwd: spawnCwd,
|
|
530
539
|
env: {
|
|
531
540
|
...process.env,
|
|
@@ -544,6 +553,7 @@ export function createConductor(opts = {}) {
|
|
|
544
553
|
session: session.id,
|
|
545
554
|
error: err.message,
|
|
546
555
|
cwd: spawnCwd || null,
|
|
556
|
+
command: spawnSpec.command,
|
|
547
557
|
});
|
|
548
558
|
handleFailure(session, `spawn_error:${err.message}`);
|
|
549
559
|
return;
|
|
@@ -561,7 +571,9 @@ export function createConductor(opts = {}) {
|
|
|
561
571
|
session: session.id,
|
|
562
572
|
agent: session.config.agent,
|
|
563
573
|
pid: child.pid,
|
|
564
|
-
command:
|
|
574
|
+
command: spawnSpec.command,
|
|
575
|
+
args: spawnSpec.args,
|
|
576
|
+
legacyCommand: launcher.command,
|
|
565
577
|
restart: session.restarts,
|
|
566
578
|
});
|
|
567
579
|
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
// hub/team/execution-mode.mjs — headless vs interactive execution mode selection
|
|
2
2
|
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
import { whichCommand } from "../platform.mjs";
|
|
6
|
+
|
|
7
|
+
const WIN32_EXT_PRECEDENCE = [".cmd", ".exe", ".bat", ".ps1"];
|
|
8
|
+
|
|
3
9
|
export const MODES = Object.freeze({
|
|
4
10
|
HEADLESS: "headless",
|
|
5
11
|
INTERACTIVE: "interactive",
|
|
@@ -10,6 +16,98 @@ function quotePrompt(prompt) {
|
|
|
10
16
|
return JSON.stringify(typeof prompt === "string" ? prompt : "");
|
|
11
17
|
}
|
|
12
18
|
|
|
19
|
+
function asPrompt(prompt) {
|
|
20
|
+
return typeof prompt === "string" ? prompt : "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function pushFlag(args, flag, value) {
|
|
24
|
+
if (typeof value === "string" && value.length > 0) {
|
|
25
|
+
args.push(flag, value);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function resolveCliExecutable(cli, opts = {}) {
|
|
30
|
+
const name = String(cli || "codex");
|
|
31
|
+
const resolveCommand = opts.resolveCommand || whichCommand;
|
|
32
|
+
const resolved = resolveCommand(name) || name;
|
|
33
|
+
|
|
34
|
+
// Windows: Node spawn({ shell: false }) calls CreateProcess directly and does NOT
|
|
35
|
+
// search PATHEXT. npm-installed CLIs (codex, gemini) live at `<npm>/codex` (Git Bash
|
|
36
|
+
// shell script) alongside `<npm>/codex.cmd` (Windows batch wrapper). whichCommand
|
|
37
|
+
// returns the extensionless path, which Windows cannot execute → ENOENT "The system
|
|
38
|
+
// cannot find the file specified." Append the correct extension when the resolved
|
|
39
|
+
// path has none.
|
|
40
|
+
const platform = opts.platform || process.platform;
|
|
41
|
+
if (platform === "win32" && resolved) {
|
|
42
|
+
const hasExt = /\.[^\\/.]+$/.test(resolved);
|
|
43
|
+
if (!hasExt) {
|
|
44
|
+
const existsFn = opts.existsSyncFn || existsSync;
|
|
45
|
+
for (const ext of WIN32_EXT_PRECEDENCE) {
|
|
46
|
+
const candidate = `${resolved}${ext}`;
|
|
47
|
+
try {
|
|
48
|
+
if (existsFn(candidate)) return candidate;
|
|
49
|
+
} catch {
|
|
50
|
+
// ignore stat failures, try next extension
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return resolved;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function buildSpawnSpecForMode(mode, opts = {}) {
|
|
60
|
+
const cli = opts.cli || "codex";
|
|
61
|
+
const prompt = asPrompt(opts.prompt);
|
|
62
|
+
const resolvedCommand = resolveCliExecutable(cli, opts);
|
|
63
|
+
const platform = opts.platform || process.platform;
|
|
64
|
+
|
|
65
|
+
// Node v20.12+ (CVE-2024-27980) rejects spawn of .cmd/.bat files with shell:false
|
|
66
|
+
// (EINVAL). npm-installed Windows wrappers (e.g. codex.cmd) hit this. Wrap via
|
|
67
|
+
// cmd.exe /c to keep shell:false while still launching the batch wrapper.
|
|
68
|
+
const needsCmdWrap =
|
|
69
|
+
platform === "win32" && /\.(cmd|bat)$/i.test(resolvedCommand);
|
|
70
|
+
const wrap = (args) =>
|
|
71
|
+
needsCmdWrap
|
|
72
|
+
? { command: "cmd", args: ["/c", resolvedCommand, ...args] }
|
|
73
|
+
: { command: resolvedCommand, args };
|
|
74
|
+
|
|
75
|
+
if (cli === "gemini") {
|
|
76
|
+
const args = [];
|
|
77
|
+
pushFlag(args, "--model", opts.model);
|
|
78
|
+
args.push("--yolo", "--prompt", prompt, "--output-format", "text");
|
|
79
|
+
return { ...wrap(args), useExec: true, shell: false };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (mode === MODES.INTERACTIVE || mode === MODES.AUTO) {
|
|
83
|
+
return { ...wrap([]), useExec: false, shell: false };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (cli === "claude") {
|
|
87
|
+
const args = [];
|
|
88
|
+
pushFlag(args, "--model", opts.model);
|
|
89
|
+
args.push("-p", prompt);
|
|
90
|
+
return { ...wrap(args), useExec: true, shell: false };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const args = [];
|
|
94
|
+
pushFlag(args, "--profile", opts.profile);
|
|
95
|
+
args.push(
|
|
96
|
+
"exec",
|
|
97
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
98
|
+
"--skip-git-repo-check",
|
|
99
|
+
"--color",
|
|
100
|
+
"never",
|
|
101
|
+
);
|
|
102
|
+
if (Array.isArray(opts.mcpServers)) {
|
|
103
|
+
for (const server of opts.mcpServers) {
|
|
104
|
+
args.push("-c", `mcp_servers.${server}.enabled=true`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
args.push(prompt);
|
|
108
|
+
return { ...wrap(args), useExec: true, shell: false };
|
|
109
|
+
}
|
|
110
|
+
|
|
13
111
|
/**
|
|
14
112
|
* @param {{
|
|
15
113
|
* cli: "codex"|"gemini"|"claude",
|
package/hub/team/swarm-cli.mjs
CHANGED
|
@@ -131,14 +131,20 @@ export async function cmdSwarmRun(args, { json = false } = {}) {
|
|
|
131
131
|
baseBranch: flags.baseBranch,
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
-
hyper.on("shardLaunched", ({
|
|
134
|
+
hyper.on("shardLaunched", ({ shardName, sessionId, remote }) => {
|
|
135
135
|
const tag = remote ? ` ${GRAY}(remote)${RESET}` : "";
|
|
136
|
-
console.log(
|
|
136
|
+
console.log(
|
|
137
|
+
` ${GREEN}▸${RESET} launched: ${shardName}${tag} [${sessionId}]`,
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
hyper.on("shardCompleted", ({ shardName, sessionId, isRedundant }) => {
|
|
141
|
+
const tag = isRedundant ? ` ${GRAY}(redundant)${RESET}` : "";
|
|
142
|
+
console.log(` ${GREEN}✓${RESET} ${shardName}${tag} [${sessionId}]`);
|
|
137
143
|
});
|
|
138
|
-
hyper.on("
|
|
139
|
-
const mark = success ? `${GREEN}✓${RESET}` : `${RED}✗${RESET}`;
|
|
144
|
+
hyper.on("shardFailed", ({ shardName, failureMode, reason }) => {
|
|
140
145
|
const reasonStr = reason ? ` ${GRAY}(${reason})${RESET}` : "";
|
|
141
|
-
|
|
146
|
+
const modeStr = failureMode ? ` ${GRAY}[${failureMode}]${RESET}` : "";
|
|
147
|
+
console.log(` ${RED}✗${RESET} ${shardName}${modeStr}${reasonStr}`);
|
|
142
148
|
});
|
|
143
149
|
hyper.on("warning", ({ type, ...rest }) => {
|
|
144
150
|
console.error(
|
|
@@ -535,6 +535,15 @@ export function createSwarmHypervisor(opts) {
|
|
|
535
535
|
branchName: sessionConfig.branchName,
|
|
536
536
|
});
|
|
537
537
|
|
|
538
|
+
emitter.emit("shardLaunched", {
|
|
539
|
+
shardName: shard.name,
|
|
540
|
+
sessionId: sessionConfig.id,
|
|
541
|
+
agent: shard.agent,
|
|
542
|
+
isRedundant,
|
|
543
|
+
remote: Boolean(shard.host),
|
|
544
|
+
host: shard.host || null,
|
|
545
|
+
});
|
|
546
|
+
|
|
538
547
|
const entry = {
|
|
539
548
|
conductor,
|
|
540
549
|
shardConfig: shard,
|
|
@@ -661,9 +670,47 @@ export function createSwarmHypervisor(opts) {
|
|
|
661
670
|
lockManager.release(shardName);
|
|
662
671
|
|
|
663
672
|
emitter.emit("shardFailed", { shardName, failureMode, reason });
|
|
673
|
+
|
|
674
|
+
// Cascade failure to dependents so they do not hold integration hostage
|
|
675
|
+
// (BFS over plan.shards). Without this, a dead shard keeps dependents in
|
|
676
|
+
// `pending` forever and integrateResults is never reachable.
|
|
677
|
+
cascadeDependencyFailure(shardName);
|
|
678
|
+
|
|
664
679
|
checkAllShardsCompleted();
|
|
665
680
|
}
|
|
666
681
|
|
|
682
|
+
function cascadeDependencyFailure(rootFailedShard) {
|
|
683
|
+
const queue = [rootFailedShard];
|
|
684
|
+
const visited = new Set([rootFailedShard]);
|
|
685
|
+
while (queue.length > 0) {
|
|
686
|
+
const failedName = queue.shift();
|
|
687
|
+
for (const candidate of plan.shards) {
|
|
688
|
+
if (visited.has(candidate.name)) continue;
|
|
689
|
+
if (failures.has(candidate.name)) continue;
|
|
690
|
+
if (completedShards.has(candidate.name)) continue;
|
|
691
|
+
if (!candidate.depends?.includes(failedName)) continue;
|
|
692
|
+
|
|
693
|
+
visited.add(candidate.name);
|
|
694
|
+
failures.set(candidate.name, {
|
|
695
|
+
mode: FAILURE_MODES.F1_CRASH,
|
|
696
|
+
reason: `dep_failed:${failedName}`,
|
|
697
|
+
sessionId: null,
|
|
698
|
+
});
|
|
699
|
+
lockManager.release(candidate.name);
|
|
700
|
+
eventLog.append("shard_blocked", {
|
|
701
|
+
shard: candidate.name,
|
|
702
|
+
failedDep: failedName,
|
|
703
|
+
});
|
|
704
|
+
emitter.emit("shardFailed", {
|
|
705
|
+
shardName: candidate.name,
|
|
706
|
+
failureMode: FAILURE_MODES.F1_CRASH,
|
|
707
|
+
reason: `dep_failed:${failedName}`,
|
|
708
|
+
});
|
|
709
|
+
queue.push(candidate.name);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
667
714
|
function classifyFailure(reason) {
|
|
668
715
|
if (!reason) return FAILURE_MODES.F1_CRASH;
|
|
669
716
|
const r = String(reason).toLowerCase();
|
|
@@ -687,10 +734,16 @@ export function createSwarmHypervisor(opts) {
|
|
|
687
734
|
function checkAllShardsCompleted() {
|
|
688
735
|
if (state !== SWARM_STATES.RUNNING) return;
|
|
689
736
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
737
|
+
// `completedShards` + `failures` are set synchronously inside
|
|
738
|
+
// handleShardCompleted / handleShardFailed, so they are the authoritative
|
|
739
|
+
// terminal signal. `isTerminal(w)` queries conductor.getSnapshot() which
|
|
740
|
+
// lags because the snapshot state transition is applied asynchronously
|
|
741
|
+
// after the onCompleted callback chain that invoked this check. Using the
|
|
742
|
+
// snapshot here races with its own producer and leaves integrateResults
|
|
743
|
+
// unreachable (2026-04-17 swarm hang — required manual cherry-pick).
|
|
744
|
+
const allDone = plan.mergeOrder.every(
|
|
745
|
+
(name) => completedShards.has(name) || failures.has(name),
|
|
746
|
+
);
|
|
694
747
|
|
|
695
748
|
if (allDone) {
|
|
696
749
|
void integrateResults();
|
|
@@ -256,6 +256,18 @@ export function planSwarm(prdPath, opts = {}) {
|
|
|
256
256
|
);
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
+
const emptyPromptShards = shards.filter(
|
|
260
|
+
(s) => !s.prompt || s.prompt.trim() === "",
|
|
261
|
+
);
|
|
262
|
+
if (emptyPromptShards.length > 0) {
|
|
263
|
+
const names = emptyPromptShards.map((s) => s.name).join(", ");
|
|
264
|
+
throw new Error(
|
|
265
|
+
`Shard(s) with empty prompt: ${names}. ` +
|
|
266
|
+
`Each shard must include a non-empty "- prompt: |" block. ` +
|
|
267
|
+
`See docs/prd/_template.md for PRD format.`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
259
271
|
const { leaseMap, conflicts } = buildFileLeaseMap(shards);
|
|
260
272
|
const mcpManifest = buildMcpManifest(shards);
|
|
261
273
|
const { order: mergeOrder, cycles } = computeMergeOrder(shards);
|
|
@@ -361,7 +361,6 @@ export class CodexAppServerWorker {
|
|
|
361
361
|
typeof options.warn === "function"
|
|
362
362
|
? options.warn
|
|
363
363
|
: (label, payload) => {
|
|
364
|
-
// biome-ignore lint/suspicious/noConsole: defensive warn-once path
|
|
365
364
|
console.warn(`[codex-app-server] ${label}`, payload);
|
|
366
365
|
};
|
|
367
366
|
this._onUnknownMethod =
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "triflux",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.10.0",
|
|
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
|
-
"
|
|
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.29.0",
|
|
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
|
}
|