triflux 10.57.0 → 10.57.2
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/rules/tfx-routing.md +178 -0
- package/adapters/codex/skills/tfx-harness/SKILL.md +9 -7
- package/bin/triflux.mjs +6 -5
- package/hud/constants.mjs +2 -0
- package/hud/hud-qos-status.mjs +37 -13
- package/hud/hud-statusline.sh +15 -0
- package/hud/providers/claude.mjs +33 -11
- package/hud/providers/codex.mjs +7 -6
- package/hud/providers/gemini.mjs +8 -4
- package/hud/renderers.mjs +15 -4
- package/hud/utils.mjs +48 -18
- package/package.json +2 -1
- package/scripts/release/check-packages-mirror.mjs +44 -35
- package/scripts/setup.mjs +76 -21
- package/skills/tfx-harness/SKILL.md +8 -5
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# 자연어 → 스킬 라우팅
|
|
2
|
+
|
|
3
|
+
사용자가 스킬명을 모르더라도 자연어로 요청하면 아래 규칙에 따라 적절한 스킬을 호출한다.
|
|
4
|
+
|
|
5
|
+
이 문서는 **무엇을 어떤 순서로 밟는가**(워크플로 단계 + 자연어 트리거)를 정한다.
|
|
6
|
+
**어느 시스템이 그 기능을 소유하는가**(gstack / superpowers / triflux)는
|
|
7
|
+
`tfx-stack-coexistence.md` 의 책임 매트릭스가 SSOT다. 두 문서가 어긋나 보이면 층위를
|
|
8
|
+
먼저 구분한다. 실행 계획은 superpowers `writing-plans`, TFX 다중모델 계획·실행은
|
|
9
|
+
`tfx-auto --mode deep`이 맡는다. 같은 층위에서 실제로 갈릴 때만 coexistence를 따른다.
|
|
10
|
+
|
|
11
|
+
## 기본 워크플로우 ladder
|
|
12
|
+
|
|
13
|
+
넓은 운영 순서와 스택 간 책임 경계는 이 파일에 둔다. 개별 `SKILL.md`
|
|
14
|
+
description에는 해당 스킬을 고르는 데 필요한 좁은 activation phrase만 넣는다.
|
|
15
|
+
|
|
16
|
+
| 상황 | 기본 순서 | 비고 |
|
|
17
|
+
|------|-----------|------|
|
|
18
|
+
| 메타 라우팅/스킬 선택 | `/tfx-harness` | 어떤 스킬/순서가 맞는지 먼저 판정해야 할 때 쓰는 단일 진입점. |
|
|
19
|
+
| 신규 제품/아이디어 | `/gstack-office-hours` → `/gstack-autoplan` → `/superpowers:writing-plans` → `/tfx-auto` | 먼저 문제/범위를 잡고, 리뷰된 계획을 실행으로 넘긴다. |
|
|
20
|
+
| 기존 작업 계속 | `/gstack-context-restore` → `/tfx-auto` | 저장된 맥락을 복원한 뒤 실행한다. |
|
|
21
|
+
| 직접 구현/수정 | `/tfx-auto` (Codex 우선) | "이거 고쳐줘", "구현해줘". 기존 작업 연장이면 `/gstack-context-restore` 먼저. |
|
|
22
|
+
| 디버그 | Claude 기본 Explore 에이전트 → `/superpowers:systematic-debugging` → `/tfx-auto` | 코드 위치/사용처 확인과 원인 분석을 분리한다. |
|
|
23
|
+
| 리서치 | `/tfx-research` 또는 `/multilingual-parallel-research` | 외부/최신/도구 가능 여부 의문문은 `/tfx-research`; 언어권 병렬 조사는 multilingual 경로. |
|
|
24
|
+
| 검증 | `/superpowers:requesting-code-review` + `/gstack-qa` + `/superpowers:verification-before-completion` | 코드 판정은 review, 브라우저/워크플로우 게이트는 qa, 완료 주장 전 evidence 수집은 verification. |
|
|
25
|
+
| 디자인 검토 | `/gstack-design-review` | 시각/UX 판정. 코드 판정과 분리. |
|
|
26
|
+
| TDD 시작 | `/superpowers:test-driven-development` | 테스트부터 짜는 흐름. 구현 전에 invoke. |
|
|
27
|
+
| 코드 변경 병렬 실행 | 작업별 worktree와 세션을 분리하고 각 세션에서 `/tfx-auto` 실행 | Claude Agent는 `isolation: worktree` 사용 가능. |
|
|
28
|
+
| 병렬 작업 (read-only) | `/tfx-auto --parallel N --mode deep` 또는 `/superpowers:dispatching-parallel-agents` | cwd 공유가 가능한 read-only 병렬. 코드 변경은 작업별 worktree를 분리. |
|
|
29
|
+
| plan 실행 (체크포인트 별도) | `/superpowers:executing-plans` | 다른 세션에서 plan 을 단계별 실행. |
|
|
30
|
+
| 머지 직전 정리 | `/superpowers:finishing-a-development-branch` → `/tfx-ship` 또는 `/gstack-ship` | merge/PR/cleanup 결정 후 ship. |
|
|
31
|
+
| 리뷰 응답 | `/superpowers:receiving-code-review` | 검토 의견에 기술적 rigor 로 응답. blind apply 금지. |
|
|
32
|
+
| triflux 릴리즈 | `/tfx-ship` | triflux 본체는 AI trailer 금지 정책이 있는 전용 ship 경로를 쓴다. |
|
|
33
|
+
| 일반 PR/배포 | `/gstack-ship` | 로컬 `/gstack-ship`도 AI attribution footer/trailer 없이 실행되어야 한다. |
|
|
34
|
+
| 저장/복원 | `/gstack-context-save` / `/gstack-context-restore` | 진행 상태는 gstack checkpoint 계열이 소유한다. |
|
|
35
|
+
| 회고/슬롭 정리 | `/gstack-retro` 또는 명시 `tfx-auto --mode consensus` | 세션 회고는 gstack, 무수식 AI slop/deslop은 host ai-slop-cleaner, TFX 3자 cleanup만 `tfx-auto --mode consensus`. |
|
|
36
|
+
|
|
37
|
+
## 스킬 선택 결정트리 (SSOT)
|
|
38
|
+
|
|
39
|
+
우선순위는 **명시 스킬/플래그 > D0-D11 결정트리 > 위 quick reference > 개별 description**이다. 위 표는 파생 참조이며 충돌하면 이 결정트리가 이긴다.
|
|
40
|
+
|
|
41
|
+
- D0: 명시 `/skill`, `$skill`, `tfx-*`, `--mode/--cli`는 안전 위반이 없으면 유지한다.
|
|
42
|
+
- D1: “어떤 스킬/경로?”만 묻는 요청은 `tfx-harness`가 recommendation-only로 owner 하나를 반환한다.
|
|
43
|
+
- D2: 목표·non-goal·acceptance criteria가 불명확하면 host `deep-interview`; 자연어 목표의 `/goal` 변환은 Claude Code 기본 `/goal`을 쓴다.
|
|
44
|
+
- D3: 제품 수요·wedge 단계는 `office-hours`; D4 해법/UX 발산은 `brainstorming`.
|
|
45
|
+
- D5: 승인된 spec의 실행 계획은 superpowers `writing-plans`; 명시 TFX 다중모델 계획·실행은 `tfx-auto --mode deep`.
|
|
46
|
+
- D6: 실패/버그/test failure는 `systematic-debugging` 후 `tfx-auto`; 정상 구조 설명은 `tfx-auto --mode consensus --shape panel`.
|
|
47
|
+
- D7: 로컬 파일·심볼은 Claude 기본 Explore 에이전트(스킬 없음); 외부·최신·공식 문서는 `tfx-research`.
|
|
48
|
+
- D8: 무수식 AI slop/deslop/refactor는 host `ai-slop-cleaner`; 명시 TFX 3자 cleanup만 `tfx-auto --mode consensus`; 회고는 `retro`. “정리” 한 단어로 cleanup을 강제하지 않는다.
|
|
49
|
+
- D9: web/app flow는 `qa`; code verdict는 review backend; 완료 주장은 `verification-before-completion`이 필수다.
|
|
50
|
+
- D10: 명확한 직접 구현은 TDD 필요 여부 뒤 `tfx-auto`; 2+ code-changing lane은 worktree와 세션을 각각 분리.
|
|
51
|
+
- D11: fresh verification 없이 ship 금지. triflux release는 `tfx-ship`, 일반 release는 `ship`, 중단/재개는 context save/restore.
|
|
52
|
+
|
|
53
|
+
owner availability를 실제로 검출한 경우에만 `owner unavailable → tfx-X fallback`을 명시해 fallback할 수 있다. availability가 unknown이면 추측 fallback하지 않는다.
|
|
54
|
+
|
|
55
|
+
## 행동 유형 → 스킬 매핑
|
|
56
|
+
|
|
57
|
+
| 의도 | 자연어 신호 | 스킬 |
|
|
58
|
+
|------|-----------|------|
|
|
59
|
+
| 리드 운영 | 리드 맡아, 세션들 지휘, 워커 띄워서 조율, 승계·머지·릴리스 조율 | tfx-lead |
|
|
60
|
+
| 구현/수정 | 만들어, 고쳐, 구현해, 짜줘, 수정해, 바꿔 | tfx-auto |
|
|
61
|
+
| 리뷰 | 봐줘, 리뷰해, 검토해, 괜찮아? | `superpowers:requesting-code-review` (owner). 교차모델 판정이 필요할 때만 tfx-review |
|
|
62
|
+
| 분석 | 분석해, 어떻게 돌아가?, 구조가 뭐야 | tfx-auto (`--mode consensus --shape panel`) |
|
|
63
|
+
| 계획 | 계획, 어떻게 하지, 설계해 | superpowers `writing-plans`; TFX 다중모델 계획·실행은 `tfx-auto --mode deep` |
|
|
64
|
+
| 요구사항 명확화 | 요구사항, 인터뷰, 목표 구체화 | host `deep-interview`; 목표 변환은 Claude Code 기본 `/goal` |
|
|
65
|
+
| Codex 프로필 관리 | 프로필, 모델, effort 변경 | `~/.codex/<프로필>.config.toml` 직접 관리 |
|
|
66
|
+
| 검색 | 찾아, 어디있어, 파일 찾아 | Claude 기본 Explore 에이전트 |
|
|
67
|
+
| 리서치 (빠른) | 검색해줘, 찾아봐, 공식문서, 이거 뭐야 | tfx-research |
|
|
68
|
+
| 리서치 (자율) | 자율 리서치, 검색하고 정리해, research and plan | tfx-research (Auto 모드) |
|
|
69
|
+
| 테스트 | 테스트, 검증, 돌려봐, QA | 코드 판정은 tfx-review, 테스트→수정 반복은 tfx-auto, 브라우저 흐름은 gstack `/gstack-qa` |
|
|
70
|
+
| 정리 | 대상 domain을 먼저 판정; 무수식 AI slop/deslop은 host ai-slop-cleaner | D8 참조 (명시 TFX 3자 cleanup만 `tfx-auto --mode consensus`) |
|
|
71
|
+
| 토론/비교 | 뭐가 나을까, 비교해, A vs B | tfx-auto (`--mode consensus --shape debate`) |
|
|
72
|
+
| 합의 | 합의로 분석해, 3자 합의, consensus | tfx-auto (`--mode consensus`) |
|
|
73
|
+
| 패널 | panel, 패널, 전문가 의견, expert panel | tfx-auto (`--mode consensus --shape panel`) |
|
|
74
|
+
|
|
75
|
+
위 표는 Claude가 요청을 읽고 스킬을 고를 때 쓰는 지도다. 자동 키워드 주입은 [ADR-0023](../../docs/adr/0023-remove-command-hooks-hub-and-adopt-mods.md)에 따라 제거한다. 명시적 스킬 호출은 해당 스킬의 설치 여부와 호출 문법을 확인한다.
|
|
76
|
+
|
|
77
|
+
## 깊이 수정자
|
|
78
|
+
|
|
79
|
+
| 수정자 | 신호 | 효과 |
|
|
80
|
+
|--------|------|------|
|
|
81
|
+
| 기본 | (없음), 빠르게, 간단히 | Light 스킬 |
|
|
82
|
+
| 깊이 | 제대로, 꼼꼼히, 철저히 | `tfx-auto --mode deep` |
|
|
83
|
+
| 합의 | 3자, 교차, 다각도 | `tfx-auto --mode consensus` |
|
|
84
|
+
| 반복 | 끝까지, 멈추지마, ralph | `--retry ralph` (Phase 3 true state machine, `.claude/rules/tfx-escalation-chain.md` 참조) |
|
|
85
|
+
| 승격 | 알아서 승격, 안 되면 더 강한 모델 | `--retry auto-escalate` (Phase 3 CLI 체인 승격) |
|
|
86
|
+
| 자율 | 알아서, 자동으로, autopilot | autopilot 모드 |
|
|
87
|
+
| 최대 effort | ultracode, 울트라코드 | Claude Code `--effort ultracode`(v2.1.203+). 멀티에이전트 Workflow 오케스트레이션, 토큰 비용 무관 철저성 우선. tfx CLI 플래그는 아니다 |
|
|
88
|
+
|
|
89
|
+
## CLI 우선순위 정책: default = Codex
|
|
90
|
+
|
|
91
|
+
> 근거(why): [ADR-0004: 기본 구현 CLI = Codex](../../docs/adr/0004-codex-as-default-cli.md). 이 절이 SSOT(어떻게), ADR은 결정 이력(왜).
|
|
92
|
+
|
|
93
|
+
triflux 본체 개발의 실측 운영 패턴 (v10.18.0 ~ v10.20.2, 2주, 25+ PR) 이 거의 전부 Codex 단독 또는 Codex worker spawn 으로 진행되었다. 이 운영 fact 를 default policy 로 명시한다.
|
|
94
|
+
|
|
95
|
+
| 우선순위 | CLI / 모델 | default 적용 lane |
|
|
96
|
+
|---------|-----------|-------------------|
|
|
97
|
+
| 1차 (default) | **Codex** (프로필 설정 기준) | 구현, 수정, 디버그, 리뷰, 분석, 테스트 작성, 회귀 가드, 릴리즈 prepare |
|
|
98
|
+
| 2차 | **Antigravity** | Codex quota exhaust, 가독성 cross-check, 별도 시각 검토 |
|
|
99
|
+
| 한정 (Claude 만) | **Claude** (`opus` 최신 tier 별칭) | 메타 라우팅 (`/tfx-harness`), planning gate (`/gstack-office-hours`, `/gstack-autoplan`), gstack-specific surface (`/gstack-context-*`, `/gstack-qa`), 또는 Codex/Antigravity 미가용 |
|
|
100
|
+
|
|
101
|
+
`tfx-auto`, `tfx-review`, `tfx-research` 등 multi-CLI wrapper 는 이 정책을 따른다. 명시 플래그 (`--cli claude`, `--cli codex`) 가 있으면 override. `--mode consensus` (3-CLI 합의) 도 default head 는 Codex.
|
|
102
|
+
|
|
103
|
+
`--retry auto-escalate` 체인 (`.claude/rules/tfx-escalation-chain.md`) 의 1단계가 Codex 인 것도 이 정책과 정합한다 (2단계 Claude `fable` 최신 tier 별칭은 최종 수단).
|
|
104
|
+
|
|
105
|
+
### 모델 배치
|
|
106
|
+
|
|
107
|
+
> 근거(why): [ADR-0016](../../docs/adr/0016-codex-astra-top-tier-and-fable-escalation.md), [ADR-0017](../../docs/adr/0017-gpt6-sol-luna-lanes.md), [ADR-0019](../../docs/adr/0019-gpt61-sol-and-sonnet-55.md). 기준 문서는 OpenAI Codex 모델 페이지와 Anthropic 모델 개요(2026-09-30 확인)다.
|
|
108
|
+
|
|
109
|
+
Codex 역할별 프로필의 SSOT는 `scripts/lib/agent-route-policy.mjs`이고, 모델 ID는 `~/.codex/<프로필>.config.toml`이 정한다. `designer`, `writer` 항목은 직접 Codex를 지정한 경우에만 적용하며 일반 실행의 provider는 `hub/team/agent-map.json`이 정한다.
|
|
110
|
+
|
|
111
|
+
| 프로필 | triflux lane |
|
|
112
|
+
|---------|--------------|
|
|
113
|
+
| `gpt6_astra_xhigh` | architect, planner, critic, analyst, debugger, deep-executor, security-reviewer, scientist-deep, designer(직접 지정) |
|
|
114
|
+
| `gpt6_astra_max` / `gpt6_astra_ultra` | `TFX_CODEX_PROFILE=max/ultra`, 재시도 1단계 |
|
|
115
|
+
| `gpt61_sol_high` | executor, codex, code-reviewer, quality-reviewer, verifier, test-engineer, qa-tester, scientist, document-specialist |
|
|
116
|
+
| `gpt61_sol_med` | cleanup, deslop |
|
|
117
|
+
| `gpt6_luna_high` | build-fixer, writer(직접 지정) |
|
|
118
|
+
| `gpt6_luna_low` | spark, explore 재매핑, ChatGPT tier 폴백 |
|
|
119
|
+
| `fable` | `--retry auto-escalate` 최종 단계 |
|
|
120
|
+
| `opus` | 메타 라우팅, planning gate, 스킬의 `model="opus"` lane |
|
|
121
|
+
| `sonnet` | Claude native test-engineer, qa-tester, verifier |
|
|
122
|
+
| `haiku` | Claude native explore, claude |
|
|
123
|
+
|
|
124
|
+
## CLI 라우팅
|
|
125
|
+
|
|
126
|
+
`codex exec` / `agy -y -p` 직접 호출은 금지한다. tfx 스킬 경유 필수. 자동 차단 훅(headless-guard)은 제거되었으므로 호출자가 지킨다.
|
|
127
|
+
|
|
128
|
+
**Layer 1: Light** (tfx-route.sh → 단일 CLI)
|
|
129
|
+
|
|
130
|
+
| 경로 | CLI | 용도 |
|
|
131
|
+
|------|-----|------|
|
|
132
|
+
| `tfx-auto` | 자동 | 통합 진입점 |
|
|
133
|
+
| `tfx-auto --cli codex` | Codex | Codex 전용 lane |
|
|
134
|
+
| `tfx-auto --cli antigravity` | Antigravity | Antigravity 전용 lane |
|
|
135
|
+
| `tfx-auto --mode quick` | Codex→검증 | 단일 파일, 5분 이내 |
|
|
136
|
+
| `tfx-auto --retry auto-escalate` | 자동 승격 | 실패→더 강한 모델 |
|
|
137
|
+
|
|
138
|
+
**Layer 2: Deep** (headless 3-CLI 합의)
|
|
139
|
+
|
|
140
|
+
`tfx-auto --mode deep`, `tfx-auto --mode consensus --shape consensus|debate|panel`,
|
|
141
|
+
`tfx-auto --retry ralph`
|
|
142
|
+
|
|
143
|
+
**Layer 3: Remote/병렬**
|
|
144
|
+
|
|
145
|
+
| 스킬 | 용도 |
|
|
146
|
+
|------|------|
|
|
147
|
+
| `tfx multi` CLI | 2+개 태스크 headless 병렬 |
|
|
148
|
+
| tfx-remote | Claude Code 원격 세션 (SSH, user-state hosts.json setup 필수) |
|
|
149
|
+
|
|
150
|
+
**Claude 네이티브** (CLI 불필요): tfx-setup, tfx-doctor
|
|
151
|
+
|
|
152
|
+
**Headless UI default** : `tfx-auto`, `tfx multi`의 headless 워커는 default 로 `claude agents` 패널에 노출 (`--native-bridge-ui agents`). opt-out: `--no-native-bridge-ui`. interactive (tmux/wt) 경로는 default-off. 행에서 Enter 를 누르면 워커가 도는 tmux pane 이 열린다. 상세 행동표는 `CLAUDE.md` 의 `<native-bridge>` 섹션. 근거(why): [ADR-0008 : headless 워커 native-bridge 기본 노출](../../docs/adr/0008-native-bridge-ui-default-on.md), [ADR-0026 : agents 행을 워커 tmux 방의 attach client 로](../../docs/adr/0026-agents-row-tmux-attach.md).
|
|
153
|
+
|
|
154
|
+
자원 우선순위: remote-spawn > multi > Light > 로컬 단독
|
|
155
|
+
|
|
156
|
+
**원격 오프로드 기본 정책** : 로컬 기기의 램이 빠듯해지면 무거운 작업을 `hosts.json` 에 등록한 원격 호스트로 넘긴다. 판단 시점은 병렬 dispatch 직전이다.
|
|
157
|
+
|
|
158
|
+
- 판정 명령: `sysctl -n kern.memorystatus_vm_pressure_level` (1=normal, 2=warn, 4=critical; Stats 앱·활성 상태 보기의 메모리 색과 같은 커널 신호, 노랑=2 빨강=4)
|
|
159
|
+
- 4(critical): 무조건 오프로드
|
|
160
|
+
- 2(warn): 무거운 shard(빌드·전체 테스트 스위트·대규모 리팩터)를 띄우거나 워커 3+개를 새로 만들 때만 오프로드
|
|
161
|
+
- 1(normal): 로컬 유지
|
|
162
|
+
- sysctl 실패 시 대체 신호: `vm_stat` 5초 간격 2회 측정에서 swapouts 증가
|
|
163
|
+
- 원격으로 넘기기 전에 `node scripts/remote-spawn.mjs --probe <host>`로 호스트 상태를 먼저 확인한다.
|
|
164
|
+
- 행동: 원격 코드 변경은 별도 worktree와 세션에서 실행한다. 탐색·대화형은 tfx-remote로 원격 세션을 띄운다
|
|
165
|
+
- 로컬 여유가 충분하면 로컬 유지. 사용자가 로컬/원격을 명시하면 그에 따른다
|
|
166
|
+
- 원격 호스트 부재 시(ssh 불응) 조용히 로컬로 강등하지 말고 한 줄 알린 뒤 로컬 진행
|
|
167
|
+
|
|
168
|
+
원격 hosts 설정은 user-state 경로만 참조한다: macOS/Linux `~/.config/triflux/hosts.json`, Windows `%APPDATA%\triflux\hosts.json`. 기존 source-tree `references/hosts.json` 은 라우팅 입력으로 사용하지 않으며 첫 실행 lazy auto-migration 대상이다.
|
|
169
|
+
|
|
170
|
+
## 충돌 해소
|
|
171
|
+
|
|
172
|
+
- ralph = persist alias
|
|
173
|
+
- "auto" 단독 → tfx-auto. "알아서 해" → autopilot 모드 (위 깊이 수정자)
|
|
174
|
+
- "코드에서 찾아" → Claude 기본 Explore 에이전트. "알아봐" → tfx-research
|
|
175
|
+
- 복합 의도: "구현하고 리뷰까지" → tfx-auto 실행 후 리뷰 결과 확인
|
|
176
|
+
- "합의해서 비교해" 류 요청은 기본적으로 `tfx-auto --mode consensus --shape debate` 로 fold 한다
|
|
177
|
+
- `ralph` 표기는 항상 `--retry ralph` mode 를 지칭. persist 스킬도 동일 의미. `--retry auto-escalate` 와 동시 사용 불가 (escalation-chain.md 규약).
|
|
178
|
+
- 코드 변경 병렬 작업은 작업별 worktree와 세션을 분리하고 각 세션에서 `tfx-auto`로 실행한다.
|
|
@@ -15,24 +15,26 @@ tree, keyword rules, or owner matrix into this file.
|
|
|
15
15
|
## Locate the routing SSOT
|
|
16
16
|
|
|
17
17
|
This skill is installed globally but the SSOT belongs to the Triflux project.
|
|
18
|
-
Try these locations in order and return blocked only when all
|
|
18
|
+
Try these locations in order and return blocked only when all four fail:
|
|
19
19
|
|
|
20
20
|
1. `$TFX_ROUTING_SSOT`, when set.
|
|
21
21
|
2. `.claude/rules/tfx-routing.md` below the main Git worktree, derived from
|
|
22
22
|
the parent of `git rev-parse --git-common-dir`.
|
|
23
23
|
3. Walk upward from the current working directory looking for
|
|
24
24
|
`.claude/rules/tfx-routing.md`.
|
|
25
|
+
4. `references/tfx-routing.md` inside this skill directory, copied from the
|
|
26
|
+
package by `tfx setup`. It serves machines without a clone and may lag the
|
|
27
|
+
clone, so it comes last.
|
|
25
28
|
|
|
26
29
|
Do not use `git rev-parse --show-toplevel` for step 2. In a linked worktree it
|
|
27
30
|
returns that worktree root, whose SSOT may be a stale branch snapshot.
|
|
28
31
|
`--git-common-dir` still points to the main repository's `.git` directory.
|
|
29
32
|
|
|
30
|
-
When blocked, include this one-line remedy:
|
|
31
|
-
|
|
32
|
-
`export TFX_ROUTING_SSOT=<triflux clone>/.claude/rules/tfx-routing.md`
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
load interactive `~/.zshrc`. Do not create a global copy or symlink under
|
|
33
|
+
When blocked, include this one-line remedy: rerun `tfx setup` to install the
|
|
34
|
+
step 4 copy. To follow a clone's latest policy instead, set
|
|
35
|
+
`export TFX_ROUTING_SSOT=<triflux clone>/.claude/rules/tfx-routing.md` in
|
|
36
|
+
`~/.zshenv`, because hooks and headless workers may not load interactive
|
|
37
|
+
`~/.zshrc`. Do not create a global copy or symlink under
|
|
36
38
|
`~/.claude/rules/`; Claude auto-loads that directory into unrelated sessions.
|
|
37
39
|
|
|
38
40
|
## Return contract
|
package/bin/triflux.mjs
CHANGED
|
@@ -74,6 +74,7 @@ import {
|
|
|
74
74
|
listInlineProfileNames,
|
|
75
75
|
MODS_UPDATE_COMMAND,
|
|
76
76
|
persistSettings,
|
|
77
|
+
planStatusLine,
|
|
77
78
|
REQUIRED_CODEX_PROFILES,
|
|
78
79
|
retireOldInstallFiles,
|
|
79
80
|
runConsentSteps,
|
|
@@ -1139,11 +1140,11 @@ function previewStatusLineAction() {
|
|
|
1139
1140
|
return {
|
|
1140
1141
|
type: "statusLine",
|
|
1141
1142
|
path: settingsPath,
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1143
|
+
// setup 은 statusLine 을 다루기 전에 HUD 파일(래퍼 포함)을 먼저 복사한다.
|
|
1144
|
+
change: planStatusLine(settings, {
|
|
1145
|
+
hudPath,
|
|
1146
|
+
willSyncWrapper: existsSync(join(PKG_ROOT, "hud", "hud-statusline.sh")),
|
|
1147
|
+
}),
|
|
1147
1148
|
current: currentCmd || null,
|
|
1148
1149
|
target: hudPath,
|
|
1149
1150
|
};
|
package/hud/constants.mjs
CHANGED
|
@@ -17,6 +17,8 @@ export const CLAUDE_BAND_MARKER_DIR = join(
|
|
|
17
17
|
export const BAND_FLAG = "--band";
|
|
18
18
|
export const BAND_HEADER = "tfx-band 1";
|
|
19
19
|
export const BAND_ALL_TTL_MS = 3 * 60 * 1000;
|
|
20
|
+
// 양옆 여백 4칸과 첫 행 오른쪽 알림 자리 30칸.
|
|
21
|
+
export const STATUSLINE_RESERVE_COLS = 34;
|
|
20
22
|
export const CLAUDE_BAND_MARKER_TTL_MS = 12 * 60 * 60 * 1000;
|
|
21
23
|
|
|
22
24
|
export const CONTEXT_MONITOR_CACHE_PATH = join(
|
package/hud/hud-qos-status.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
CODEX_REFRESH_FLAG,
|
|
25
25
|
getCodexAuthPath,
|
|
26
26
|
getCodexHome,
|
|
27
|
+
STATUSLINE_RESERVE_COLS,
|
|
27
28
|
} from "./constants.mjs";
|
|
28
29
|
import { buildContextUsageView } from "./context-monitor.mjs";
|
|
29
30
|
import {
|
|
@@ -69,11 +70,15 @@ async function main() {
|
|
|
69
70
|
|
|
70
71
|
// --band: triflux mods 가 입력창 위에 그릴 줄을 만든다. statusLine 은 band 가 다 그리는 세션에서 비운다.
|
|
71
72
|
const bandMode = process.argv.includes(BAND_FLAG);
|
|
73
|
+
// statusLine 은 엔진이 양옆에 2칸씩 여백을 두고, 첫 행 오른쪽에 알림을 띄운다(공식 문서). 그만큼 좁게 그린다.
|
|
74
|
+
// 알림 자리를 빼고 40칸이 안 남는 좁은 화면은 그대로 두어 원래 축소 단계를 따른다.
|
|
75
|
+
const reserved = Number(process.env.COLUMNS) - STATUSLINE_RESERVE_COLS;
|
|
76
|
+
if (!bandMode && reserved >= 40) process.env.COLUMNS = String(reserved);
|
|
72
77
|
const stdin = await readStdinJson();
|
|
73
78
|
const bandState = bandMode ? "none" : readBandState(stdin?.session_id);
|
|
74
79
|
if (bandState === "all") return;
|
|
75
|
-
//
|
|
76
|
-
const
|
|
80
|
+
// Claude Code 가 넘긴 세션 사용량이 있으면 Claude API 를 따로 조회하지 않는다.
|
|
81
|
+
const stdinClaudeUsage = claudeUsageFromStdin(stdin);
|
|
77
82
|
|
|
78
83
|
const { showCodex, antigravityAllowed } = resolveHudCliVisibility();
|
|
79
84
|
const claudeUsageSnapshot = readClaudeUsageSnapshot();
|
|
@@ -84,7 +89,7 @@ async function main() {
|
|
|
84
89
|
if (antigravitySnapshot?.shouldRefresh) scheduleAntigravityQuotaRefresh();
|
|
85
90
|
// 설정이 없는 홈에서는 갱신 프로세스를 시작하지 않는다.
|
|
86
91
|
if (
|
|
87
|
-
!
|
|
92
|
+
!stdinClaudeUsage &&
|
|
88
93
|
claudeUsageSnapshot.shouldRefresh &&
|
|
89
94
|
existsSync(join(homedir(), ".claude"))
|
|
90
95
|
) {
|
|
@@ -96,7 +101,7 @@ async function main() {
|
|
|
96
101
|
|
|
97
102
|
const contextView = buildContextUsageView(stdin);
|
|
98
103
|
const claudeUsage =
|
|
99
|
-
|
|
104
|
+
stdinClaudeUsage ??
|
|
100
105
|
(claudeUsageSnapshot.data
|
|
101
106
|
? { ...claudeUsageSnapshot.data, stale: claudeUsageSnapshot.isStale }
|
|
102
107
|
: null);
|
|
@@ -123,7 +128,12 @@ async function main() {
|
|
|
123
128
|
const rows =
|
|
124
129
|
bandState === "claude"
|
|
125
130
|
? []
|
|
126
|
-
: getClaudeRows(
|
|
131
|
+
: getClaudeRows(
|
|
132
|
+
currentTier,
|
|
133
|
+
contextView,
|
|
134
|
+
claudeUsage,
|
|
135
|
+
Number(stdin?.cost?.total_cost_usd ?? Number.NaN),
|
|
136
|
+
);
|
|
127
137
|
let codexRowIndex = -1;
|
|
128
138
|
if (showCodex) {
|
|
129
139
|
codexRowIndex = rows.length;
|
|
@@ -191,16 +201,30 @@ function readBandState(sessionId) {
|
|
|
191
201
|
}
|
|
192
202
|
}
|
|
193
203
|
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
204
|
+
// statusLine 입력의 rate_limits(Claude Code 2.1.251+, Pro/Max 와 gateway)를 HUD 의 Claude 사용량 형식으로 바꾼다.
|
|
205
|
+
// 10.57.0 mods 가 넘기던 claude_rate_limits 배열도 받는다.
|
|
206
|
+
function claudeUsageFromStdin(stdin) {
|
|
207
|
+
const windows = {};
|
|
208
|
+
const limits = stdin?.rate_limits;
|
|
209
|
+
if (limits && typeof limits === "object" && !Array.isArray(limits)) {
|
|
210
|
+
for (const kind of ["five_hour", "seven_day", "spend_limit"]) {
|
|
211
|
+
const w = limits[kind];
|
|
212
|
+
if (w)
|
|
213
|
+
windows[kind] = { percent: w.used_percentage, resetsAt: w.resets_at };
|
|
214
|
+
}
|
|
215
|
+
} else if (Array.isArray(stdin?.claude_rate_limits)) {
|
|
216
|
+
for (const w of stdin.claude_rate_limits)
|
|
217
|
+
if (w?.kind)
|
|
218
|
+
windows[w.kind] = { percent: w.percentUsed, resetsAt: w.resetsAt };
|
|
219
|
+
}
|
|
220
|
+
const {
|
|
221
|
+
five_hour: fiveHour,
|
|
222
|
+
seven_day: weekly,
|
|
223
|
+
spend_limit: spendLimit,
|
|
224
|
+
} = windows;
|
|
201
225
|
if (!fiveHour && !weekly && !spendLimit) return null;
|
|
202
226
|
const percent = (w) =>
|
|
203
|
-
Number.isFinite(w?.
|
|
227
|
+
Number.isFinite(w?.percent) ? Math.round(w.percent) : null;
|
|
204
228
|
return {
|
|
205
229
|
fiveHourPercent: percent(fiveHour),
|
|
206
230
|
weeklyPercent: percent(weekly),
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# statusLine 진입점. triflux mods band 가 HUD 를 입력창 위에 그리는 세션이면 node 를 띄우지 않고 끝낸다.
|
|
3
|
+
# band 는 1분마다 표식에 "all:<시각>" 을 다시 쓰므로, 3분 넘게 멈췄거나 표식이 없으면 HUD 를 그대로 돌린다.
|
|
4
|
+
# 인자 1: node 실행 파일(없으면 PATH 의 node).
|
|
5
|
+
# Claude Code 는 입력을 닫아 주지만, 닫히지 않아도 1초 뒤에는 읽기를 끝낸다.
|
|
6
|
+
# 백그라운드 명령은 stdin 이 /dev/null 로 바뀌므로 fd 3 으로 넘긴다.
|
|
7
|
+
exec 3<&0
|
|
8
|
+
input=$({ cat <&3 & cat_pid=$!; (sleep 1; kill "$cat_pid" 2>/dev/null) >/dev/null 2>&1 & wait "$cat_pid"; })
|
|
9
|
+
exec 3<&-
|
|
10
|
+
sid=$(printf '%s' "$input" | sed -n 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([A-Za-z0-9_-]*\)".*/\1/p' | head -n 1)
|
|
11
|
+
marker="$HOME/.claude/cache/triflux/claude-band/$sid"
|
|
12
|
+
if [ -n "$sid" ] && [ -f "$marker" ] && [ -n "$(find "$marker" -mmin -3 2>/dev/null)" ]; then
|
|
13
|
+
case "$(head -c 4 "$marker")" in all:) exit 0 ;; esac
|
|
14
|
+
fi
|
|
15
|
+
printf '%s' "$input" | "${1:-node}" "$(dirname "$0")/hud-qos-status.mjs"
|
package/hud/providers/claude.mjs
CHANGED
|
@@ -26,8 +26,10 @@ import {
|
|
|
26
26
|
SPAWN_LOCK_TTL_MS,
|
|
27
27
|
} from "../constants.mjs";
|
|
28
28
|
import {
|
|
29
|
+
acquireSpawnLock,
|
|
29
30
|
advanceToNextCycle,
|
|
30
31
|
clampPercent,
|
|
32
|
+
parseRetryAfterMs,
|
|
31
33
|
readJson,
|
|
32
34
|
writeJsonSafe,
|
|
33
35
|
} from "../utils.mjs";
|
|
@@ -77,10 +79,27 @@ export function computeClaudeUsagePollState({
|
|
|
77
79
|
};
|
|
78
80
|
}
|
|
79
81
|
|
|
82
|
+
// 서버가 Retry-After 로 더 기다리라고 하면 따르되, 잘못된 큰 값에 오래 묶이지 않게 1시간에서 자른다.
|
|
83
|
+
const MAX_RETRY_AFTER_MS = 60 * 60 * 1000;
|
|
84
|
+
function withRetryAfter(pollState, retryAfterMs) {
|
|
85
|
+
if (!retryAfterMs) return pollState;
|
|
86
|
+
return {
|
|
87
|
+
...pollState,
|
|
88
|
+
delayMs: Math.max(
|
|
89
|
+
pollState.delayMs,
|
|
90
|
+
Math.min(retryAfterMs, MAX_RETRY_AFTER_MS),
|
|
91
|
+
),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
80
95
|
function getSnapshotSchedule(cache) {
|
|
81
96
|
const timestamp = Number(cache?.timestamp);
|
|
82
97
|
const nextRefreshAt = Number(cache?.nextRefreshAt);
|
|
83
|
-
|
|
98
|
+
// 다음 갱신이 상한보다 멀면 시계가 뒤로 간 것이다. 그대로 두면 갱신이 오래 멈춘다.
|
|
99
|
+
if (
|
|
100
|
+
Number.isFinite(nextRefreshAt) &&
|
|
101
|
+
nextRefreshAt - Date.now() <= MAX_RETRY_AFTER_MS
|
|
102
|
+
) {
|
|
84
103
|
return {
|
|
85
104
|
nextRefreshAt,
|
|
86
105
|
shouldRefresh: Date.now() >= nextRefreshAt,
|
|
@@ -448,7 +467,11 @@ export function fetchClaudeUsageFromApi(accessToken) {
|
|
|
448
467
|
resolve({ ok: false, status: 0 });
|
|
449
468
|
}
|
|
450
469
|
} else {
|
|
451
|
-
resolve({
|
|
470
|
+
resolve({
|
|
471
|
+
ok: false,
|
|
472
|
+
status: res.statusCode,
|
|
473
|
+
retryAfterMs: parseRetryAfterMs(res.headers?.["retry-after"]),
|
|
474
|
+
});
|
|
452
475
|
}
|
|
453
476
|
});
|
|
454
477
|
},
|
|
@@ -674,10 +697,13 @@ export async function fetchClaudeUsage(forceRefresh = false) {
|
|
|
674
697
|
: "unknown";
|
|
675
698
|
const pollState =
|
|
676
699
|
errorType === "rate_limit"
|
|
677
|
-
?
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
700
|
+
? withRetryAfter(
|
|
701
|
+
computeClaudeUsagePollState({
|
|
702
|
+
consecutive429s,
|
|
703
|
+
outcome: "rate_limit",
|
|
704
|
+
}),
|
|
705
|
+
result.retryAfterMs,
|
|
706
|
+
)
|
|
681
707
|
: null;
|
|
682
708
|
writeClaudeUsageCache(
|
|
683
709
|
existingSnapshot.data,
|
|
@@ -720,11 +746,7 @@ export function scheduleClaudeUsageRefresh() {
|
|
|
720
746
|
|
|
721
747
|
// 스폰 락: 30초 내 이미 스폰했으면 중복 방지 (첫 설치 시 429 방지)
|
|
722
748
|
try {
|
|
723
|
-
if (
|
|
724
|
-
const lockAge = Date.now() - readJson(CLAUDE_REFRESH_LOCK_PATH, {}).t;
|
|
725
|
-
if (lockAge < SPAWN_LOCK_TTL_MS) return;
|
|
726
|
-
}
|
|
727
|
-
writeJsonSafe(CLAUDE_REFRESH_LOCK_PATH, { t: Date.now() });
|
|
749
|
+
if (!acquireSpawnLock(CLAUDE_REFRESH_LOCK_PATH, SPAWN_LOCK_TTL_MS)) return;
|
|
728
750
|
} catch {
|
|
729
751
|
/* 락 실패 무시 — 스폰 진행 */
|
|
730
752
|
}
|
package/hud/providers/codex.mjs
CHANGED
|
@@ -26,7 +26,12 @@ import {
|
|
|
26
26
|
getCodexHome,
|
|
27
27
|
SPAWN_LOCK_TTL_MS,
|
|
28
28
|
} from "../constants.mjs";
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
acquireSpawnLock,
|
|
31
|
+
decodeJwtEmail,
|
|
32
|
+
readJson,
|
|
33
|
+
writeJsonSafe,
|
|
34
|
+
} from "../utils.mjs";
|
|
30
35
|
import {
|
|
31
36
|
listProbeTargets,
|
|
32
37
|
probeRateLimitsViaAppServer,
|
|
@@ -552,11 +557,7 @@ export function scheduleCodexRateLimitRefresh() {
|
|
|
552
557
|
|
|
553
558
|
// 스폰 락: 30초 내 이미 스폰했으면 중복 방지
|
|
554
559
|
try {
|
|
555
|
-
if (
|
|
556
|
-
const lockAge = Date.now() - readJson(CODEX_REFRESH_LOCK_PATH, {}).t;
|
|
557
|
-
if (lockAge < SPAWN_LOCK_TTL_MS) return;
|
|
558
|
-
}
|
|
559
|
-
writeJsonSafe(CODEX_REFRESH_LOCK_PATH, { t: Date.now() });
|
|
560
|
+
if (!acquireSpawnLock(CODEX_REFRESH_LOCK_PATH, SPAWN_LOCK_TTL_MS)) return;
|
|
560
561
|
} catch {
|
|
561
562
|
/* 락 실패 무시 — 스폰 진행 */
|
|
562
563
|
}
|
package/hud/providers/gemini.mjs
CHANGED
|
@@ -10,7 +10,12 @@ import {
|
|
|
10
10
|
ANTIGRAVITY_SIGNED_IN_GRACE_MS,
|
|
11
11
|
SPAWN_LOCK_TTL_MS,
|
|
12
12
|
} from "../constants.mjs";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
acquireSpawnLock,
|
|
15
|
+
decodeJwtEmail,
|
|
16
|
+
readJson,
|
|
17
|
+
writeJsonSafe,
|
|
18
|
+
} from "../utils.mjs";
|
|
14
19
|
|
|
15
20
|
function getCredentialEmail(credential) {
|
|
16
21
|
return (
|
|
@@ -140,9 +145,8 @@ export function scheduleAntigravityQuotaRefresh() {
|
|
|
140
145
|
isGcpProjectAuth()
|
|
141
146
|
)
|
|
142
147
|
return;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
writeJsonSafe(ANTIGRAVITY_REFRESH_LOCK_PATH, { t: Date.now() });
|
|
148
|
+
if (!acquireSpawnLock(ANTIGRAVITY_REFRESH_LOCK_PATH, SPAWN_LOCK_TTL_MS))
|
|
149
|
+
return;
|
|
146
150
|
try {
|
|
147
151
|
const child = spawn(
|
|
148
152
|
process.execPath,
|
package/hud/renderers.mjs
CHANGED
|
@@ -177,9 +177,20 @@ function spendOnlyPercent(claudeUsage) {
|
|
|
177
177
|
// ============================================================================
|
|
178
178
|
// Claude 행 렌더러
|
|
179
179
|
// ============================================================================
|
|
180
|
-
export function getClaudeRows(
|
|
180
|
+
export function getClaudeRows(
|
|
181
|
+
currentTier,
|
|
182
|
+
contextView,
|
|
183
|
+
claudeUsage,
|
|
184
|
+
costUsd = null,
|
|
185
|
+
) {
|
|
181
186
|
const ctxView = contextView || buildContextUsageView({});
|
|
182
187
|
const prefix = `${bold(claudeOrange("c"))}:`;
|
|
188
|
+
// 이번 세션 비용. 폭이 넉넉한 full, compact 에서만 CTX 뒤에 붙인다.
|
|
189
|
+
const costText =
|
|
190
|
+
Number.isFinite(costUsd) &&
|
|
191
|
+
(currentTier === "full" || currentTier === "compact")
|
|
192
|
+
? ` ${dim(`$${costUsd.toFixed(2)}`)}`
|
|
193
|
+
: "";
|
|
183
194
|
const spend = spendOnlyPercent(claudeUsage);
|
|
184
195
|
if (spend != null) {
|
|
185
196
|
if (currentTier === "nano" || currentTier === "micro") {
|
|
@@ -188,7 +199,7 @@ export function getClaudeRows(currentTier, contextView, claudeUsage) {
|
|
|
188
199
|
}
|
|
189
200
|
const pct = colorByProvider(spend, formatPercentCell(spend), claudeOrange);
|
|
190
201
|
const left = `${dim("$:")}${tierBar(currentTier, spend, CLAUDE_ORANGE)}${pct}`;
|
|
191
|
-
const right = `${dim("CTX:")}${contextPercentText(ctxView)}`;
|
|
202
|
+
const right = `${dim("CTX:")}${contextPercentText(ctxView)}${costText}`;
|
|
192
203
|
return [{ prefix, left, right }];
|
|
193
204
|
}
|
|
194
205
|
// API 실측 데이터
|
|
@@ -256,7 +267,7 @@ export function getClaudeRows(currentTier, contextView, claudeUsage) {
|
|
|
256
267
|
const warning = ctxView.warningTag
|
|
257
268
|
? ` ${dim("|")} ${yellow(ctxView.warningTag)}`
|
|
258
269
|
: "";
|
|
259
|
-
const contextSection = `${dim("CTX:")}${contextPercentText(ctxView)}${warning}`;
|
|
270
|
+
const contextSection = `${dim("CTX:")}${contextPercentText(ctxView)}${costText}${warning}`;
|
|
260
271
|
return [{ prefix, left: quotaSection, right: contextSection }];
|
|
261
272
|
}
|
|
262
273
|
|
|
@@ -266,7 +277,7 @@ export function getClaudeRows(currentTier, contextView, claudeUsage) {
|
|
|
266
277
|
const warning = ctxView.warningTag
|
|
267
278
|
? ` ${dim("|")} ${yellow(ctxView.warningTag)}`
|
|
268
279
|
: "";
|
|
269
|
-
const contextSection = `${dim("CTX:")}${contextPercentText(ctxView)}${warning}`;
|
|
280
|
+
const contextSection = `${dim("CTX:")}${contextPercentText(ctxView)}${costText}${warning}`;
|
|
270
281
|
return [{ prefix, left: quotaSection, right: contextSection }];
|
|
271
282
|
}
|
|
272
283
|
|
package/hud/utils.mjs
CHANGED
|
@@ -7,32 +7,40 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
7
7
|
import { dirname } from "node:path";
|
|
8
8
|
import { PERCENT_CELL_WIDTH, TIME_CELL_INNER_WIDTH } from "./constants.mjs";
|
|
9
9
|
|
|
10
|
+
const STDIN_MAX_BYTES = 1024 * 1024;
|
|
11
|
+
|
|
12
|
+
// 입력이 JSON 으로 읽히는 순간 끝낸다. Claude Code 가 stdin 을 늦게 닫아도 기다리지 않는다.
|
|
10
13
|
export async function readStdinJson() {
|
|
11
14
|
if (process.stdin.isTTY) return {};
|
|
12
15
|
return new Promise((resolve) => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
19
|
-
process.stdin.on("end", () => {
|
|
16
|
+
let raw = "";
|
|
17
|
+
let done = false;
|
|
18
|
+
const finish = (value) => {
|
|
19
|
+
if (done) return;
|
|
20
|
+
done = true;
|
|
20
21
|
clearTimeout(timeout);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
process.stdin.removeAllListeners("data");
|
|
23
|
+
process.stdin.destroy();
|
|
24
|
+
resolve(value);
|
|
25
|
+
};
|
|
26
|
+
const tryParse = () => {
|
|
26
27
|
try {
|
|
27
|
-
|
|
28
|
+
return JSON.parse(raw);
|
|
28
29
|
} catch {
|
|
29
|
-
|
|
30
|
+
return null;
|
|
30
31
|
}
|
|
32
|
+
};
|
|
33
|
+
// 200ms 안에 다 못 받으면 받은 만큼으로 판정한다.
|
|
34
|
+
const timeout = setTimeout(() => finish(tryParse() ?? {}), 200);
|
|
35
|
+
process.stdin.setEncoding("utf8");
|
|
36
|
+
process.stdin.on("data", (chunk) => {
|
|
37
|
+
raw += chunk;
|
|
38
|
+
if (raw.length > STDIN_MAX_BYTES) return finish({});
|
|
39
|
+
const parsed = tryParse();
|
|
40
|
+
if (parsed) finish(parsed);
|
|
31
41
|
});
|
|
32
|
-
process.stdin.on("
|
|
33
|
-
|
|
34
|
-
resolve({});
|
|
35
|
-
});
|
|
42
|
+
process.stdin.on("end", () => finish(tryParse() ?? {}));
|
|
43
|
+
process.stdin.on("error", () => finish({}));
|
|
36
44
|
process.stdin.resume();
|
|
37
45
|
});
|
|
38
46
|
}
|
|
@@ -56,6 +64,28 @@ export function writeJsonSafe(filePath, data) {
|
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
// .omc/ → .claude/cache/ 마이그레이션: 새 경로 우선, 없으면 레거시 읽고 복사
|
|
67
|
+
// 갱신 프로세스를 겹쳐 띄우지 않는 락. 만료 시각을 적고, 남은 시간이 ttl 보다 길면(시계가 뒤로 감) 깨진 락으로 본다.
|
|
68
|
+
// 예전 { t } 락도 읽는다.
|
|
69
|
+
export function acquireSpawnLock(lockPath, ttlMs, now = Date.now()) {
|
|
70
|
+
const lock = readJson(lockPath, null);
|
|
71
|
+
const until = Number(lock?.until ?? Number(lock?.t) + ttlMs);
|
|
72
|
+
const remaining = until - now;
|
|
73
|
+
if (Number.isFinite(remaining) && remaining > 0 && remaining <= ttlMs)
|
|
74
|
+
return false;
|
|
75
|
+
writeJsonSafe(lockPath, { until: now + ttlMs });
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Retry-After 헤더(초 또는 HTTP 날짜)를 ms 로 바꾼다. 없거나 지난 값이면 null.
|
|
80
|
+
export function parseRetryAfterMs(value, now = Date.now()) {
|
|
81
|
+
if (value == null || value === "") return null;
|
|
82
|
+
const seconds = Number(value);
|
|
83
|
+
const ms = Number.isFinite(seconds)
|
|
84
|
+
? seconds * 1000
|
|
85
|
+
: Date.parse(value) - now;
|
|
86
|
+
return Number.isFinite(ms) && ms > 0 ? ms : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
59
89
|
export function readJsonMigrate(newPath, legacyPath, fallback) {
|
|
60
90
|
const data = readJson(newPath, null);
|
|
61
91
|
if (data != null) return data;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "triflux",
|
|
3
|
-
"version": "10.57.
|
|
3
|
+
"version": "10.57.2",
|
|
4
4
|
"description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Antigravity, and Claude",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"adapters",
|
|
24
24
|
"bin",
|
|
25
25
|
"config",
|
|
26
|
+
".claude/rules/tfx-routing.md",
|
|
26
27
|
"skills",
|
|
27
28
|
"!skills/tfx-workspace",
|
|
28
29
|
"hooks",
|
|
@@ -33,6 +33,8 @@ const MIRROR_TOPS = [
|
|
|
33
33
|
"scripts",
|
|
34
34
|
"skills",
|
|
35
35
|
];
|
|
36
|
+
// 디렉터리 전체가 아니라 한 파일만 게시하는 경로. tfx-harness 가 clone 없는 기기에서 읽는다.
|
|
37
|
+
const MIRROR_FILES = [".claude/rules/tfx-routing.md"];
|
|
36
38
|
// core 미러 대상은 pack.mjs 의 CORE_ENTRIES 하나로 정한다. 검사기가 따로 손 목록을 두면
|
|
37
39
|
// 하위 디렉터리나 새 파일이 빠져도 OK 가 나왔다(#674).
|
|
38
40
|
const CORE_NON_MIRROR = new Set([
|
|
@@ -189,48 +191,55 @@ function compareMirror({
|
|
|
189
191
|
const issues = [];
|
|
190
192
|
const fixed = [];
|
|
191
193
|
|
|
194
|
+
const pairs = [];
|
|
192
195
|
for (const top of MIRROR_TOPS) {
|
|
193
|
-
const srcDir = join(repoRoot, top);
|
|
194
|
-
const dstDir = join(mirrorRoot, top);
|
|
195
196
|
const skipRels = SKIP_RELS.get(top) ?? new Set();
|
|
196
|
-
const srcFiles = new Set(walkRelFiles(
|
|
197
|
-
const dstFiles = new Set(walkRelFiles(
|
|
198
|
-
const
|
|
197
|
+
const srcFiles = new Set(walkRelFiles(join(repoRoot, top), skipRels));
|
|
198
|
+
const dstFiles = new Set(walkRelFiles(join(mirrorRoot, top), skipRels));
|
|
199
|
+
for (const rel of new Set([...srcFiles, ...dstFiles]))
|
|
200
|
+
pairs.push({
|
|
201
|
+
rel: `${top}/${rel}`,
|
|
202
|
+
inSrc: srcFiles.has(rel),
|
|
203
|
+
inDst: dstFiles.has(rel),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
for (const rel of MIRROR_FILES) {
|
|
207
|
+
const inSrc = existsSync(join(repoRoot, rel));
|
|
208
|
+
const inDst = existsSync(join(mirrorRoot, rel));
|
|
209
|
+
if (inSrc || inDst) pairs.push({ rel, inSrc, inDst });
|
|
210
|
+
}
|
|
199
211
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const inDst = dstFiles.has(rel);
|
|
205
|
-
const displayPath = `packages/triflux/${top}/${rel}`;
|
|
212
|
+
for (const { rel, inSrc, inDst } of pairs) {
|
|
213
|
+
const srcPath = join(repoRoot, rel);
|
|
214
|
+
const dstPath = join(mirrorRoot, rel);
|
|
215
|
+
const displayPath = `packages/triflux/${rel}`;
|
|
206
216
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
continue;
|
|
217
|
+
if (inSrc && !inDst) {
|
|
218
|
+
if (fix) {
|
|
219
|
+
mkdirSync(dirname(dstPath), { recursive: true });
|
|
220
|
+
copyFileSync(srcPath, dstPath);
|
|
221
|
+
fixed.push({ path: displayPath, kind: "added" });
|
|
222
|
+
} else {
|
|
223
|
+
issues.push({ path: displayPath, kind: "missing-in-mirror" });
|
|
216
224
|
}
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
217
227
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
228
|
+
if (!inSrc && inDst) {
|
|
229
|
+
// Orphan in mirror — source of truth is root, mirror must not have
|
|
230
|
+
// extra files. Do not auto-delete; require manual decision.
|
|
231
|
+
issues.push({ path: displayPath, kind: "orphan-in-mirror" });
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
224
234
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
235
|
+
const a = readFileSync(srcPath);
|
|
236
|
+
const b = readFileSync(dstPath);
|
|
237
|
+
if (!a.equals(b)) {
|
|
238
|
+
if (fix) {
|
|
239
|
+
copyFileSync(srcPath, dstPath);
|
|
240
|
+
fixed.push({ path: displayPath, kind: "updated" });
|
|
241
|
+
} else {
|
|
242
|
+
issues.push({ path: displayPath, kind: "content-diff" });
|
|
234
243
|
}
|
|
235
244
|
}
|
|
236
245
|
}
|
package/scripts/setup.mjs
CHANGED
|
@@ -676,7 +676,7 @@ function installEntries(pluginRoot) {
|
|
|
676
676
|
"scripts/codex-profile-sanitize.mjs",
|
|
677
677
|
// tfx-route.sh 가 $sd/lib 에서 찾는다.
|
|
678
678
|
...listFiles(pluginRoot, "scripts/lib", [".mjs", ".sh"]),
|
|
679
|
-
...listFiles(pluginRoot, "hud", [".mjs"]),
|
|
679
|
+
...listFiles(pluginRoot, "hud", [".mjs", ".sh"]),
|
|
680
680
|
];
|
|
681
681
|
}
|
|
682
682
|
|
|
@@ -1241,6 +1241,19 @@ function syncCodexManagedSkills({
|
|
|
1241
1241
|
}));
|
|
1242
1242
|
}
|
|
1243
1243
|
|
|
1244
|
+
// tfx-harness 는 triflux clone 밖에서도 호출된다. clone 이 없는 기기에서도 라우팅 문서를
|
|
1245
|
+
// 읽도록 패키지에 든 사본을 설치된 스킬 옆에 둔다. ~/.claude/rules 는 모든 세션이 자동
|
|
1246
|
+
// 로드하므로 쓰지 않는다.
|
|
1247
|
+
function installRoutingSsot(pluginRoot, skillDir) {
|
|
1248
|
+
const src = join(pluginRoot, ".claude", "rules", "tfx-routing.md");
|
|
1249
|
+
const dst = join(skillDir, "references", "tfx-routing.md");
|
|
1250
|
+
if (!existsSync(src) || !existsSync(join(skillDir, "SKILL.md"))) return false;
|
|
1251
|
+
if (!shouldSyncTextFile(src, dst)) return false;
|
|
1252
|
+
mkdirSync(dirname(dst), { recursive: true });
|
|
1253
|
+
copyFileSync(src, dst);
|
|
1254
|
+
return true;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1244
1257
|
export function syncSkills({
|
|
1245
1258
|
pluginRoot = PLUGIN_ROOT,
|
|
1246
1259
|
claudeDir = CLAUDE_DIR,
|
|
@@ -1293,6 +1306,12 @@ export function syncSkills({
|
|
|
1293
1306
|
} else if (skill.action === "skipped")
|
|
1294
1307
|
result.warnings.push(`Codex ${skill.name}: 사용자 스킬 보존`);
|
|
1295
1308
|
}
|
|
1309
|
+
const harnessDirs = [join(destination, "tfx-harness")];
|
|
1310
|
+
const codexHarness = join(codexDir, "skills", "tfx-harness");
|
|
1311
|
+
if (existsSync(join(codexHarness, MANAGED_CODEX_SKILL_MARKER)))
|
|
1312
|
+
harnessDirs.push(codexHarness);
|
|
1313
|
+
for (const dir of harnessDirs)
|
|
1314
|
+
if (installRoutingSsot(pluginRoot, dir)) result.changed++;
|
|
1296
1315
|
return result;
|
|
1297
1316
|
}
|
|
1298
1317
|
|
|
@@ -1689,35 +1708,71 @@ export function persistSettings(settings, settingsPath = SETTINGS_PATH) {
|
|
|
1689
1708
|
}
|
|
1690
1709
|
}
|
|
1691
1710
|
|
|
1711
|
+
const HUD_STATUSLINE_SCRIPT = "hud-statusline.sh";
|
|
1712
|
+
|
|
1713
|
+
function commandTokens(command) {
|
|
1714
|
+
return [
|
|
1715
|
+
...String(command || "").matchAll(/"([^"\n]+)"|'([^'\n]+)'|(\S+)/gu),
|
|
1716
|
+
].map((m) => m[1] ?? m[2] ?? m[3]);
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
// macOS/Linux 는 sh 래퍼로 돌려 mods band 가 그리는 세션에서 node 를 띄우지 않는다(hud/hud-statusline.sh).
|
|
1720
|
+
// willSyncWrapper: 미리보기처럼 래퍼를 아직 복사하기 전이지만 setup 이 곧 복사할 때.
|
|
1721
|
+
function buildStatusLineCommand(hudPath, { willSyncWrapper = false } = {}) {
|
|
1722
|
+
const wrapper = join(dirname(hudPath), HUD_STATUSLINE_SCRIPT);
|
|
1723
|
+
if (process.platform === "win32" || !(willSyncWrapper || existsSync(wrapper)))
|
|
1724
|
+
return buildNodeScriptCommand(hudPath);
|
|
1725
|
+
const node = resolveStableNodeBin(process.execPath, { fallback: "node" });
|
|
1726
|
+
return `sh ${quoteShellCommandArg(wrapper)} ${quoteShellCommandArg(node)}`;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// triflux 가 등록한 statusLine 인지: node <hud> 또는 sh <래퍼> <node>.
|
|
1730
|
+
function isTrifluxStatusLine(command, hudPath) {
|
|
1731
|
+
const isNode = (bin) => /(?:^|[/\\])node(?:\.exe)?$/u.test(bin ?? "");
|
|
1732
|
+
const tokens = commandTokens(command);
|
|
1733
|
+
const hud = hudPath.replace(/\\/g, "/");
|
|
1734
|
+
const wrapper = join(dirname(hudPath), HUD_STATUSLINE_SCRIPT).replace(
|
|
1735
|
+
/\\/g,
|
|
1736
|
+
"/",
|
|
1737
|
+
);
|
|
1738
|
+
if (tokens.length === 2) return isNode(tokens[0]) && tokens[1] === hud;
|
|
1739
|
+
if (tokens.length === 3)
|
|
1740
|
+
return tokens[0] === "sh" && tokens[1] === wrapper && isNode(tokens[2]);
|
|
1741
|
+
return false;
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
// 미리보기와 실제 적용이 같은 판정을 쓴다. 사용자가 직접 넣은 statusLine 은 skip 이다.
|
|
1745
|
+
export function planStatusLine(
|
|
1746
|
+
settings,
|
|
1747
|
+
{ hudPath = HUD_PATH, willSyncWrapper = false } = {},
|
|
1748
|
+
) {
|
|
1749
|
+
const current = settings.statusLine;
|
|
1750
|
+
if (current == null) return "create";
|
|
1751
|
+
if (current.command === buildStatusLineCommand(hudPath, { willSyncWrapper }))
|
|
1752
|
+
return "noop";
|
|
1753
|
+
return current.type === "command" &&
|
|
1754
|
+
isTrifluxStatusLine(current.command, hudPath)
|
|
1755
|
+
? "update"
|
|
1756
|
+
: "skip";
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1692
1759
|
export function applyStatusLine(
|
|
1693
1760
|
settings,
|
|
1694
1761
|
{ hudPath = HUD_PATH, warn = console.warn } = {},
|
|
1695
1762
|
) {
|
|
1696
1763
|
if (!existsSync(hudPath)) return false;
|
|
1697
|
-
const
|
|
1698
|
-
|
|
1699
|
-
if (
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
/^(?:"([^"\n]+)"|'([^'\n]+)'|(\S+))\s+(?:"([^"\n]+)"|'([^'\n]+)'|(\S+))$/u,
|
|
1764
|
+
const change = planStatusLine(settings, { hudPath });
|
|
1765
|
+
if (change === "noop") return false;
|
|
1766
|
+
if (change === "skip") {
|
|
1767
|
+
warn(
|
|
1768
|
+
"기존 statusLine 유지: Triflux HUD를 쓰려면 settings.json에서 직접 선택하세요.",
|
|
1703
1769
|
);
|
|
1704
|
-
|
|
1705
|
-
const script = tokens?.[4] ?? tokens?.[5] ?? tokens?.[6];
|
|
1706
|
-
if (
|
|
1707
|
-
current.type !== "command" ||
|
|
1708
|
-
!/(?:^|[/\\])node(?:\.exe)?$/u.test(node) ||
|
|
1709
|
-
script !== hudPath.replace(/\\/g, "/")
|
|
1710
|
-
) {
|
|
1711
|
-
warn(
|
|
1712
|
-
"기존 statusLine 유지: Triflux HUD를 쓰려면 settings.json에서 직접 선택하세요.",
|
|
1713
|
-
);
|
|
1714
|
-
return false;
|
|
1715
|
-
}
|
|
1770
|
+
return false;
|
|
1716
1771
|
}
|
|
1717
1772
|
settings.statusLine = {
|
|
1718
|
-
...
|
|
1773
|
+
...settings.statusLine,
|
|
1719
1774
|
type: "command",
|
|
1720
|
-
command:
|
|
1775
|
+
command: buildStatusLineCommand(hudPath),
|
|
1721
1776
|
};
|
|
1722
1777
|
return true;
|
|
1723
1778
|
}
|
|
@@ -21,19 +21,22 @@ host capability evidence를 판정한다. 정책 표, keyword 표, owner 매트
|
|
|
21
21
|
2. git 메인 워크트리 — `$(git rev-parse --git-common-dir)`의 부모 아래
|
|
22
22
|
`.claude/rules/tfx-routing.md`
|
|
23
23
|
3. cwd에서 위로 올라가며 `.claude/rules/tfx-routing.md`
|
|
24
|
+
4. 이 스킬 디렉터리의 `references/tfx-routing.md` (`tfx setup`이 패키지에서 복사한 사본)
|
|
25
|
+
|
|
26
|
+
4순위는 clone이 없는 기기를 위한 것이다. 설치한 버전의 정책이라 clone보다 늦을 수 있으므로
|
|
27
|
+
1~3순위 뒤에 둔다.
|
|
24
28
|
|
|
25
29
|
2순위에 `--show-toplevel`을 쓰지 않는다. linked worktree 안에서는 worktree 루트를
|
|
26
30
|
반환하는데 그곳의 SSOT는 브랜치 분기 시점 사본이라, blocked 없이 stale 정책으로
|
|
27
31
|
조용히 오판정한다. `--git-common-dir`은 worktree 안에서도 메인 repo의 `.git`을 가리킨다.
|
|
28
32
|
|
|
29
|
-
blocked를 반환할 때는 해소 방법을 한 줄 덧붙인다:
|
|
30
|
-
`export TFX_ROUTING_SSOT=<triflux clone>/.claude/rules/tfx-routing.md`
|
|
31
|
-
|
|
32
|
-
워커·툴 셸에서는 1순위가 통째로 죽는다.)
|
|
33
|
+
blocked를 반환할 때는 해소 방법을 한 줄 덧붙인다: `tfx setup`을 다시 실행해 4순위 사본을 설치한다.
|
|
34
|
+
clone의 최신 정책을 쓰려면 `export TFX_ROUTING_SSOT=<triflux clone>/.claude/rules/tfx-routing.md`
|
|
35
|
+
를 `~/.zshenv`에 둔다. `~/.zshrc`는 interactive 전용이라 훅·headless 워커·툴 셸에서는 읽히지 않는다.
|
|
33
36
|
|
|
34
37
|
`~/.claude/rules/`에 사본이나 symlink를 두지 않는다. 그 디렉터리는 Claude가 모든 세션에
|
|
35
38
|
자동 로드하므로 tfx와 무관한 세션까지 SSOT 전문을 매번 컨텍스트에 싣게 된다.
|
|
36
|
-
npm
|
|
39
|
+
npm 패키지에는 `.claude/` 중 `rules/tfx-routing.md` 한 파일만 들어 있다.
|
|
37
40
|
|
|
38
41
|
반환 형식:
|
|
39
42
|
|