triflux 8.12.2 → 9.0.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.
Files changed (49) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/bin/triflux.mjs +64 -0
  4. package/hub/team/backend.mjs +2 -1
  5. package/hub/team/cli/commands/start/index.mjs +2 -2
  6. package/hub/team/cli/commands/start/parse-args.mjs +10 -0
  7. package/hub/workers/delegator-mcp.mjs +2 -5
  8. package/package.json +1 -1
  9. package/scripts/cache-buildup.mjs +24 -395
  10. package/scripts/cache-doctor.mjs +149 -0
  11. package/scripts/cache-warmup.mjs +514 -0
  12. package/scripts/cross-review-gate.mjs +180 -0
  13. package/scripts/cross-review-tracker.mjs +279 -0
  14. package/scripts/headless-guard.mjs +38 -0
  15. package/scripts/lib/env-probe.mjs +130 -0
  16. package/scripts/lib/mcp-filter.mjs +730 -720
  17. package/scripts/lib/mcp-manifest.mjs +79 -0
  18. package/scripts/mcp-gateway-config.mjs +104 -7
  19. package/scripts/mcp-gateway-start.mjs +7 -0
  20. package/scripts/mcp-gateway-verify.mjs +15 -1
  21. package/scripts/preflight-cache.mjs +68 -137
  22. package/scripts/session-spawn-helper.mjs +184 -0
  23. package/scripts/setup.mjs +7 -8
  24. package/scripts/tfx-route-worker.mjs +59 -1
  25. package/skills/merge-worktree/SKILL.md +144 -0
  26. package/skills/tfx-analysis/SKILL.md +1 -0
  27. package/skills/tfx-auto/SKILL.md +1 -0
  28. package/skills/tfx-auto-codex/SKILL.md +1 -0
  29. package/skills/tfx-autopilot/SKILL.md +1 -2
  30. package/skills/tfx-codex/SKILL.md +2 -0
  31. package/skills/tfx-codex-swarm/SKILL.md +62 -18
  32. package/skills/tfx-codex-swarm/mcp-daemon/start-daemons.ps1 +54 -0
  33. package/skills/tfx-codex-swarm/mcp-daemon/stop-daemons.ps1 +15 -0
  34. package/skills/tfx-consensus/SKILL.md +1 -0
  35. package/skills/tfx-deep-analysis/SKILL.md +1 -0
  36. package/skills/tfx-deep-plan/SKILL.md +1 -0
  37. package/skills/tfx-deep-qa/SKILL.md +1 -0
  38. package/skills/tfx-deep-research/SKILL.md +1 -0
  39. package/skills/tfx-deep-review/SKILL.md +1 -0
  40. package/skills/tfx-doctor/SKILL.md +5 -0
  41. package/skills/tfx-gemini/SKILL.md +1 -0
  42. package/skills/tfx-hub/SKILL.md +1 -0
  43. package/skills/tfx-multi/SKILL.md +1 -0
  44. package/skills/tfx-plan/SKILL.md +1 -0
  45. package/skills/tfx-qa/SKILL.md +1 -0
  46. package/skills/tfx-ralph/SKILL.md +2 -5
  47. package/skills/tfx-research/SKILL.md +1 -0
  48. package/skills/tfx-review/SKILL.md +2 -0
  49. package/skills/tfx-setup/SKILL.md +182 -7
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, existsSync } from "node:fs";
3
+ import { spawn, execFileSync } from "node:child_process";
4
+ import { resolve } from "node:path";
5
+
6
+ const SESSION_PREFIX = "tfx-isolated";
7
+ const DEFAULT_ATTACH_PROFILE = "triflux";
8
+ const SESSION_EXPIRE_MS = 30 * 60 * 1000;
9
+
10
+ const STOP_WORDS = new Set([
11
+ "a", "an", "and", "as", "at", "be", "by", "for", "from", "in",
12
+ "is", "it", "of", "on", "or", "that", "the", "to", "with",
13
+ "작업", "요청", "합니다", "그리고", "에서", "으로",
14
+ ]);
15
+
16
+ // ── psmux helpers ──
17
+
18
+ function hasPsmux() {
19
+ try {
20
+ execFileSync("psmux", ["-V"], { timeout: 2000, stdio: "ignore" });
21
+ return true;
22
+ } catch { return false; }
23
+ }
24
+
25
+ function psmux(...args) {
26
+ execFileSync("psmux", args, { timeout: 5000, stdio: "ignore" });
27
+ }
28
+
29
+ function psmuxCapture(sessionName) {
30
+ try {
31
+ return execFileSync("psmux", ["capture-pane", "-t", sessionName, "-p"], {
32
+ timeout: 5000, encoding: "utf8",
33
+ }).trim();
34
+ } catch { return ""; }
35
+ }
36
+
37
+ function psmuxHasSession(sessionName) {
38
+ try {
39
+ execFileSync("psmux", ["has-session", "-t", sessionName], { timeout: 2000, stdio: "ignore" });
40
+ return true;
41
+ } catch { return false; }
42
+ }
43
+
44
+ // ── core functions ──
45
+
46
+ export function createIsolatedSessionName(timestamp = Date.now()) {
47
+ return `${SESSION_PREFIX}-${Math.trunc(timestamp)}`;
48
+ }
49
+
50
+ export function createIsolatedSession(options = {}) {
51
+ const ts = options.timestamp ?? Date.now();
52
+ const sessionName = options.name || createIsolatedSessionName(ts);
53
+
54
+ psmux("new-session", "-s", sessionName, "-d");
55
+
56
+ // cd to project root
57
+ const projectRoot = options.projectRoot || process.cwd();
58
+ psmux("send-keys", "-t", sessionName, `cd '${projectRoot}'`, "Enter");
59
+
60
+ // send prompt as claude command
61
+ if (options.prompt) {
62
+ const safePrompt = options.prompt.replace(/'/g, "'\\''");
63
+ psmux("send-keys", "-t", sessionName, `claude --prompt '${safePrompt}'`, "Enter");
64
+ }
65
+
66
+ return { sessionName };
67
+ }
68
+
69
+ export function attachWithWindowsTerminal(sessionName, options = {}) {
70
+ const profile = options.profile || DEFAULT_ATTACH_PROFILE;
71
+ const title = options.title || sessionName;
72
+
73
+ // sp (split-pane), not new-tab
74
+ const wtArgs = ["sp", "-p", profile, "--title", title, "--", "psmux", "attach-session", "-t", sessionName];
75
+ const child = spawn("wt.exe", wtArgs, { detached: true, stdio: "ignore", windowsHide: false });
76
+ child.unref();
77
+ return wtArgs;
78
+ }
79
+
80
+ export function waitForCompletion(sessionName, opts = {}) {
81
+ const pollMs = opts.pollMs || 3000;
82
+ const maxMs = opts.maxMs || SESSION_EXPIRE_MS;
83
+ const start = Date.now();
84
+
85
+ return new Promise((res) => {
86
+ const check = () => {
87
+ if (!psmuxHasSession(sessionName) || Date.now() - start > maxMs) {
88
+ const output = psmuxCapture(sessionName);
89
+ // cleanup expired session
90
+ try { psmux("kill-session", "-t", sessionName); } catch {}
91
+ res({ sessionName, output, expired: Date.now() - start > maxMs });
92
+ return;
93
+ }
94
+ setTimeout(check, pollMs);
95
+ };
96
+ check();
97
+ });
98
+ }
99
+
100
+ // ── context drift (kept from codex) ──
101
+
102
+ function tokenize(text) {
103
+ return String(text || "").toLowerCase()
104
+ .split(/[^\p{L}\p{N}_-]+/u)
105
+ .filter((t) => t.length >= 2 && !STOP_WORDS.has(t));
106
+ }
107
+
108
+ export function evaluateContextDrift(input = {}) {
109
+ const taskTokens = Array.from(new Set(tokenize(input.taskPrompt)));
110
+ if (!taskTokens.length) return { drift: false, overlapRatio: 1, reason: "task-token-empty" };
111
+
112
+ const outputTokens = new Set(tokenize(input.latestOutput));
113
+ const matched = taskTokens.filter((t) => outputTokens.has(t));
114
+ const ratio = matched.length / taskTokens.length;
115
+ const threshold = input.minOverlapRatio ?? 0.2;
116
+
117
+ return { drift: ratio < threshold, overlapRatio: ratio, reason: ratio < threshold ? "token-overlap-low" : "token-overlap-ok" };
118
+ }
119
+
120
+ // ── CLI ──
121
+
122
+ function parseArgs(argv) {
123
+ const a = { spawn: false, prompt: "", attach: false, background: false, name: "" };
124
+ for (let i = 2; i < argv.length; i++) {
125
+ const arg = argv[i];
126
+ if (arg === "--spawn") { a.spawn = true; continue; }
127
+ if (arg === "--attach") { a.attach = true; continue; }
128
+ if (arg === "--background") { a.background = true; continue; }
129
+ if ((arg === "--prompt" || arg === "-p") && argv[i + 1]) { a.prompt = argv[++i]; continue; }
130
+ if ((arg === "--name" || arg === "-n") && argv[i + 1]) { a.name = argv[++i]; continue; }
131
+ }
132
+ return a;
133
+ }
134
+
135
+ async function main() {
136
+ const args = parseArgs(process.argv);
137
+
138
+ if (!args.spawn) {
139
+ process.stdout.write([
140
+ "session-spawn-helper: psmux 격리 세션 생성 도구",
141
+ "",
142
+ "사용법:",
143
+ " node scripts/session-spawn-helper.mjs --spawn --prompt '작업 내용' [--attach] [--background] [--name 세션명]",
144
+ "",
145
+ "옵션:",
146
+ " --spawn 세션 생성 (필수)",
147
+ " --prompt TEXT Claude에 전달할 프롬프트",
148
+ " --attach WT split-pane으로 attach",
149
+ " --background attach 없이 실행, 완료 시 결과 출력",
150
+ " --name NAME 세션 이름 (기본: tfx-isolated-{ts})",
151
+ "",
152
+ ].join("\n"));
153
+ process.exit(0);
154
+ }
155
+
156
+ if (!hasPsmux()) {
157
+ process.stderr.write("ERROR: psmux가 설치되어 있지 않습니다. npm install -g psmux\n");
158
+ process.exit(1);
159
+ }
160
+
161
+ const { sessionName } = createIsolatedSession({
162
+ name: args.name || undefined,
163
+ prompt: args.prompt || undefined,
164
+ projectRoot: process.cwd(),
165
+ });
166
+
167
+ process.stdout.write(`[session-spawn] 세션 생성: ${sessionName}\n`);
168
+
169
+ if (args.attach) {
170
+ attachWithWindowsTerminal(sessionName);
171
+ process.stdout.write(`[session-spawn] WT split-pane attach 완료\n`);
172
+ }
173
+
174
+ if (args.background) {
175
+ process.stdout.write(`[session-spawn] 백그라운드 대기 중...\n`);
176
+ const result = await waitForCompletion(sessionName);
177
+ const preview = (result.output || "(no output)").slice(0, 200);
178
+ process.stdout.write(`[session-spawn] 완료: ${sessionName} | expired=${result.expired} | preview=${preview}\n`);
179
+ }
180
+ }
181
+
182
+ if (process.argv[1]?.endsWith("session-spawn-helper.mjs")) {
183
+ main().catch((e) => { process.stderr.write(`${e.message}\n`); process.exit(1); });
184
+ }
package/scripts/setup.mjs CHANGED
@@ -10,6 +10,7 @@ import { homedir } from "os";
10
10
  import { spawn, execFileSync } from "child_process";
11
11
  import { fileURLToPath } from "url";
12
12
  import { cleanupTmpFiles } from "./tmp-cleanup.mjs";
13
+ import { buildAll as buildCacheWarmup } from "./cache-warmup.mjs";
13
14
 
14
15
  const PLUGIN_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
15
16
  const CLAUDE_DIR = join(homedir(), ".claude");
@@ -824,17 +825,15 @@ if (existsSync(mcpCheck)) {
824
825
  child.unref(); // 부모 프로세스와 분리 — 비동기 실행
825
826
  }
826
827
 
827
- // ── 캐시 빌드업 Phase 1 (백그라운드) ──
828
+ // ── Step 6. 캐시 웜업 Phase 1 ──
828
829
 
829
- const cacheBuildupScript = join(PLUGIN_ROOT, "scripts", "cache-buildup.mjs");
830
- if (existsSync(cacheBuildupScript)) {
831
- const child2 = spawn(process.execPath, [cacheBuildupScript], {
832
- detached: true,
833
- stdio: "ignore",
834
- windowsHide: true,
830
+ try {
831
+ buildCacheWarmup({
835
832
  cwd: process.cwd(),
833
+ ttlMs: 5 * 60 * 1000,
836
834
  });
837
- child2.unref();
835
+ } catch {
836
+ // cache-warmup 실패는 setup 전체를 막지 않는다
838
837
  }
839
838
 
840
839
  // ── /tmp 임시 파일 자동 정리 (setup 지연 방지: fire-and-forget) ──
@@ -14,6 +14,13 @@ const FACTORY_CANDIDATES = [
14
14
  // MCP transport 실패 시 tfx-route.sh가 exec fallback을 수행할 수 있도록
15
15
  // CODEX_MCP_TRANSPORT_EXIT_CODE(70)으로 종료한다.
16
16
  const MCP_TRANSPORT_EXIT_CODE = 70;
17
+ const GEMINI_RETRY_DELAY_MS = 5000;
18
+ const GEMINI_RETRY_PATTERN_SNIPPETS = [
19
+ '429',
20
+ 'quota',
21
+ 'rate limit',
22
+ 'resource_exhausted',
23
+ ];
17
24
 
18
25
  let createWorker = null;
19
26
 
@@ -129,6 +136,57 @@ function resolveDefaultMcpConfig(cwd) {
129
136
  return [];
130
137
  }
131
138
 
139
+ function sleep(ms) {
140
+ return new Promise((resolve) => setTimeout(resolve, ms));
141
+ }
142
+
143
+ function isGeminiQuotaRetrySignal(error) {
144
+ if (Number(error?.result?.exitCode) === 429) {
145
+ return true;
146
+ }
147
+
148
+ const fragments = [
149
+ error?.message,
150
+ error?.stderr,
151
+ error?.result?.stderr,
152
+ ]
153
+ .filter((value) => typeof value === 'string' && value.trim().length > 0)
154
+ .map((value) => value.toLowerCase());
155
+
156
+ if (fragments.length === 0) return false;
157
+ const merged = fragments.join('\n');
158
+ return GEMINI_RETRY_PATTERN_SNIPPETS.some((pattern) => merged.includes(pattern));
159
+ }
160
+
161
+ async function runWorker(worker, type, prompt) {
162
+ const maxAttempts = type === 'gemini' ? 2 : 1;
163
+ let lastError = null;
164
+
165
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
166
+ try {
167
+ return await worker.run(prompt);
168
+ } catch (error) {
169
+ lastError = error;
170
+ const shouldRetry = (
171
+ type === 'gemini'
172
+ && attempt < maxAttempts
173
+ && isGeminiQuotaRetrySignal(error)
174
+ );
175
+
176
+ if (!shouldRetry) {
177
+ throw error;
178
+ }
179
+
180
+ process.stderr.write(
181
+ '[tfx-route-worker] Gemini 429/quota 감지 — 5초 후 1회 재시도합니다.\n',
182
+ );
183
+ await sleep(GEMINI_RETRY_DELAY_MS);
184
+ }
185
+ }
186
+
187
+ throw lastError;
188
+ }
189
+
132
190
  const args = parseArgs(process.argv.slice(2));
133
191
  const prompt = readPromptFromStdin();
134
192
 
@@ -148,7 +206,7 @@ const worker = createWorker(args.type, {
148
206
  });
149
207
 
150
208
  try {
151
- const result = await worker.run(prompt);
209
+ const result = await runWorker(worker, args.type, prompt);
152
210
  if (result.response) {
153
211
  process.stdout.write(result.response);
154
212
  if (!result.response.endsWith('\n')) process.stdout.write('\n');
@@ -0,0 +1,144 @@
1
+ ---
2
+ name: merge-worktree
3
+ description: "워크트리 브랜치를 main으로 squash-merge + conventional commit 자동 생성. codex-swarm 워크트리 자동 인식. '머지해', 'merge worktree', '워크트리 머지', '결과 수집', 'squash merge' 요청에 사용."
4
+ argument-hint: "[target-branch]"
5
+ disable-model-invocation: true
6
+ ---
7
+
8
+ # Merge Worktree
9
+
10
+ 워크트리 브랜치를 대상 브랜치로 squash-merge하고 conventional commit 메시지를 자동 작성한다.
11
+
12
+ ## Current context
13
+
14
+ * Git dir: `!git rev-parse --git-dir`
15
+ * Current branch: `!git branch --show-current`
16
+ * Recent commits: `!git log --oneline -20`
17
+ * Working tree status: `!git status --short`
18
+
19
+ ## Instructions
20
+
21
+ ### Phase 1: Validation
22
+
23
+ 1. **Worktree 확인**: `git rev-parse --git-dir` 출력에 `/worktrees/`가 포함되어야 한다. 아니면 중지.
24
+
25
+ 2. **현재 브랜치 확인**: `git branch --show-current`
26
+
27
+ 3. **대상 브랜치 결정**:
28
+ * `$ARGUMENTS`가 있으면 해당 브랜치 사용
29
+ * 없으면 `main` 존재 확인, 없으면 `master`
30
+
31
+ 4. **원본 레포 경로 확인**: `git rev-parse --git-common-dir`의 부모 디렉토리
32
+
33
+ 5. **클린 상태 확인**: `git status --porcelain`이 비어있어야 한다. 미커밋 변경이 있으면 먼저 커밋/스태시 안내.
34
+
35
+ ### Phase 2: Research
36
+
37
+ 1. **커밋 이력**: `git log --oneline <target>..HEAD`
38
+
39
+ 2. **변경 파일 요약**: `git diff <target>...HEAD --stat`
40
+
41
+ 3. **전체 diff**: `git diff <target>...HEAD` — 꼼꼼히 읽는다.
42
+
43
+ 4. **핵심 파일 읽기**: 가장 큰 변경, 신규 파일, 삭제 파일을 Read로 확인.
44
+
45
+ 5. **변경 분류**:
46
+ * Features (신규 기능)
47
+ * Fixes (버그 수정)
48
+ * Refactors (구조 변경)
49
+ * Tests (테스트)
50
+ * Docs (문서)
51
+ * Config/Chore (빌드, CI, 의존성)
52
+
53
+ 6. **dominant type 결정**: `feat`, `fix`, `refactor`, `docs`, `chore`, `test` 중 하나
54
+
55
+ ### Phase 3: 대상 브랜치 준비
56
+
57
+ 1. **대상 브랜치 최근 커밋 확인**: `git -C <원본레포> log --oneline -10 <target>`
58
+
59
+ 2. **WIP 커밋 감지**: `wip:`, `auto-commit`, `WIP` 시작 커밋이 있으면 사용자에게 경고.
60
+
61
+ 3. **최신 fetch**: `git -C <원본레포> fetch origin <target> 2>/dev/null`
62
+
63
+ ### Phase 4: Squash Merge
64
+
65
+ 1. **대상 브랜치 checkout**:
66
+ ```
67
+ git -C <원본레포> checkout <target>
68
+ ```
69
+
70
+ 2. **squash merge 실행**:
71
+ ```
72
+ git -C <원본레포> merge --squash <워크트리브랜치>
73
+ ```
74
+
75
+ 3. **충돌 처리**: 충돌 발생 시 충돌 파일 목록 + 마커를 보여주고 **중지**. 자동 해결 시도 금지.
76
+
77
+ ### Phase 5: 커밋 메시지 작성 + 커밋
78
+
79
+ Phase 2 분석 기반으로 아래 구조의 커밋 메시지를 작성한다:
80
+
81
+ ```
82
+ <type>: <명령형 요약, 72자 이내, 마침표 없음>
83
+
84
+ <무엇을 왜 했는지 2-4문장. 동기와 접근 방식 중심.>
85
+
86
+ Changes:
87
+ * <그룹별 변경 사항>
88
+ * <하위 항목은 서브 불릿>
89
+ ```
90
+
91
+ **규칙:**
92
+ * `<type>`은 `feat`, `fix`, `refactor`, `docs`, `chore`, `test` 중 하나
93
+ * 여러 유형이 섞이면 dominant 사용
94
+ * 요약: 명령형 ("add", "fix", "refactor"), 마침표 없음, 72자 제한
95
+ * 본문: *왜*와 *맥락*, *무엇*만이 아님
96
+ * Changes: 관련 항목 그룹핑, 중요한 것 먼저
97
+ * Co-Authored-By 푸터 **절대 추가 금지** (글로벌 설정 `includeCoAuthoredBy: false`)
98
+
99
+ **커밋 실행**:
100
+ ```bash
101
+ git -C <원본레포> commit -m "$(cat <<'EOF'
102
+ <커밋 메시지>
103
+ EOF
104
+ )"
105
+ ```
106
+
107
+ ### Phase 6: 정리 + 검증
108
+
109
+ 1. **커밋 확인**: `git -C <원본레포> log --oneline -3`
110
+
111
+ 2. **워크트리 자동 정리**:
112
+ ```bash
113
+ git -C <원본레포> worktree remove <워크트리경로>
114
+ git -C <원본레포> branch -d <워크트리브랜치>
115
+ ```
116
+
117
+ 3. **codex-swarm 정리 감지**: 워크트리 경로가 `.codex-swarm/wt-*` 패턴이면:
118
+ * 같은 `.codex-swarm/` 디렉토리에 다른 워크트리가 남아있는지 확인
119
+ * 모든 워크트리가 머지 완료되었으면 `.codex-swarm/` 전체 정리 제안
120
+ * `git worktree prune` 실행
121
+
122
+ 4. **결과 보고**:
123
+ * 커밋 해시 + 요약
124
+ * 머지 대상 브랜치
125
+ * 워크트리 정리 완료 여부
126
+ * push 안내 (`git push`)
127
+
128
+ ## codex-swarm 연동
129
+
130
+ 이 스킬은 `tfx-codex-swarm`의 Step 10 "결과 수집"에서 자동으로 호출된다.
131
+ codex-swarm이 완료한 각 워크트리에 대해 순차적으로 실행:
132
+
133
+ ```
134
+ 각 워크트리에 대해:
135
+ 1. 워크트리로 cd
136
+ 2. /merge-worktree main
137
+ 3. 다음 워크트리로 이동
138
+ ```
139
+
140
+ ## 주의사항
141
+
142
+ * force-push, destructive 연산은 사용자 확인 없이 절대 실행 금지
143
+ * pre-commit hook 건너뛰기(`--no-verify`) 금지
144
+ * 예상치 못한 상황이면 추측하지 말고 **중지 후 설명**
@@ -11,6 +11,7 @@ argument-hint: "<분석 대상 — 파일, 디렉토리, 또는 주제>"
11
11
 
12
12
  # tfx-analysis — Light Code Analysis
13
13
 
14
+ > **Deep 버전**: tfx-deep-analysis. "제대로/꼼꼼히" 수정자로 자동 에스컬레이션.
14
15
  > Codex 단일 분석으로 빠른 인사이트. SuperClaude sc:analyze 영감.
15
16
 
16
17
  ## 용도
@@ -33,6 +33,7 @@ argument-hint: "<command|task> [args...]"
33
33
  > 3. **DAG**: SEQUENTIAL/DAG이면 레벨 기반 순차 실행. `.omc/context/{sid}/` 생성, context_output 저장, 실패 시 후속 SKIP.
34
34
  > 4. **트리아지**: Codex `exec --full-auto` 분류 + Opus 인라인 분해. Agent 스폰 금지.
35
35
  > 5. **thorough**: `-t`/`--thorough` 시 파이프라인 init 필수. 커맨드 숏컷은 항상 quick.
36
+ > 6. **직접 수정 금지**: implement/review/analyze 등 커맨드 숏컷 실행 시 절대로 Edit/Write 도구로 직접 코드를 수정하지 마라. 반드시 Bash(tfx-route.sh)를 통해 Codex/Gemini에 위임하라. 작업이 아무리 사소해도 예외 없음.
36
37
 
37
38
  ## 모드
38
39
 
@@ -8,6 +8,7 @@ argument-hint: "\"작업 설명\" | N:agent_type \"작업 설명\""
8
8
 
9
9
  # tfx-auto-codex — Codex 리드형 tfx-auto
10
10
 
11
+ > **래퍼**: tfx-auto의 Codex 전용 바로가기. TFX_NO_CLAUDE_NATIVE=1.
11
12
  > 목적: 기존 `tfx-auto`의 오케스트레이션 패턴을 유지하면서
12
13
  > Claude 네이티브 역할(`explore`, `verifier`, `test-engineer`, `qa-tester`)을
13
14
  > Codex로 치환해 Codex/Gemini만으로 실행한다.
@@ -1,11 +1,10 @@
1
1
  ---
2
2
  name: tfx-autopilot
3
- description: "간단한 작업을 자율적으로 구현해야 할 때 사용한다. 'autopilot', '자동으로', '알아서 해', '그냥 해줘', 'auto' 같은 요청에 반드시 사용. 명확한 단일 작업을 빠르게 자동 구현+검증할 때 적극 활용."
3
+ description: "간단한 작업을 자율적으로 구현해야 할 때 사용한다. 'autopilot', '자동으로', '알아서 해', '그냥 해줘' 같은 요청에 반드시 사용. 명확한 단일 작업을 빠르게 자동 구현+검증할 때 적극 활용."
4
4
  triggers:
5
5
  - autopilot
6
6
  - 자동
7
7
  - 알아서 해
8
- - auto
9
8
  argument-hint: "<구현할 작업 설명>"
10
9
  ---
11
10
 
@@ -8,6 +8,8 @@ argument-hint: "\"작업 설명\" | N:codex \"작업 설명\""
8
8
 
9
9
  # tfx-codex — Codex-Only 오케스트레이터
10
10
 
11
+ > **래퍼**: tfx-auto의 Codex 전용 바로가기. TFX_CLI_MODE=codex.
12
+ > **HARD RULE**: Claude는 이 스킬에서 Edit/Write를 사용하면 안 된다. 모든 코드 수정은 Codex CLI를 통해 수행한다.
11
13
  > Codex CLI만 사용하여 모든 외부 CLI 작업을 라우팅합니다.
12
14
  > Gemini CLI가 없는 환경에서 사용합니다.
13
15
 
@@ -13,8 +13,18 @@ description: OMX 스킬을 활용하는 Codex 다중 세션 스폰 오케스트
13
13
  - `codex` CLI 설치됨
14
14
  - `psmux` 설치됨 (세션 관리)
15
15
  - `git` (worktree 생성)
16
- - Windows Terminal (탭 기반 attach)
17
16
  - Windows Terminal (`wt.exe` 탭 기반 attach)
17
+ - MCP 싱글톤 데몬 (`supergateway` + `mcp-remote`) — 선택적이나 스웜 시 강력 권장
18
+
19
+ ## 설정
20
+
21
+ | 설정 | 기본값 | 설명 |
22
+ |------|--------|------|
23
+ | MAX_CONCURRENCY | 4 | 동시 실행 세션 수. 초과분은 큐 대기 후 순차 시작 |
24
+ | WT_ATTACH_MODE | attach | `attach`: split-pane 직접 attach (기본). `dashboard`: 모니터링 탭만 |
25
+ | MCP_DAEMON_REQUIRED | true | MCP 싱글톤 데몬 사전 확인 필수. false면 세션별 MCP 직접 스폰 (비권장) |
26
+
27
+ 사용자가 명시하지 않으면 기본값 사용. AskUserQuestion으로 오버라이드 가능.
18
28
 
19
29
  ## 워크플로우
20
30
 
@@ -267,7 +277,13 @@ git worktree add .codex-swarm/wt-issue-{N} codex/issue-{N}
267
277
  cp {PRD_PATH} .codex-swarm/wt-issue-{N}/{PRD_PATH}
268
278
  ```
269
279
 
270
- ### Step 7: psmux 세션 생성 + Codex 실행
280
+ ### Step 7: psmux 세션 생성 + Codex 실행 (웨이브 방식)
281
+
282
+ **MAX_CONCURRENCY(기본 4)에 따라 웨이브 단위로 실행한다.**
283
+ - Wave 1: 태스크 1~MAX_CONCURRENCY 동시 시작
284
+ - Wave 2+: 이전 웨이브에서 완료된 슬롯만큼 다음 태스크 시작
285
+ - 완료 감지: `psmux capture-pane`으로 codex 종료 여부 확인 (30초 폴링)
286
+ - 전체 태스크 > MAX_CONCURRENCY일 때만 큐잉 적용
271
287
 
272
288
  각 태스크에 대해 psmux 세션을 생성하고 Codex를 실행한다:
273
289
 
@@ -293,26 +309,39 @@ psmux send-keys -t "codex-swarm-{id}" \
293
309
  # --skip-git-repo-check: codex exec 전용이므로 대화식 모드에서 사용 불가
294
310
  ```
295
311
 
296
- ### Step 8: WT triflux 프로파일로 attach
312
+ ### Step 8: WT attach
297
313
 
298
314
  > WT `triflux` 프로파일 사용 필수 (`commandline: "psmux"`, One Half Dark, acrylic).
299
- > triflux 프로파일은 psmux를 기본 셸로 쓰므로 attach가 바로 동작한다.
300
- > 탭 대신 split-pane 사용 (feedback: WT 새탭 금지, split+dashboard 기본).
315
+ > 기본은 **split-pane 직접 attach**. 4개 이하 2x2 그리드, 5개 이상은 사용자 확인.
301
316
 
302
317
  ```bash
303
- # 단일 세션: triflux 프로파일로 attach
304
- wt.exe -w new -p triflux --title "{title1}" psmux attach-session -t codex-swarm-{id1}
305
-
306
- # 다중 세션: split-pane (상/하 분할)
307
- wt.exe -w new \
308
- -p triflux --title "{title1}" psmux attach-session -t codex-swarm-{id1} \; \
309
- split-pane -H -p triflux --title "{title2}" psmux attach-session -t codex-swarm-{id2}
310
-
311
- # 3개 이상: 추가 split-pane
312
- wt.exe -w new \
313
- -p triflux --title "{title1}" psmux attach-session -t codex-swarm-{id1} \; \
314
- split-pane -H -p triflux --title "{title2}" psmux attach-session -t codex-swarm-{id2} \; \
315
- split-pane -V -p triflux --title "{title3}" psmux attach-session -t codex-swarm-{id3}
318
+ # 2개: 상하 분할
319
+ wt.exe -w 0 \
320
+ sp -H -p triflux --title "{t1}" psmux attach-session -t {id1} \; \
321
+ sp -V -p triflux --title "{t2}" psmux attach-session -t {id2}
322
+
323
+ # 4개: 2x2 그리드
324
+ wt.exe -w 0 \
325
+ sp -H -p triflux --title "{t1}" psmux attach-session -t {id1} \; \
326
+ sp -V -p triflux --title "{t2}" psmux attach-session -t {id2} \; \
327
+ move-focus up \; \
328
+ sp -V -p triflux --title "{t3}" psmux attach-session -t {id3} \; \
329
+ move-focus down \; \
330
+ sp -V -p triflux --title "{t4}" psmux attach-session -t {id4}
331
+ ```
332
+
333
+ 5개 이상이면 AskUserQuestion으로 확인 후 dashboard 모드 제안:
334
+ ```bash
335
+ # dashboard 모드 (모니터링 전용)
336
+ wt.exe -w 0 -p triflux --title "swarm-dashboard" bash -c '
337
+ while true; do
338
+ clear; echo "=== Codex Swarm Dashboard ==="
339
+ for s in $(psmux list-sessions -F "#{session_name}" 2>/dev/null | grep codex-swarm); do
340
+ echo " [$s] $(psmux capture-pane -t "$s" -p | tail -1)"
341
+ done
342
+ sleep 10
343
+ done
344
+ '
316
345
  ```
317
346
 
318
347
  ### Step 9: 상태 보고
@@ -432,6 +461,21 @@ for bin in codex psmux git; do
432
461
  exit 1
433
462
  }
434
463
  done
464
+
465
+ # MCP 싱글톤 데몬 검사 (MCP_DAEMON_REQUIRED=true일 때)
466
+ if [ "$MCP_DAEMON_REQUIRED" != "false" ]; then
467
+ DAEMON_OK=true
468
+ for port in 9001 9002 9003 9004 9005; do
469
+ curl -s --max-time 1 "http://localhost:$port/sse" >/dev/null 2>&1 || {
470
+ DAEMON_OK=false
471
+ break
472
+ }
473
+ done
474
+ if [ "$DAEMON_OK" = "false" ]; then
475
+ echo "WARN: MCP daemons not running. Starting..."
476
+ powershell -ExecutionPolicy Bypass -File "$HOME/.codex/mcp-daemon/start-daemons.ps1"
477
+ fi
478
+ fi
435
479
  ```
436
480
 
437
481
  ## 정리
@@ -0,0 +1,54 @@
1
+ # MCP Singleton Daemons - supergateway wrapper
2
+ # Each OMX MCP server runs once, codex sessions connect via mcp-remote
3
+ # Usage: powershell -ExecutionPolicy Bypass -File start-daemons.ps1
4
+
5
+ $OMX_BASE = "$env:APPDATA/npm/node_modules/oh-my-codex/dist/mcp"
6
+ $SG_CMD = "$env:APPDATA\npm\supergateway.cmd"
7
+ $DAEMON_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path
8
+
9
+ $servers = @(
10
+ @{ Name = "omx_state"; Script = "state-server.js"; Port = 9001 }
11
+ @{ Name = "omx_memory"; Script = "memory-server.js"; Port = 9002 }
12
+ @{ Name = "omx_code_intel"; Script = "code-intel-server.js"; Port = 9003 }
13
+ @{ Name = "omx_trace"; Script = "trace-server.js"; Port = 9004 }
14
+ @{ Name = "omx_team_run"; Script = "team-server.js"; Port = 9005 }
15
+ )
16
+
17
+ foreach ($srv in $servers) {
18
+ $port = $srv.Port
19
+ $name = $srv.Name
20
+ $script = "$OMX_BASE/$($srv.Script)"
21
+
22
+ # Check if already running on this port
23
+ $existing = Get-NetTCPConnection -LocalPort $port -ErrorAction SilentlyContinue
24
+ if ($existing) {
25
+ Write-Host "[SKIP] $name already running on port $port"
26
+ continue
27
+ }
28
+
29
+ # Create individual launcher .cmd
30
+ $launcher = Join-Path $DAEMON_DIR "run-$name.cmd"
31
+ $content = "@echo off`r`ncall `"$SG_CMD`" --stdio `"node $script`" --port $port"
32
+ Set-Content -Path $launcher -Value $content -Encoding ASCII
33
+
34
+ Write-Host "[START] $name on port $port"
35
+ Start-Process -WindowStyle Hidden -FilePath $launcher
36
+
37
+ Start-Sleep -Milliseconds 800
38
+ }
39
+
40
+ Start-Sleep -Milliseconds 1000
41
+
42
+ # Verify
43
+ $ok = 0
44
+ foreach ($srv in $servers) {
45
+ $c = Get-NetTCPConnection -LocalPort $srv.Port -ErrorAction SilentlyContinue
46
+ if ($c) {
47
+ Write-Host "[OK] $($srv.Name) listening on port $($srv.Port)"
48
+ $ok++
49
+ } else {
50
+ Write-Host "[FAIL] $($srv.Name) NOT listening on port $($srv.Port)"
51
+ }
52
+ }
53
+ Write-Host ""
54
+ Write-Host "$ok / $($servers.Count) daemons running"
@@ -0,0 +1,15 @@
1
+ # Stop all MCP singleton daemons
2
+ # Usage: powershell -ExecutionPolicy Bypass -File stop-daemons.ps1
3
+
4
+ 9001..9005 | ForEach-Object {
5
+ $port = $_
6
+ $conn = Get-NetTCPConnection -LocalPort $port -ErrorAction SilentlyContinue
7
+ if ($conn) {
8
+ $pid = $conn[0].OwningProcess
9
+ Write-Host "[STOP] Killing PID $pid on port $port"
10
+ Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue
11
+ } else {
12
+ Write-Host "[SKIP] Nothing on port $port"
13
+ }
14
+ }
15
+ Write-Host "All MCP daemons stopped."