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.
- package/hub/team/swarm-hypervisor.mjs +24 -0
- package/hub/team/worktree-lifecycle.mjs +37 -1
- package/package.json +1 -1
- package/scripts/__tests__/keyword-detector.test.mjs +5 -5
- package/scripts/pack.mjs +1 -1
- package/skills/tfx-auto/SKILL.md +45 -0
- package/skills/tfx-codex/SKILL.md +1 -0
- package/skills/tfx-codex/skill.json +2 -1
- package/skills/tfx-find/SKILL.md +1 -0
- package/skills/tfx-find/skill.json +2 -1
- package/skills/tfx-plan/SKILL.md +1 -0
- package/skills/tfx-plan/skill.json +2 -1
- package/skills/tfx-qa/SKILL.md +1 -0
- package/skills/tfx-qa/skill.json +2 -1
- package/skills/tfx-research/SKILL.md +1 -0
- package/skills/tfx-research/skill.json +2 -1
- package/skills/tfx-review/SKILL.md +1 -0
- package/skills/tfx-review/skill.json +2 -1
|
@@ -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
|
@@ -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,
|
|
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,
|
|
104
|
-
assert.equal(rules.filter((rule) => rule.skill).length,
|
|
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,
|
|
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-
|
|
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) {
|
package/skills/tfx-auto/SKILL.md
CHANGED
|
@@ -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
|
>
|
package/skills/tfx-find/SKILL.md
CHANGED
package/skills/tfx-plan/SKILL.md
CHANGED
|
@@ -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:
|
package/skills/tfx-qa/SKILL.md
CHANGED
package/skills/tfx-qa/skill.json
CHANGED
|
@@ -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:
|