triflux 10.0.5 → 10.0.6

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.
@@ -19,6 +19,8 @@ import { createConductor, STATES } from './conductor.mjs';
19
19
  import { createSwarmLocks } from './swarm-locks.mjs';
20
20
  import { createEventLog } from './event-log.mjs';
21
21
  import { probeRemoteEnv, resolveRemoteDir } from './remote-session.mjs';
22
+ import { fetchRemoteShard } from './worktree-lifecycle.mjs';
23
+ import { getHostConfig } from '../lib/ssh-command.mjs';
22
24
 
23
25
  // ── Swarm states ──────────────────────────────────────────────
24
26
 
@@ -337,6 +339,28 @@ export function createSwarmHypervisor(opts) {
337
339
  const worker = workers.get(shardName);
338
340
  if (!worker) continue;
339
341
 
342
+ // Fetch remote shard branch to local (push-blocked hosts like Ultra4)
343
+ const shard = plan.shards.find((s) => s.name === shardName);
344
+ if (shard?.host && shard._remoteEnv) {
345
+ const hostConfig = getHostConfig(shard.host, config.rootDir);
346
+ const sshUser = hostConfig?.ssh_user || shard.host;
347
+ const remoteRepoPath = resolveRemoteDir(config.rootDir || process.cwd(), shard._remoteEnv);
348
+ const fetchResult = await fetchRemoteShard({
349
+ host: shard.host,
350
+ sshUser,
351
+ remoteRepoPath,
352
+ branchName: worker.branchName || `swarm/${config.runId}/${shardName}`,
353
+ rootDir: config.rootDir || process.cwd(),
354
+ });
355
+
356
+ if (!fetchResult.ok) {
357
+ eventLog.append('remote_fetch_failed', { shard: shardName, error: fetchResult.error });
358
+ integrationFailures.push(shardName);
359
+ continue;
360
+ }
361
+ eventLog.append('remote_fetch_ok', { shard: shardName, headCommit: fetchResult.headCommit });
362
+ }
363
+
340
364
  // Read shard output log for changed files
341
365
  const changedFiles = detectChangedFiles(shardName, worker);
342
366
 
@@ -6,7 +6,7 @@
6
6
  import { execFile } from 'node:child_process';
7
7
  import { resolve, normalize } from 'node:path';
8
8
  import { mkdir, rm, access } from 'node:fs/promises';
9
- import { remoteGit } from './remote-session.mjs';
9
+ import { remoteGit, validateHost } from './remote-session.mjs';
10
10
 
11
11
  const SWARM_ROOT = '.codex-swarm';
12
12
  const SLEEP_MS = 2000; // WT race-guard (MEMORY.md: wt-attach-spacing)
@@ -191,3 +191,39 @@ export async function pruneWorktree({ worktreePath, branchName, rootDir = proces
191
191
  try { await git(['branch', '-D', branchName], rootDir); } catch { /* may not exist */ }
192
192
  }
193
193
  }
194
+
195
+ /**
196
+ * Fetch a remote shard's branch to the local repo via SSH.
197
+ * Workaround for hosts that cannot push to GitHub (e.g. Ultra4).
198
+ *
199
+ * Flow: add temp remote → fetch branch → remove remote.
200
+ *
201
+ * @param {object} opts
202
+ * @param {string} opts.host — SSH host (e.g. "ultra4")
203
+ * @param {string} opts.sshUser — SSH user (e.g. "SSAFY")
204
+ * @param {string} opts.remoteRepoPath — absolute path on remote (e.g. "/c/Users/SSAFY/Desktop/Projects/cli/triflux")
205
+ * @param {string} opts.branchName — branch to fetch (e.g. "swarm/run123/auth")
206
+ * @param {string} [opts.rootDir=process.cwd()] — local repo root
207
+ * @returns {Promise<{ ok: boolean, localRef?: string, error?: string }>}
208
+ */
209
+ export async function fetchRemoteShard({ host, sshUser, remoteRepoPath, branchName, rootDir = process.cwd() }) {
210
+ validateHost(host);
211
+
212
+ const remoteName = `_swarm-${host}-${Date.now()}`;
213
+ const sshUrl = `ssh://${sshUser}@${host}${remoteRepoPath}`;
214
+
215
+ try {
216
+ await git(['remote', 'add', remoteName, sshUrl], rootDir);
217
+
218
+ await git(['fetch', remoteName, branchName, '--no-tags'], rootDir);
219
+
220
+ const localRef = `${remoteName}/${branchName}`;
221
+ const headCommit = await git(['rev-parse', `FETCH_HEAD`], rootDir);
222
+
223
+ return { ok: true, localRef, headCommit };
224
+ } catch (err) {
225
+ return { ok: false, error: err.message };
226
+ } finally {
227
+ try { await git(['remote', 'remove', remoteName], rootDir); } catch { /* cleanup */ }
228
+ }
229
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "10.0.5",
3
+ "version": "10.0.6",
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": {
@@ -29,7 +29,7 @@ const { extractPrompt, sanitizeForKeywordDetection } = detectorModule;
29
29
 
30
30
  function loadCompiledRules() {
31
31
  const rules = loadRules(rulesPath);
32
- assert.equal(rules.length, 33);
32
+ assert.equal(rules.length, 31);
33
33
  return compileRules(rules);
34
34
  }
35
35
 
@@ -100,8 +100,8 @@ test("sanitizeForKeywordDetection: 코드블록/URL/파일경로/XML 태그 제
100
100
 
101
101
  test("loadRules: 유효한 JSON 로드", () => {
102
102
  const rules = loadRules(rulesPath);
103
- assert.equal(rules.length, 33);
104
- assert.equal(rules.filter((rule) => rule.skill).length, 20);
103
+ assert.equal(rules.length, 31);
104
+ assert.equal(rules.filter((rule) => rule.skill).length, 18);
105
105
  assert.equal(rules.filter((rule) => rule.mcp_route).length, 10);
106
106
  });
107
107
 
@@ -122,7 +122,7 @@ test("loadRules: 잘못된 파일 처리", () => {
122
122
  test("compileRules: 정규식 컴파일 성공", () => {
123
123
  const rules = loadRules(rulesPath);
124
124
  const compiled = compileRules(rules);
125
- assert.equal(compiled.length, 33);
125
+ assert.equal(compiled.length, 31);
126
126
  for (const rule of compiled) {
127
127
  assert.ok(Array.isArray(rule.compiledPatterns));
128
128
  assert.ok(rule.compiledPatterns.length > 0);
@@ -153,7 +153,7 @@ test("matchRules: tfx 키워드 매칭", () => {
153
153
  const compiledRules = loadCompiledRules();
154
154
  const cases = [
155
155
  { text: "tfx multi 세션 시작", expectedId: "tfx-multi" },
156
- { text: "tfx auto 돌려줘", expectedId: "tfx-auto" },
156
+ { text: "tfx auto 돌려줘", expectedId: "tfx-unified" },
157
157
  { text: "tfx codex 로 실행", expectedId: "tfx-codex" },
158
158
  { text: "tfx gemini 로 실행", expectedId: "tfx-gemini" },
159
159
  { text: "canceltfx", expectedId: "tfx-cancel" }
package/scripts/pack.mjs CHANGED
@@ -227,7 +227,7 @@ export { createSwarmLocks } from './team/swarm-locks.mjs';
227
227
  export { parseShards, buildFileLeaseMap, buildMcpManifest, computeMergeOrder, planSwarm } from './team/swarm-planner.mjs';
228
228
  export { createSwarmHypervisor, SWARM_STATES } from './team/swarm-hypervisor.mjs';
229
229
  export { reconcile, buildRedundantIds, shouldRunRedundant } from './team/swarm-reconciler.mjs';
230
- export { ensureWorktree, prepareIntegrationBranch, rebaseShardOntoIntegration, pruneWorktree } from './team/worktree-lifecycle.mjs';
230
+ export { ensureWorktree, prepareIntegrationBranch, rebaseShardOntoIntegration, pruneWorktree, fetchRemoteShard } from './team/worktree-lifecycle.mjs';
231
231
  `;
232
232
 
233
233
  function writeIndex(dest, content) {
@@ -40,6 +40,51 @@ argument-hint: "<command|task> [args...]"
40
40
  > - Errors: 실패 시 원인/복구/재시도 여부를 구조화해 기록한다.
41
41
 
42
42
 
43
+ ### Step 0: 스마트 라우팅 (tfx-auto 진입 시 자동 실행)
44
+
45
+ preamble에서 routing-weights.json을 읽고, 사용자 입력을 분석하여 dispatch 결정.
46
+
47
+ ```bash
48
+ SLUG=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null || echo "unknown")
49
+ WEIGHTS_FILE="$HOME/.gstack/projects/$SLUG/routing-weights.json"
50
+ USER_MODE=""
51
+ if [ -f "$WEIGHTS_FILE" ]; then
52
+ USER_MODE=$(node -e "
53
+ const w=JSON.parse(require('fs').readFileSync('$WEIGHTS_FILE','utf8'));
54
+ const m=w.weights?.mode_bias||{};
55
+ const top=Object.entries(m).sort((a,b)=>b[1]-a[1])[0];
56
+ if(top && top[1]>0.3) console.log(top[0]);
57
+ " 2>/dev/null)
58
+ fi
59
+ echo "USER_PREFERRED_MODE: ${USER_MODE:-none}"
60
+ ```
61
+
62
+ 판단 기준 (우선순위 순):
63
+
64
+ 1. **사용자 명시 키워드** (최우선):
65
+ - "병렬", "swarm", "PRD 돌려" → `Skill("tfx-swarm")` dispatch
66
+ - "꼼꼼히", "제대로", "deep" → 해당 `tfx-deep-*` dispatch
67
+ - "끝까지", "멈추지마", "ralph" → `Skill("tfx-persist")` dispatch
68
+ - "multi", "팀", "협업" → `Skill("tfx-multi")` dispatch
69
+ - "codex로", "gemini로" → `Skill("tfx-codex")` 또는 `Skill("tfx-gemini")` dispatch
70
+
71
+ 2. **PRD 인자 분석**:
72
+ - PRD 경로 2개 이상 → `Skill("tfx-swarm")` dispatch
73
+ - PRD 1개 + XL 규모 → `Skill("tfx-fullcycle")` dispatch
74
+
75
+ 3. **선호도 가중치** (tiebreaker):
76
+ - USER_PREFERRED_MODE가 있고 가중치 > 0.3이면 제안
77
+ - "[tfx] 사용자 선호: {mode}. 이 모드로 실행할까요?" 1줄 표시
78
+ - 응답 없으면 기본(auto) 진행
79
+
80
+ 4. **기본**: 기존 tfx-auto 워크플로우 그대로 실행
81
+
82
+ dispatch 시 해당 스킬을 Skill 도구로 호출하고 **이 워크플로우를 종료**한다. dispatch하지 않으면 아래 기존 워크플로우 진행.
83
+
84
+ 라우팅 결정 후 1줄 표시:
85
+ ```
86
+ [tfx] 규모: {S/M/L/XL}, 모드: {mode} ({profile}) — 오버라이드: /tfx-multi, /tfx-swarm 등
87
+ ```
43
88
 
44
89
  > **MANDATORY RULES**
45
90
  >
@@ -1,4 +1,5 @@
1
1
  ---
2
+ internal: true
2
3
  name: tfx-codex
3
4
  description: >
4
5
  Codex-Only 오케스트레이터. tfx-auto 워크플로우를 Codex 전용으로 고정합니다.
@@ -4,5 +4,6 @@
4
4
  "triggers": [
5
5
  "tfx-codex"
6
6
  ],
7
- "argument_hint": "\\\"작업 설명\\\" | N:codex \\\"작업 설명\\\""
7
+ "argument_hint": "\\\"작업 설명\\\" | N:codex \\\"작업 설명\\\"",
8
+ "internal": true
8
9
  }
@@ -1,4 +1,5 @@
1
1
  ---
2
+ internal: true
2
3
  name: tfx-find
3
4
  description: >
4
5
  코드베이스에서 파일, 함수, 클래스, 문자열을 빠르게 찾을 때 사용한다.
@@ -8,5 +8,6 @@
8
8
  "코드에서 찾기",
9
9
  "코드베이스 검색"
10
10
  ],
11
- "argument_hint": "<검색 패턴 또는 질문>"
11
+ "argument_hint": "<검색 패턴 또는 질문>",
12
+ "internal": true
12
13
  }
@@ -1,4 +1,5 @@
1
1
  ---
2
+ internal: true
2
3
  name: tfx-plan
3
4
  description: "구현 계획이 필요할 때 사용한다. '계획 세워줘', 'plan', '플랜', '어떻게 구현하지', '태스크 분해', '작업 순서' 같은 요청에 반드시 사용. 기능 구현 전 영향 범위 파악과 태스크 분해가 필요할 때 적극 활용. 작업 분해, 순서 정리, '어떤 순서로', '먼저 뭐 해야', 'breakdown', 'decompose', 'task list' 같은 요청에도 적극 활용. 단순 계획은 이 스킬, 3자 합의 계획은 tfx-deep-plan을 사용."
4
5
  triggers:
@@ -7,5 +7,6 @@
7
7
  "플랜",
8
8
  "설계"
9
9
  ],
10
- "argument_hint": "<구현할 기능 설명>"
10
+ "argument_hint": "<구현할 기능 설명>",
11
+ "internal": true
11
12
  }
@@ -1,4 +1,5 @@
1
1
  ---
2
+ internal: true
2
3
  name: tfx-qa
3
4
  description: "테스트 실행하고 실패하면 수정해서 통과시켜야 할 때 사용한다. 'qa', '검증해', '테스트 돌려', 'test-fix', '테스트 통과시켜' 같은 요청에 반드시 사용. 테스트 실패를 반복 수정하여 전부 통과시켜야 할 때 적극 활용."
4
5
  triggers:
@@ -7,5 +7,6 @@
7
7
  "테스트 검증",
8
8
  "test-fix"
9
9
  ],
10
- "argument_hint": "[테스트 명령 또는 파일 경로]"
10
+ "argument_hint": "[테스트 명령 또는 파일 경로]",
11
+ "internal": true
11
12
  }
@@ -1,4 +1,5 @@
1
1
  ---
2
+ internal: true
2
3
  name: tfx-research
3
4
  description: "빠른 웹 검색과 요약이 필요할 때 사용한다. '검색해줘', '찾아봐', '최신 정보', '이거 뭐야', 'search', '공식 문서 확인' 같은 요청에 반드시 사용. 라이브러리 문서, API 레퍼런스, 에러 해결, 최신 뉴스, 팩트 체크 등 외부 정보가 필요한 모든 상황에 적극 활용한다."
4
5
  triggers:
@@ -9,5 +9,6 @@
9
9
  "search",
10
10
  "web search"
11
11
  ],
12
- "argument_hint": "<검색 주제>"
12
+ "argument_hint": "<검색 주제>",
13
+ "internal": true
13
14
  }
@@ -1,4 +1,5 @@
1
1
  ---
2
+ internal: true
2
3
  name: tfx-review
3
4
  description: "코드 리뷰가 필요할 때 사용한다. 'review', '리뷰해줘', '코드 봐줘', '이거 괜찮아?', 'PR 리뷰', '변경사항 확인' 같은 요청에 반드시 사용. git diff, 특정 파일, 또는 최근 변경에 대한 빠른 피드백이 필요할 때 적극 활용. 코드 변경사항 확인, '이거 문제 없어?', 'looks good?', 'LGTM?', '머지해도 될까' 같은 요청에도 적극 활용. 꼼꼼한 심층 리뷰는 tfx-deep-review를 사용."
4
5
  triggers:
@@ -7,5 +7,6 @@
7
7
  "코드 리뷰",
8
8
  "code review"
9
9
  ],
10
- "argument_hint": "[파일 경로 또는 변경 설명]"
10
+ "argument_hint": "[파일 경로 또는 변경 설명]",
11
+ "internal": true
11
12
  }