triflux 10.45.0 → 10.47.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/bin/tfx-live.mjs +576 -53
- package/config/routing-policy.json +1 -1
- package/hooks/session-start-fast.mjs +6 -0
- package/hub/dynamic-routing-engine.mjs +1 -1
- package/hub/lib/cto-env.mjs +2 -3
- package/hub/server.mjs +2 -51
- package/hub/team/recovery-store.mjs +1 -1
- package/hub/team/retry-state-machine.mjs +9 -1
- package/hub/team/synapse-registry.mjs +14 -11
- package/hub/team/uds-orchestrator.mjs +384 -83
- package/hub/team/worktree-lifecycle.mjs +8 -48
- package/hub/tray-lifecycle.mjs +1 -1
- package/hub/workers/codex-mcp.mjs +1 -1
- package/hud/providers/codex.mjs +91 -8
- package/hud/providers/cto.mjs +3 -0
- package/package.json +1 -1
- package/scripts/__tests__/release-governance.test.mjs +235 -11
- package/scripts/__tests__/release-publish-ancestor.test.mjs +67 -0
- package/scripts/__tests__/release-verify-wait.test.mjs +169 -0
- package/scripts/__tests__/resolve-auto-release.test.mjs +216 -0
- package/scripts/__tests__/tfx-route-bash-node-parity.test.mjs +1 -1
- package/scripts/__tests__/tfx-route-node-entry.test.mjs +4 -4
- package/scripts/lib/agent-route-policy.mjs +18 -17
- package/scripts/lib/codex-profile-config.mjs +19 -5
- package/scripts/release/publish.mjs +16 -1
- package/scripts/release/resolve-auto-release.mjs +136 -0
- package/scripts/release/verify.mjs +74 -25
- package/scripts/setup.mjs +20 -7
- package/scripts/tfx-route.sh +27 -24
- package/skills/tfx-auto/SKILL.md +5 -4
- package/skills/tfx-live/SKILL.md +112 -7
- package/skills/tfx-profile/SKILL.md +9 -7
- package/skills/tfx-setup/SKILL.md +1 -1
- package/skills/tfx-ship/SKILL.md +43 -36
- package/tui/codex-profile.mjs +2 -3
- package/tui/setup.mjs +4 -3
- package/hub/team/git-preflight.mjs +0 -286
|
@@ -45,7 +45,7 @@ options:
|
|
|
45
45
|
```
|
|
46
46
|
| 프로파일 | 모델 | Effort |
|
|
47
47
|
|----------|------|--------|
|
|
48
|
-
|
|
|
48
|
+
| gpt6_sol_high | 프로필 설정의 현재 모델 | high |
|
|
49
49
|
| ... | ... | ... |
|
|
50
50
|
```
|
|
51
51
|
|
|
@@ -75,9 +75,10 @@ options:
|
|
|
75
75
|
2. AskUserQuestion으로 프로필/alias 선택:
|
|
76
76
|
```
|
|
77
77
|
options:
|
|
78
|
-
- label: "
|
|
79
|
-
- label: "
|
|
80
|
-
- label: "
|
|
78
|
+
- label: "gpt6_sol_high" → 구현과 리뷰 기본값 (Recommended)
|
|
79
|
+
- label: "gpt6_sol_med" → 정리와 리팩터 균형형
|
|
80
|
+
- label: "gpt6_luna_high" → 명확한 반복 작업
|
|
81
|
+
- label: "gpt6_luna_low" → 지연 우선 경량
|
|
81
82
|
- label: "gpt6_astra_xhigh" → 고강도 추론
|
|
82
83
|
- label: "gpt6_astra_max" → 최난도 단일 작업
|
|
83
84
|
- label: "gpt6_astra_ultra" → 최상위 자동 위임 (비중첩 전용)
|
|
@@ -187,12 +188,13 @@ options:
|
|
|
187
188
|
|
|
188
189
|
| 프로필 | 용도 |
|
|
189
190
|
|------|------|
|
|
190
|
-
|
|
|
191
|
+
| gpt6_sol_high | 구현과 리뷰 기본값 |
|
|
191
192
|
| gpt6_astra_xhigh | 고강도 추론 |
|
|
192
193
|
| gpt6_astra_max | 최난도 단일 작업 |
|
|
193
194
|
| gpt6_astra_ultra | 최상위 자동 위임 (비중첩 전용) |
|
|
194
|
-
|
|
|
195
|
-
|
|
|
195
|
+
| gpt6_sol_med | 정리와 리팩터 균형형 |
|
|
196
|
+
| gpt6_luna_high | 명확한 반복 작업 |
|
|
197
|
+
| gpt6_luna_low | 지연 우선 경량 |
|
|
196
198
|
| custom | 사용자가 관리하는 프로필 |
|
|
197
199
|
|
|
198
200
|
### Codex Effort 레벨
|
|
@@ -112,7 +112,7 @@ Bash("triflux setup")
|
|
|
112
112
|
#### 단계 3: Codex 프로파일
|
|
113
113
|
|
|
114
114
|
`~/.codex/config.toml`을 Read 도구로 읽어 필수 프로파일 존재 여부 확인.
|
|
115
|
-
필수: `gpt6_astra_ultra`, `gpt6_astra_max`, `gpt6_astra_xhigh`, `
|
|
115
|
+
필수: `gpt6_astra_ultra`, `gpt6_astra_max`, `gpt6_astra_xhigh`, `gpt6_sol_high`, `gpt6_sol_med`, `gpt6_luna_high`, `gpt6_luna_low`.
|
|
116
116
|
|
|
117
117
|
- 모두 존재 → ✅ 표시
|
|
118
118
|
- 누락 있으면 → AskUserQuestion:
|
package/skills/tfx-ship/SKILL.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: tfx-ship
|
|
3
3
|
description: >
|
|
4
|
-
triflux 전용 릴리즈 자동화. **GitHub Actions(release.yml dispatch / npm-publish.yml) 기반 CI 릴리즈가 기본 경로
|
|
4
|
+
triflux 전용 릴리즈 자동화. **GitHub Actions(버전 증가 PR 머지 / release.yml dispatch / npm-publish.yml) 기반 CI 릴리즈가 기본 경로 -
|
|
5
5
|
npm publish 는 OIDC Trusted Publishing 으로 CI 가 수행하므로 로컬 npm login 불필요.** scripts/release/* 래퍼 +
|
|
6
6
|
AskUserQuestion 기반 버전 선택 + CHANGELOG 편집 게이트 + Co-Authored-By/AI trailer 금지 강제. 'ship', '배포', '릴리즈',
|
|
7
7
|
'release', 'tfx-ship', 'publish' 같은 요청에 반드시 사용.
|
|
8
8
|
argument-hint: "[patch|minor|major|<version>] [--skip-tests] [--no-publish] [--dry-run]"
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
# tfx-ship
|
|
11
|
+
# tfx-ship - triflux 릴리즈 자동화
|
|
12
12
|
|
|
13
13
|
> **ARGUMENTS 처리**: ARGUMENTS 가 있으면 첫 토큰을 version bump 타입 또는 명시 버전으로 해석.
|
|
14
14
|
> 플래그는 `--skip-tests`, `--no-publish`, `--dry-run` 지원.
|
|
@@ -28,11 +28,11 @@ triflux 는 아래 3채널로 동시 배포. 각 채널의 버전은 반드시
|
|
|
28
28
|
| # | 채널 | 명령 | 우선순위 |
|
|
29
29
|
|---|------|------|---------|
|
|
30
30
|
| 1 | **GitHub Releases** | `gh release create vX.Y.Z --notes-file <notes>` | 공지 + changelog 공식 소스 |
|
|
31
|
-
| 2 | **npm registry** | **CI `npm-publish.yml`** (`v*` 태그 push 자동 / OIDC Trusted Publishing
|
|
31
|
+
| 2 | **npm registry** | **CI `npm-publish.yml`** (`v*` 태그 push 자동 / OIDC Trusted Publishing) | primary distribution |
|
|
32
32
|
| 3 | **Claude Code marketplace** | `.claude-plugin/marketplace.json` (`source: npm` 참조) | `claude plugin add triflux` |
|
|
33
33
|
| 4 | **pypi** (future, 비활성) | 현재 `pyproject.toml` 없음 | 활성화 시 Step 10.5 신설 |
|
|
34
34
|
|
|
35
|
-
- **npm publish 는 로컬에서 하지 않는다**
|
|
35
|
+
- **npm publish 는 로컬에서 하지 않는다** - 버전을 올린 PR 머지 후 ci 성공, `release.yml` dispatch 또는 `v*` 태그 push 시 `npm-publish.yml` 이 OIDC 로 발행한다. 로컬 `publish.mjs --execute` 는 CI 불가 시 폴백 전용.
|
|
36
36
|
- marketplace 는 자체 publish 명령이 없음. marketplace.json 의 version 만 갱신하면 git push 로 반영됨 (GitHub 호스팅).
|
|
37
37
|
- `release:check-sync` 가 package.json + marketplace.json + package-lock.json 3곳 version 일치를 강제한다.
|
|
38
38
|
- pypi 는 triflux 가 Python 모듈을 가지게 되면 활성화. 현 단계는 플레이스홀더.
|
|
@@ -42,19 +42,26 @@ triflux 는 아래 3채널로 동시 배포. 각 채널의 버전은 반드시
|
|
|
42
42
|
## 전제 조건
|
|
43
43
|
|
|
44
44
|
- `~/.claude/scripts/tfx-route.sh` 불필요 (CLI 워커 호출 없음)
|
|
45
|
-
- `gh` CLI 인증됨 (`gh auth status`)
|
|
46
|
-
- **npm 인증 불필요**
|
|
47
|
-
- 릴리즈할
|
|
45
|
+
- `gh` CLI 인증됨 (`gh auth status`) - `release.yml` dispatch / GitHub release 에 필요. 깨졌으면 웹 UI 대체
|
|
46
|
+
- **npm 인증 불필요** - npm publish 는 CI(`npm-publish.yml`)가 OIDC 로 수행 (로컬 `npm login` 필요 없음)
|
|
47
|
+
- 릴리즈할 커밋이 `origin/main` 에 포함되어 있을 것. 자동 경로는 ci 의 head_sha, 수동 경로는 dispatch 시점의 github.sha 를 checkout 한다.
|
|
48
48
|
|
|
49
|
-
## 기본 경로
|
|
49
|
+
## 기본 경로 - CI 릴리즈 (권장)
|
|
50
|
+
|
|
51
|
+
PR 에서 `npm run release:bump -- --version X --write` 로 버전을 올려 main 에 머지하면,
|
|
52
|
+
같은 커밋의 ci 통과 뒤 `release.yml` 이 자동으로 실행된다. 버전을 안 올린 머지는 릴리즈하지 않는다.
|
|
53
|
+
자동 경로는 해당 커밋과 첫 번째 부모의 package.json 버전을 비교한다. 버전이 같거나 이미 태그가 있으면 건너뛰고, prerelease 는 수동 dispatch 로 진행한다.
|
|
54
|
+
main 이 앞서 있어도 해당 커밋이 origin/main 의 조상이면 릴리즈한다. 강제 푸시로 main 에서 사라진 커밋은 건너뛰고, 최신 안정 태그 이하로 버전이 역행하면 실패한다.
|
|
55
|
+
`resolve` 잡은 동시성 제한 없이 판정하고, 실제 `release` 잡만 `release` concurrency 그룹에서 직렬 실행한다. 자동 경로만 `publish.mjs --allow-ancestor` 를 사용하며 수동 경로는 기존 HEAD 동등성 검사를 유지한다.
|
|
56
|
+
버전을 올린 커밋 두 개가 거의 동시에 들어오면 앞의 대기 릴리즈가 취소될 수 있다. 해당 커밋의 ci 를 Re-run 하거나 `release.yml` 을 수동 dispatch 해 복구한다.
|
|
50
57
|
|
|
51
58
|
> **npm publish 는 로컬에서 하지 않는다.** GitHub Actions 가 OIDC(Trusted Publishing)로 발행한다.
|
|
52
59
|
> 워크플로우: `.github/workflows/{release,npm-publish,ci}.yml`
|
|
53
60
|
|
|
54
|
-
- **`release.yml`** (`workflow_dispatch`, inputs: `version`, `channel`):
|
|
55
|
-
- **`npm-publish.yml`** (`on: push tags ['v*']` + dispatch): `v*` 태그가 push 되면 자동 npm publish (`--provenance --access public`,
|
|
61
|
+
- **`release.yml`** (`workflow_run` + `workflow_dispatch`, inputs: `version`, `channel`): prepare → 태그 + GitHub release(`publish.mjs --skip-npm`) → 태그 ref 와 channel 에 따른 npm_tag(stable=latest, canary=canary)로 `npm-publish.yml` dispatch → 반환 URL의 run ID로 npm publish 완료 대기 → `verify.mjs` 까지 CI(ubuntu, node 24)에서 수행한다. 자동 경로는 같은 커밋의 ci 가 전체 테스트를 통과했으므로 prepare 에서 테스트만 생략한다. 수동 dispatch 는 기존대로 테스트한다. npm 레지스트리 반영은 20초 간격으로 최대 900초 기다리고, 기대 버전이 아직 보이지 않으면 pending 경고를 남기고 통과한다. 2026-09-23 게시에서 반영까지 `@triflux/core` 3분, `triflux` 1시간 25분, `@triflux/remote` 11시간이 걸렸다. 구버전 gh 가 실행 URL을 반환하지 않으면 dispatch 직전 저장한 기존 실행 ID를 제외해 새 실행을 고른다.
|
|
62
|
+
- **`npm-publish.yml`** (`on: push tags ['v*']` + dispatch): `v*` 태그가 push 되면 자동 npm publish (`--provenance --access public --tag "$NPM_TAG"`, OIDC 전용). dispatch 의 선택 입력 `npm_tag` 가 있으면 사용하고, 없으면 버전에 `-` 가 있을 때 canary, 그 밖에는 latest 로 게시한다. latest 와 canary 외의 값은 거부한다. `@triflux/core`, `@triflux/remote`, `triflux` 3패키지 각각, 이미 게시된 버전은 skip. 세 패키지 모두 Trusted Publisher 등록이 필요하며 `npm trust list <패키지>` 로 확인한다.
|
|
56
63
|
|
|
57
|
-
###
|
|
64
|
+
### 수동 실행 - release.yml 디스패치
|
|
58
65
|
|
|
59
66
|
```bash
|
|
60
67
|
gh workflow run release.yml --ref main -f version=<X.Y.Z> -f channel=stable
|
|
@@ -66,7 +73,7 @@ gh run watch "$(gh run list --workflow release.yml -L1 --json databaseId -q '.[0
|
|
|
66
73
|
- gh 인증이 없는 환경(예: SSH→m2, hosts.yml PAT 만료)이면 **gh 가 정상인 머신(m5)에서** dispatch 하거나 **GitHub 웹 UI**(Actions → release → Run workflow)로 대체.
|
|
67
74
|
- CHANGELOG 가 필요하면 dispatch 전에 main 에 별도 커밋으로 반영(아래 로컬 플로우 Step 4 양식 참조).
|
|
68
75
|
|
|
69
|
-
### 경량 경로
|
|
76
|
+
### 경량 경로 - 태그만 push
|
|
70
77
|
|
|
71
78
|
이미 검증된 hotfix 등 prepare/verify 없이 npm publish 만 트리거하려면:
|
|
72
79
|
|
|
@@ -79,12 +86,12 @@ git tag v<X.Y.Z> && git push origin v<X.Y.Z> # → npm-publish.yml 자동 발
|
|
|
79
86
|
|
|
80
87
|
---
|
|
81
88
|
|
|
82
|
-
## 로컬 수동 플로우 (폴백
|
|
89
|
+
## 로컬 수동 플로우 (폴백 - CI 불가 시에만)
|
|
83
90
|
|
|
84
91
|
> CI 가 정석이다. 아래는 Actions 가 막혔거나 로컬 디버깅 시의 수동 절차다.
|
|
85
92
|
> **Step 10 의 로컬 `npm publish` 는 CI 와 중복이므로 평상시 실행 금지.**
|
|
86
93
|
|
|
87
|
-
### Step 0
|
|
94
|
+
### Step 0 - 환경 확인
|
|
88
95
|
|
|
89
96
|
```bash
|
|
90
97
|
cd "$REPO_ROOT"
|
|
@@ -100,7 +107,7 @@ node --version # Node 18+ 확인
|
|
|
100
107
|
- uncommitted 있음 → **AskUserQuestion**: "먼저 커밋할까? / ship 에 포함 / 취소"
|
|
101
108
|
- behind origin → `git pull origin main` 먼저 실행
|
|
102
109
|
|
|
103
|
-
### Step 1
|
|
110
|
+
### Step 1 - 버전 선택
|
|
104
111
|
|
|
105
112
|
ARGUMENTS 에 version 있으면 그 값 사용. 없으면 AskUserQuestion:
|
|
106
113
|
|
|
@@ -108,15 +115,15 @@ ARGUMENTS 에 version 있으면 그 값 사용. 없으면 AskUserQuestion:
|
|
|
108
115
|
현재: v{CURRENT}
|
|
109
116
|
어떤 버전으로 올릴까요?
|
|
110
117
|
|
|
111
|
-
A) patch (v{CURRENT +0.0.1})
|
|
112
|
-
B) minor (v{CURRENT +0.1.0})
|
|
113
|
-
C) major (v{CURRENT +1.0.0})
|
|
114
|
-
D) custom
|
|
118
|
+
A) patch (v{CURRENT +0.0.1}) - bug fix / small refactor
|
|
119
|
+
B) minor (v{CURRENT +0.1.0}) - 새 기능, backward compatible
|
|
120
|
+
C) major (v{CURRENT +1.0.0}) - breaking change
|
|
121
|
+
D) custom - 직접 입력
|
|
115
122
|
```
|
|
116
123
|
|
|
117
124
|
선택된 버전을 `TARGET_VERSION` 으로 저장.
|
|
118
125
|
|
|
119
|
-
### Step 2
|
|
126
|
+
### Step 2 - 버전 동기화 사전 체크
|
|
120
127
|
|
|
121
128
|
```bash
|
|
122
129
|
npm run release:check-sync
|
|
@@ -126,7 +133,7 @@ npm run release:check-sync
|
|
|
126
133
|
- 자동 수정 제안: `npm run release:check-sync:fix`
|
|
127
134
|
- 사용자 승인 후 실행
|
|
128
135
|
|
|
129
|
-
### Step 3
|
|
136
|
+
### Step 3 - 버전 bump
|
|
130
137
|
|
|
131
138
|
```bash
|
|
132
139
|
npm run release:bump -- --version "$TARGET_VERSION" --write
|
|
@@ -136,7 +143,7 @@ npm run release:bump -- --version "$TARGET_VERSION" --write
|
|
|
136
143
|
|
|
137
144
|
> **MANDATORY `--write`**: 플래그 없으면 dry-run 만 동작 (`scripts/release/bump-version.mjs:28` `if (write)` 분기). `--write` 누락 시 후속 Step 이 모두 이전 버전으로 진행되는 silent failure 가 된다.
|
|
138
145
|
|
|
139
|
-
### Step 4
|
|
146
|
+
### Step 4 - CHANGELOG 초안 생성
|
|
140
147
|
|
|
141
148
|
`git log --oneline <previous-tag>..HEAD` 로 범위 커밋 나열 후, Claude 가 아래 섹션 구조로 초안 작성:
|
|
142
149
|
|
|
@@ -166,7 +173,7 @@ B) 편집하고 계속 (사용자가 에디터에서 수정 후 진행)
|
|
|
166
173
|
C) 취소
|
|
167
174
|
```
|
|
168
175
|
|
|
169
|
-
### Step 5
|
|
176
|
+
### Step 5 - 테스트 + 빌드 검증
|
|
170
177
|
|
|
171
178
|
```bash
|
|
172
179
|
npm run release:prepare -- --execute --version "$TARGET_VERSION" --allow-dirty
|
|
@@ -186,7 +193,7 @@ npm run release:prepare -- --execute --version "$TARGET_VERSION" --allow-dirty
|
|
|
186
193
|
|
|
187
194
|
실패 시 → **STOP**. 사용자에게 에러 보여주고 재시도 옵션 제공.
|
|
188
195
|
|
|
189
|
-
### Step 6
|
|
196
|
+
### Step 6 - pack.mjs 미러
|
|
190
197
|
|
|
191
198
|
```bash
|
|
192
199
|
node scripts/pack.mjs all
|
|
@@ -197,7 +204,7 @@ node scripts/pack.mjs all
|
|
|
197
204
|
- `git status` 로 실제 변경 파일만 선별 스테이징
|
|
198
205
|
- 예: `git add packages/triflux/` (구체 경로 지정)
|
|
199
206
|
|
|
200
|
-
### Step 7
|
|
207
|
+
### Step 7 - Commit + Tag
|
|
201
208
|
|
|
202
209
|
커밋 메시지 (절대 Co-Authored-By 등 포함 금지):
|
|
203
210
|
|
|
@@ -211,7 +218,7 @@ git tag "v${TARGET_VERSION}"
|
|
|
211
218
|
|
|
212
219
|
`.gitmessage` 템플릿이 AI trailer 를 자동 주입하는 경우 제거. `--no-edit` 으로 추가 편집 차단.
|
|
213
220
|
|
|
214
|
-
### Step 8
|
|
221
|
+
### Step 8 - Push
|
|
215
222
|
|
|
216
223
|
**AskUserQuestion** (릴리즈 전 마지막 확인):
|
|
217
224
|
|
|
@@ -220,7 +227,7 @@ git tag "v${TARGET_VERSION}"
|
|
|
220
227
|
|
|
221
228
|
A) git push origin main --tags (권장)
|
|
222
229
|
B) tag 없이 push (수동 tag 후처리)
|
|
223
|
-
C) 중단
|
|
230
|
+
C) 중단 - 로컬에만 유지
|
|
224
231
|
```
|
|
225
232
|
|
|
226
233
|
선택 A 시:
|
|
@@ -229,7 +236,7 @@ git push origin main
|
|
|
229
236
|
git push origin "v${TARGET_VERSION}"
|
|
230
237
|
```
|
|
231
238
|
|
|
232
|
-
### Step 9
|
|
239
|
+
### Step 9 - GitHub Release
|
|
233
240
|
|
|
234
241
|
```bash
|
|
235
242
|
NOTES_PATH=".omx/plans/release-notes-v${TARGET_VERSION}.md"
|
|
@@ -242,7 +249,7 @@ gh release create "v${TARGET_VERSION}" \
|
|
|
242
249
|
- 노트 본문 검증: Co-Authored-By / AI trailer 포함됐는지 grep 후 제거
|
|
243
250
|
- `--draft` 로 초안 생성 후 수동 publish 도 가능 (안전 모드)
|
|
244
251
|
|
|
245
|
-
### Step 10
|
|
252
|
+
### Step 10 - npm publish (= CI 가 수행, 로컬 금지가 기본)
|
|
246
253
|
|
|
247
254
|
> **평상시 로컬에서 `npm publish` 하지 않는다.** Step 8 에서 `git push origin v<X.Y.Z>` 로 태그가 올라가면 `npm-publish.yml` 이 OIDC 로 자동 발행한다.
|
|
248
255
|
> CI run 관찰: `gh run list --workflow npm-publish.yml -L1` → `gh run watch <id>`
|
|
@@ -250,10 +257,10 @@ gh release create "v${TARGET_VERSION}" \
|
|
|
250
257
|
CI 가 완전히 불가능한 비상시에만, npm 인증을 갖춘 환경에서 수동 폴백:
|
|
251
258
|
|
|
252
259
|
```bash
|
|
253
|
-
node scripts/release/publish.mjs --execute # 비상 폴백 전용
|
|
260
|
+
node scripts/release/publish.mjs --execute # 비상 폴백 전용 - 중복 publish 주의
|
|
254
261
|
```
|
|
255
262
|
|
|
256
|
-
### Step 10.5
|
|
263
|
+
### Step 10.5 - pypi publish (future, 현재 비활성)
|
|
257
264
|
|
|
258
265
|
triflux 가 Python 모듈을 가지게 되면 이 단계를 활성화:
|
|
259
266
|
|
|
@@ -267,7 +274,7 @@ fi
|
|
|
267
274
|
|
|
268
275
|
현 단계는 `pyproject.toml` 없음 → skip. 플레이스홀더만 유지.
|
|
269
276
|
|
|
270
|
-
### Step 11
|
|
277
|
+
### Step 11 - 사후 검증 (3채널 전부)
|
|
271
278
|
|
|
272
279
|
```bash
|
|
273
280
|
npm run release:verify
|
|
@@ -280,7 +287,7 @@ npm run release:verify
|
|
|
280
287
|
- 릴리즈 노트 공개됨 (`gh release view v${TARGET_VERSION}`)
|
|
281
288
|
- (future) pypi 는 pyproject.toml 있을 때만 체크
|
|
282
289
|
|
|
283
|
-
### Step 12
|
|
290
|
+
### Step 12 - 사용자 알림
|
|
284
291
|
|
|
285
292
|
```
|
|
286
293
|
RELEASE COMPLETE ✓
|
|
@@ -300,7 +307,7 @@ github: https://github.com/tellang/triflux/releases/tag/v${TARGET_VERSION}
|
|
|
300
307
|
| 단계 | 에러 | 복구 |
|
|
301
308
|
|------|------|------|
|
|
302
309
|
| Step 0 | 브랜치가 main 아님 | `git checkout main` 또는 PR 플로우 |
|
|
303
|
-
| Step 0 | unpushed 커밋 있음 (ship 전 확인) | 정상
|
|
310
|
+
| Step 0 | unpushed 커밋 있음 (ship 전 확인) | 정상 - 릴리즈 대상 |
|
|
304
311
|
| Step 0 | uncommitted 변경 | 먼저 커밋 / ship 에 흡수 / 취소 선택 |
|
|
305
312
|
| Step 2 | 버전 동기화 실패 | `release:check-sync --fix` 실행 |
|
|
306
313
|
| Step 5 | 테스트 실패 | 수정 후 재시도. `--skip-tests` 는 위험 |
|
|
@@ -308,7 +315,7 @@ github: https://github.com/tellang/triflux/releases/tag/v${TARGET_VERSION}
|
|
|
308
315
|
| Step 7 | commit 메시지에 AI trailer 감지 | 하드 차단 + 재작성 요청 |
|
|
309
316
|
| Step 8 | push 거부 (remote 변경됨) | `git pull --rebase origin main` 후 재시도 |
|
|
310
317
|
| Step 9 | gh release create 실패 | `gh auth status` 확인, 수동 재시도 |
|
|
311
|
-
| Step 10 | npm publish(CI) 실패 | `gh run view <id> --log-failed` 로 `npm-publish.yml` 로그 확인.
|
|
318
|
+
| Step 10 | npm publish(CI) 실패 | `gh run view <id> --log-failed` 로 `npm-publish.yml` 로그 확인. 세 패키지의 OIDC Trusted Publisher 설정 점검 |
|
|
312
319
|
|
|
313
320
|
## 플래그
|
|
314
321
|
|
|
@@ -345,5 +352,5 @@ github: https://github.com/tellang/triflux/releases/tag/v${TARGET_VERSION}
|
|
|
345
352
|
- 버전 불일치: `npm run release:check-sync --fix`
|
|
346
353
|
- pack CRLF 경고: 실제 변경 파일만 선별 `git add packages/triflux/...`
|
|
347
354
|
- gh CLI 미인증: `gh auth login` (또는 gh 정상인 머신/웹 UI 로 release.yml dispatch)
|
|
348
|
-
- npm publish 는 CI(`npm-publish.yml`, OIDC)가 수행
|
|
355
|
+
- npm publish 는 CI(`npm-publish.yml`, OIDC)가 수행 - 로컬 `npm login` 불필요. CI 실패 시 `gh run view <id> --log-failed`
|
|
349
356
|
- prepare.mjs stall: `scripts/release/prepare.mjs` 가 `stdio: ["ignore","pipe","pipe"]` + 10분 timeout 적용됨 (v10.9.32 fix 739da2d)
|
package/tui/codex-profile.mjs
CHANGED
|
@@ -41,9 +41,8 @@ const CONFIG_PATH = join(CODEX_DIR, "config.toml");
|
|
|
41
41
|
|
|
42
42
|
const KNOWN_MODELS = [
|
|
43
43
|
{ label: "gpt-6-astra", hint: "프런티어" },
|
|
44
|
-
{ label: "gpt-
|
|
45
|
-
{ label: "gpt-
|
|
46
|
-
{ label: "gpt-5.6-luna", hint: "경량" },
|
|
44
|
+
{ label: "gpt-6-sol", hint: "범용" },
|
|
45
|
+
{ label: "gpt-6-luna", hint: "경량" },
|
|
47
46
|
{ label: "직접 입력", hint: "" },
|
|
48
47
|
];
|
|
49
48
|
|
package/tui/setup.mjs
CHANGED
|
@@ -39,9 +39,10 @@ const REQUIRED_CODEX_PROFILE_NAMES = [
|
|
|
39
39
|
"gpt6_astra_ultra",
|
|
40
40
|
"gpt6_astra_max",
|
|
41
41
|
"gpt6_astra_xhigh",
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
42
|
+
"gpt6_sol_high",
|
|
43
|
+
"gpt6_sol_med",
|
|
44
|
+
"gpt6_luna_high",
|
|
45
|
+
"gpt6_luna_low",
|
|
45
46
|
];
|
|
46
47
|
|
|
47
48
|
const STABLE_NODE_COMMAND_CANDIDATES = Object.freeze([
|
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
// hub/team/git-preflight.mjs — Pre-flight safety check for dangerous git ops.
|
|
2
|
-
// Blocks checkout/rebase/cherry-pick/reset/stash-pop/worktree-remove when they
|
|
3
|
-
// would conflict with other active Synapse sessions' dirty files or claimed
|
|
4
|
-
// leases. Fail-open by default: if registry/locks are unavailable, allow the op
|
|
5
|
-
// with a warning (worktree isolation is the baseline safety net).
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Factory for git pre-flight check.
|
|
9
|
-
*
|
|
10
|
-
* @param {object} opts
|
|
11
|
-
* @param {{ getActive: () => Array<object> }} opts.registry — synapse-registry duck type
|
|
12
|
-
* @param {{ snapshot: () => Array<object> }} opts.locks — swarm-locks duck type
|
|
13
|
-
* @param {boolean} [opts.failOpen=true] — allow op when registry/locks throw or are missing
|
|
14
|
-
* @param {(level: string, msg: string, data?: object) => void} [opts.logger] — optional logger
|
|
15
|
-
* @returns {{
|
|
16
|
-
* check: Function,
|
|
17
|
-
* checkRebase: Function,
|
|
18
|
-
* checkCheckout: Function,
|
|
19
|
-
* checkCherryPick: Function,
|
|
20
|
-
* checkReset: Function,
|
|
21
|
-
* checkStashPop: Function,
|
|
22
|
-
* checkWorktreeRemove: Function,
|
|
23
|
-
* }}
|
|
24
|
-
*/
|
|
25
|
-
export function createGitPreflight(opts = {}) {
|
|
26
|
-
const { registry, locks, failOpen = true, logger = null } = opts;
|
|
27
|
-
|
|
28
|
-
function log(level, msg, data) {
|
|
29
|
-
if (logger) {
|
|
30
|
-
try {
|
|
31
|
-
logger(level, msg, data);
|
|
32
|
-
} catch {
|
|
33
|
-
/* logger failure is non-fatal */
|
|
34
|
-
}
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
if (level === "warn") {
|
|
38
|
-
console.warn(`[git-preflight] ${msg}`, data ?? "");
|
|
39
|
-
} else if (level === "error") {
|
|
40
|
-
console.error(`[git-preflight] ${msg}`, data ?? "");
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// ── helpers ──────────────────────────────────────────────
|
|
45
|
-
|
|
46
|
-
function safeGetActive() {
|
|
47
|
-
try {
|
|
48
|
-
const value = registry?.getActive?.();
|
|
49
|
-
if (!Array.isArray(value)) return null;
|
|
50
|
-
return value;
|
|
51
|
-
} catch (err) {
|
|
52
|
-
log("warn", "registry.getActive threw", { error: err?.message });
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function safeSnapshot() {
|
|
58
|
-
try {
|
|
59
|
-
const value = locks?.snapshot?.();
|
|
60
|
-
if (!Array.isArray(value)) return null;
|
|
61
|
-
return value;
|
|
62
|
-
} catch (err) {
|
|
63
|
-
log("warn", "locks.snapshot threw", { error: err?.message });
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function failOpenDecision(reason) {
|
|
69
|
-
log("warn", "hub unavailable, failing open", { reason });
|
|
70
|
-
return { allowed: true, reason: "hub_unavailable_fail_open" };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function normalizeFiles(list) {
|
|
74
|
-
if (!Array.isArray(list)) return new Set();
|
|
75
|
-
return new Set(
|
|
76
|
-
list
|
|
77
|
-
.filter((f) => typeof f === "string" && f.length > 0)
|
|
78
|
-
.map((f) => f.replace(/\\/g, "/")),
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function otherActiveSessions(active, sessionId) {
|
|
83
|
-
return active.filter((s) => s?.sessionId && s.sessionId !== sessionId);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function otherLeases(snapshot, workerId) {
|
|
87
|
-
return snapshot.filter(
|
|
88
|
-
(entry) => entry?.workerId && entry.workerId !== workerId,
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function buildConflict(file, session, holder) {
|
|
93
|
-
return {
|
|
94
|
-
file,
|
|
95
|
-
activeSession: session?.sessionId ?? holder ?? "unknown",
|
|
96
|
-
activeTask: session?.taskSummary ?? "",
|
|
97
|
-
...(holder ? { leaseHolder: holder } : {}),
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function recommend(conflicts) {
|
|
102
|
-
if (!conflicts.length) return "";
|
|
103
|
-
const first = conflicts[0];
|
|
104
|
-
const owner = first.leaseHolder || first.activeSession;
|
|
105
|
-
const task = first.activeTask ? ` Active task: '${first.activeTask}'` : "";
|
|
106
|
-
return `Wait for '${owner}' to finish, or coordinate via HITL.${task}`;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function blockedDecision(op, conflicts, extraReason) {
|
|
110
|
-
return {
|
|
111
|
-
allowed: false,
|
|
112
|
-
reason: extraReason || "overlap_with_active_session",
|
|
113
|
-
conflicts,
|
|
114
|
-
recommendation: recommend(conflicts),
|
|
115
|
-
op,
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function allowedDecision() {
|
|
120
|
-
return { allowed: true };
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// ── shared scanners ─────────────────────────────────────
|
|
124
|
-
|
|
125
|
-
function findDirtyFileConflicts(sessionId, workerId) {
|
|
126
|
-
const active = safeGetActive();
|
|
127
|
-
const snapshot = safeSnapshot();
|
|
128
|
-
|
|
129
|
-
if (active == null || snapshot == null) {
|
|
130
|
-
return failOpen ? { fallOpen: true } : { fallOpen: false, conflicts: [] };
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const conflicts = [];
|
|
134
|
-
const seen = new Set();
|
|
135
|
-
|
|
136
|
-
for (const session of otherActiveSessions(active, sessionId)) {
|
|
137
|
-
const dirty = normalizeFiles(session.dirtyFiles);
|
|
138
|
-
for (const file of dirty) {
|
|
139
|
-
if (seen.has(file)) continue;
|
|
140
|
-
seen.add(file);
|
|
141
|
-
conflicts.push(buildConflict(file, session));
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
for (const lease of otherLeases(snapshot, workerId)) {
|
|
146
|
-
if (lease.leaseType && lease.leaseType !== "exclusive") continue;
|
|
147
|
-
const file = typeof lease.file === "string" ? lease.file : null;
|
|
148
|
-
if (!file || seen.has(file)) continue;
|
|
149
|
-
seen.add(file);
|
|
150
|
-
const sessionMeta = lease.sessionMeta || null;
|
|
151
|
-
conflicts.push(
|
|
152
|
-
buildConflict(
|
|
153
|
-
file,
|
|
154
|
-
sessionMeta
|
|
155
|
-
? {
|
|
156
|
-
sessionId: sessionMeta.sessionId || lease.workerId,
|
|
157
|
-
taskSummary: sessionMeta.taskSummary || "",
|
|
158
|
-
}
|
|
159
|
-
: null,
|
|
160
|
-
lease.workerId,
|
|
161
|
-
),
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return { conflicts };
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function findTargetFileConflicts(targetFiles, sessionId, workerId) {
|
|
169
|
-
const active = safeGetActive();
|
|
170
|
-
const snapshot = safeSnapshot();
|
|
171
|
-
|
|
172
|
-
if (active == null || snapshot == null) {
|
|
173
|
-
return failOpen ? { fallOpen: true } : { fallOpen: false, conflicts: [] };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const targets = normalizeFiles(targetFiles);
|
|
177
|
-
if (targets.size === 0) return { conflicts: [] };
|
|
178
|
-
|
|
179
|
-
const conflicts = [];
|
|
180
|
-
const seen = new Set();
|
|
181
|
-
|
|
182
|
-
for (const session of otherActiveSessions(active, sessionId)) {
|
|
183
|
-
const dirty = normalizeFiles(session.dirtyFiles);
|
|
184
|
-
for (const file of dirty) {
|
|
185
|
-
if (!targets.has(file) || seen.has(file)) continue;
|
|
186
|
-
seen.add(file);
|
|
187
|
-
conflicts.push(buildConflict(file, session));
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
for (const lease of otherLeases(snapshot, workerId)) {
|
|
192
|
-
if (lease.leaseType && lease.leaseType !== "exclusive") continue;
|
|
193
|
-
const file = typeof lease.file === "string" ? lease.file : null;
|
|
194
|
-
if (!file || !targets.has(file) || seen.has(file)) continue;
|
|
195
|
-
seen.add(file);
|
|
196
|
-
const sessionMeta = lease.sessionMeta || null;
|
|
197
|
-
conflicts.push(
|
|
198
|
-
buildConflict(
|
|
199
|
-
file,
|
|
200
|
-
sessionMeta
|
|
201
|
-
? {
|
|
202
|
-
sessionId: sessionMeta.sessionId || lease.workerId,
|
|
203
|
-
taskSummary: sessionMeta.taskSummary || "",
|
|
204
|
-
}
|
|
205
|
-
: null,
|
|
206
|
-
lease.workerId,
|
|
207
|
-
),
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return { conflicts };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// ── public API ──────────────────────────────────────────
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Core pre-flight check.
|
|
218
|
-
*
|
|
219
|
-
* @param {"checkout"|"rebase"|"cherry-pick"|"reset"|"stash-pop"|"worktree-remove"} op
|
|
220
|
-
* @param {{ targetFiles?: string[], branch?: string, ref?: string, worktreePath?: string }} args
|
|
221
|
-
* @param {{ sessionId: string, workerId?: string }} sessionContext
|
|
222
|
-
* @returns {{ allowed: boolean, reason?: string, conflicts?: object[], recommendation?: string, op?: string }}
|
|
223
|
-
*/
|
|
224
|
-
function check(op, args = {}, sessionContext = {}) {
|
|
225
|
-
const sessionId = sessionContext.sessionId || "";
|
|
226
|
-
const workerId = sessionContext.workerId || sessionId;
|
|
227
|
-
|
|
228
|
-
switch (op) {
|
|
229
|
-
case "checkout":
|
|
230
|
-
case "rebase":
|
|
231
|
-
case "reset":
|
|
232
|
-
case "stash-pop": {
|
|
233
|
-
const res = findDirtyFileConflicts(sessionId, workerId);
|
|
234
|
-
if (res.fallOpen) return failOpenDecision(op);
|
|
235
|
-
if (res.conflicts.length > 0) return blockedDecision(op, res.conflicts);
|
|
236
|
-
return allowedDecision();
|
|
237
|
-
}
|
|
238
|
-
case "cherry-pick": {
|
|
239
|
-
const targets = Array.isArray(args.targetFiles) ? args.targetFiles : [];
|
|
240
|
-
const res = findTargetFileConflicts(targets, sessionId, workerId);
|
|
241
|
-
if (res.fallOpen) return failOpenDecision(op);
|
|
242
|
-
if (res.conflicts.length > 0) return blockedDecision(op, res.conflicts);
|
|
243
|
-
return allowedDecision();
|
|
244
|
-
}
|
|
245
|
-
case "worktree-remove": {
|
|
246
|
-
const active = safeGetActive();
|
|
247
|
-
if (active == null) return failOpenDecision(op);
|
|
248
|
-
const target =
|
|
249
|
-
typeof args.worktreePath === "string"
|
|
250
|
-
? args.worktreePath.replace(/\\/g, "/")
|
|
251
|
-
: "";
|
|
252
|
-
if (!target) return allowedDecision();
|
|
253
|
-
const conflicts = [];
|
|
254
|
-
for (const session of otherActiveSessions(active, sessionId)) {
|
|
255
|
-
const sessionPath =
|
|
256
|
-
typeof session.worktreePath === "string"
|
|
257
|
-
? session.worktreePath.replace(/\\/g, "/")
|
|
258
|
-
: "";
|
|
259
|
-
if (sessionPath && sessionPath === target) {
|
|
260
|
-
conflicts.push({
|
|
261
|
-
file: target,
|
|
262
|
-
activeSession: session.sessionId,
|
|
263
|
-
activeTask: session.taskSummary || "",
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
if (conflicts.length > 0) {
|
|
268
|
-
return blockedDecision(op, conflicts, "active_worktree");
|
|
269
|
-
}
|
|
270
|
-
return allowedDecision();
|
|
271
|
-
}
|
|
272
|
-
default:
|
|
273
|
-
return allowedDecision();
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
return Object.freeze({
|
|
278
|
-
check,
|
|
279
|
-
checkRebase: (args, ctx) => check("rebase", args, ctx),
|
|
280
|
-
checkCheckout: (args, ctx) => check("checkout", args, ctx),
|
|
281
|
-
checkCherryPick: (args, ctx) => check("cherry-pick", args, ctx),
|
|
282
|
-
checkReset: (args, ctx) => check("reset", args, ctx),
|
|
283
|
-
checkStashPop: (args, ctx) => check("stash-pop", args, ctx),
|
|
284
|
-
checkWorktreeRemove: (args, ctx) => check("worktree-remove", args, ctx),
|
|
285
|
-
});
|
|
286
|
-
}
|