triflux 8.12.5 → 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.
@@ -9,7 +9,7 @@
9
9
  {
10
10
  "name": "triflux",
11
11
  "description": "CLI-first multi-model orchestrator for Claude Code. Routes tasks to Codex, Gemini, and Claude CLIs with automatic triage (Sonnet classification + Opus decomposition), DAG-based parallel execution, and cost-optimized routing. Includes 16 skills, HUD status bar, and shell-based CLI routing wrapper.",
12
- "version": "7.1.4",
12
+ "version": "9.0.0",
13
13
  "author": {
14
14
  "name": "tellang"
15
15
  },
@@ -27,5 +27,5 @@
27
27
  ]
28
28
  }
29
29
  ],
30
- "version": "7.1.4"
30
+ "version": "9.0.0"
31
31
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "8.11.2",
3
+ "version": "9.0.0",
4
4
  "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
5
  "author": {
6
6
  "name": "tellang"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "8.12.5",
3
+ "version": "9.0.0",
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": {
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, readFileSync, unlinkSync } from "node:fs";
4
+ import { join } from "node:path";
5
+
6
+ const SESSION_TTL_SEC = 30 * 60;
7
+ const STATE_REL_PATH = join(".omc", "state", "cross-review.json");
8
+
9
+ function readStdin() {
10
+ return new Promise((resolve) => {
11
+ let raw = "";
12
+ process.stdin.setEncoding("utf8");
13
+ process.stdin.on("data", (chunk) => {
14
+ raw += chunk;
15
+ });
16
+ process.stdin.on("end", () => resolve(raw));
17
+ process.stdin.on("error", () => resolve(""));
18
+ });
19
+ }
20
+
21
+ function parseJson(raw) {
22
+ try {
23
+ return JSON.parse(raw);
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ function nowSec() {
30
+ return Math.floor(Date.now() / 1000);
31
+ }
32
+
33
+ function resolveBaseDir(payload) {
34
+ if (typeof payload?.cwd === "string" && payload.cwd.trim()) return payload.cwd;
35
+ if (typeof payload?.directory === "string" && payload.directory.trim()) return payload.directory;
36
+ return process.cwd();
37
+ }
38
+
39
+ function expectedReviewer(author) {
40
+ if (author === "claude") return "codex";
41
+ if (author === "codex") return "claude";
42
+ if (author === "gemini") return "claude";
43
+ return "";
44
+ }
45
+
46
+ function shouldTrackPath(filePath) {
47
+ if (typeof filePath !== "string" || !filePath.trim()) return false;
48
+
49
+ const lower = filePath.toLowerCase();
50
+ if (lower.startsWith(".omc/") || lower.startsWith(".claude/")) return false;
51
+ if (lower === "package-lock.json" || lower.endsWith("/package-lock.json")) return false;
52
+ if (/\.(md|lock|yml|yaml)$/i.test(lower)) return false;
53
+ return true;
54
+ }
55
+
56
+ function loadState(statePath) {
57
+ if (!existsSync(statePath)) return null;
58
+
59
+ try {
60
+ const state = JSON.parse(readFileSync(statePath, "utf8"));
61
+ const startedAt = Number(state?.session_start || 0);
62
+ const expired = !startedAt || nowSec() - startedAt > SESSION_TTL_SEC;
63
+ if (expired) {
64
+ try {
65
+ unlinkSync(statePath);
66
+ } catch {}
67
+ return null;
68
+ }
69
+ return state;
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ function isGitCommitCommand(command) {
76
+ if (typeof command !== "string") return false;
77
+ return /\bgit\s+commit\b/i.test(command);
78
+ }
79
+
80
+ function nudge(message) {
81
+ process.stdout.write(JSON.stringify({
82
+ hookSpecificOutput: {
83
+ hookEventName: "PreToolUse",
84
+ additionalContext: message,
85
+ },
86
+ }));
87
+ process.exit(0);
88
+ }
89
+
90
+ function deny(message) {
91
+ process.stderr.write(message);
92
+ process.exit(2);
93
+ }
94
+
95
+ function summarizePending(entries) {
96
+ return entries
97
+ .map((item) => {
98
+ const reviewer = item.expectedReviewer || "cross-reviewer";
99
+ return ` * ${item.path} (author=${item.author}, reviewer=${reviewer})`;
100
+ })
101
+ .join("\n");
102
+ }
103
+
104
+ async function main() {
105
+ if (process.env.TFX_SKIP_CROSS_REVIEW === "1") {
106
+ process.exit(0);
107
+ }
108
+
109
+ const raw = await readStdin();
110
+ if (!raw.trim()) process.exit(0);
111
+
112
+ const payload = parseJson(raw);
113
+ if (!payload) process.exit(0);
114
+
115
+ const toolName = payload.tool_name || "";
116
+ const toolInput = payload.tool_input || {};
117
+
118
+ if (toolName !== "Bash") process.exit(0);
119
+ if (!isGitCommitCommand(toolInput.command || "")) process.exit(0);
120
+
121
+ // cwd 전파: tracker와 동일한 resolveBaseDir 사용
122
+ const baseDir = resolveBaseDir(payload);
123
+ const statePath = join(baseDir, STATE_REL_PATH);
124
+
125
+ const state = loadState(statePath);
126
+ if (!state || !state.files || typeof state.files !== "object") process.exit(0);
127
+
128
+ const pending = [];
129
+ const selfApproved = [];
130
+
131
+ for (const [path, info] of Object.entries(state.files)) {
132
+ if (!shouldTrackPath(path)) continue;
133
+ const meta = info && typeof info === "object" ? info : {};
134
+ const author = String(meta.author || "").toLowerCase();
135
+ const reviewer = String(meta.reviewer || "").toLowerCase();
136
+ const reviewed = meta.reviewed === true;
137
+ const requiredReviewer = expectedReviewer(author);
138
+
139
+ // tracker가 설정한 self_approved 플래그 명시적 체크
140
+ if (meta.self_approved === true) {
141
+ selfApproved.push({ path, author, reviewer: meta.reviewer || author, expectedReviewer: requiredReviewer });
142
+ continue;
143
+ }
144
+
145
+ if (reviewed && reviewer && reviewer === author) {
146
+ selfApproved.push({ path, author, reviewer, expectedReviewer: requiredReviewer });
147
+ continue;
148
+ }
149
+
150
+ if (reviewed && requiredReviewer && reviewer && reviewer !== requiredReviewer) {
151
+ selfApproved.push({ path, author, reviewer, expectedReviewer: requiredReviewer });
152
+ continue;
153
+ }
154
+
155
+ if (!reviewed) {
156
+ pending.push({ path, author, expectedReviewer: requiredReviewer });
157
+ }
158
+ }
159
+
160
+ if (selfApproved.length > 0) {
161
+ const lines = selfApproved
162
+ .map((item) => ` * ${item.path} (author=${item.author}, reviewer=${item.reviewer}, required=${item.expectedReviewer || "n/a"})`)
163
+ .join("\n");
164
+ deny(
165
+ `[cross-review] self-approve 차단: 동일/비허용 reviewer가 감지되었습니다.\n${lines}\n` +
166
+ "규칙: author=claude -> reviewer=codex, author=codex -> reviewer=claude",
167
+ );
168
+ }
169
+
170
+ if (pending.length > 0) {
171
+ nudge(
172
+ `[cross-review] git commit 전에 교차 검증이 필요합니다.\n${summarizePending(pending)}\n` +
173
+ "규칙: author=claude -> reviewer=codex, author=codex -> reviewer=claude",
174
+ );
175
+ }
176
+
177
+ process.exit(0);
178
+ }
179
+
180
+ main().catch(() => process.exit(0));
@@ -0,0 +1,279 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
4
+ import { dirname, isAbsolute, join, relative } from "node:path";
5
+
6
+ const SESSION_TTL_SEC = 30 * 60;
7
+ const STATE_REL_PATH = join(".omc", "state", "cross-review.json");
8
+ const EXCLUDED_FILE_PATTERN = /\.(md|lock|yml|yaml)$/i;
9
+
10
+ function nowSec() {
11
+ return Math.floor(Date.now() / 1000);
12
+ }
13
+
14
+ function readStdin() {
15
+ return new Promise((resolve) => {
16
+ let raw = "";
17
+ process.stdin.setEncoding("utf8");
18
+ process.stdin.on("data", (chunk) => {
19
+ raw += chunk;
20
+ });
21
+ process.stdin.on("end", () => resolve(raw));
22
+ process.stdin.on("error", () => resolve(""));
23
+ });
24
+ }
25
+
26
+ function parseJson(raw) {
27
+ try {
28
+ return JSON.parse(raw);
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ function resolveBaseDir(payload) {
35
+ if (typeof payload?.cwd === "string" && payload.cwd.trim()) return payload.cwd;
36
+ if (typeof payload?.directory === "string" && payload.directory.trim()) return payload.directory;
37
+ return process.cwd();
38
+ }
39
+
40
+ function resolveStatePath(baseDir) {
41
+ return join(baseDir, STATE_REL_PATH);
42
+ }
43
+
44
+ function createEmptyState() {
45
+ return {
46
+ files: {},
47
+ session_start: nowSec(),
48
+ };
49
+ }
50
+
51
+ function loadState(statePath) {
52
+ if (!existsSync(statePath)) return createEmptyState();
53
+
54
+ try {
55
+ const parsed = JSON.parse(readFileSync(statePath, "utf8"));
56
+ const sessionStart = Number(parsed?.session_start || 0);
57
+ const expired = !sessionStart || nowSec() - sessionStart > SESSION_TTL_SEC;
58
+ if (expired) {
59
+ try {
60
+ unlinkSync(statePath);
61
+ } catch {}
62
+ return createEmptyState();
63
+ }
64
+
65
+ return {
66
+ files: parsed?.files && typeof parsed.files === "object" ? parsed.files : {},
67
+ session_start: sessionStart,
68
+ };
69
+ } catch {
70
+ return createEmptyState();
71
+ }
72
+ }
73
+
74
+ function saveState(statePath, state) {
75
+ mkdirSync(dirname(statePath), { recursive: true });
76
+ writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
77
+ }
78
+
79
+ function normalizePath(filePath, baseDir) {
80
+ if (typeof filePath !== "string" || !filePath.trim()) return "";
81
+
82
+ const raw = filePath.trim();
83
+ let normalized = raw;
84
+
85
+ if (isAbsolute(raw)) {
86
+ const relPath = relative(baseDir, raw);
87
+ if (relPath.startsWith("..")) return "";
88
+ normalized = relPath;
89
+ }
90
+
91
+ return normalized.replace(/\\/g, "/").replace(/^\.\//, "");
92
+ }
93
+
94
+ function shouldTrackPath(filePath) {
95
+ if (!filePath) return false;
96
+ const lower = filePath.toLowerCase();
97
+
98
+ if (lower.startsWith(".omc/") || lower.startsWith(".claude/")) return false;
99
+ if (lower === "package-lock.json" || lower.endsWith("/package-lock.json")) return false;
100
+ if (EXCLUDED_FILE_PATTERN.test(lower)) return false;
101
+ return true;
102
+ }
103
+
104
+ function extractFilePath(toolInput) {
105
+ if (!toolInput || typeof toolInput !== "object") return "";
106
+ const candidate = toolInput.file_path ?? toolInput.path ?? toolInput.filePath ?? "";
107
+ return typeof candidate === "string" ? candidate : "";
108
+ }
109
+
110
+ function extractCandidatePaths(payload, baseDir) {
111
+ const candidates = new Set();
112
+
113
+ const looksLikePath = (value) => {
114
+ if (typeof value !== "string") return false;
115
+ const trimmed = value.trim();
116
+ if (!trimmed || /\s/.test(trimmed)) return false;
117
+ if (trimmed.length > 260) return false;
118
+ if (!trimmed.includes(".") && !trimmed.includes("/") && !trimmed.includes("\\")) return false;
119
+ return /^[./\\A-Za-z0-9_-]/.test(trimmed);
120
+ };
121
+
122
+ const addPath = (value) => {
123
+ if (!looksLikePath(value)) return;
124
+ const normalized = normalizePath(value, baseDir);
125
+ if (shouldTrackPath(normalized)) candidates.add(normalized);
126
+ };
127
+
128
+ const scanValue = (value, depth = 0) => {
129
+ if (depth > 3 || value == null) return;
130
+ if (typeof value === "string") {
131
+ addPath(value);
132
+ return;
133
+ }
134
+ if (Array.isArray(value)) {
135
+ for (const item of value) scanValue(item, depth + 1);
136
+ return;
137
+ }
138
+ if (typeof value !== "object") return;
139
+
140
+ for (const [key, child] of Object.entries(value)) {
141
+ const keyLower = key.toLowerCase();
142
+ if (keyLower.includes("file") || keyLower.includes("path")) {
143
+ scanValue(child, depth + 1);
144
+ }
145
+ }
146
+ };
147
+
148
+ addPath(extractFilePath(payload?.tool_input));
149
+
150
+ scanValue(payload?.tool_response);
151
+ scanValue(payload?.tool_output);
152
+ scanValue(payload?.result);
153
+ scanValue(payload?.output);
154
+
155
+ return [...candidates];
156
+ }
157
+
158
+ function collectStrings(value, out = [], depth = 0) {
159
+ if (depth > 4) return out;
160
+ if (typeof value === "string") {
161
+ out.push(value);
162
+ return out;
163
+ }
164
+ if (!value || typeof value !== "object") return out;
165
+ if (Array.isArray(value)) {
166
+ for (const item of value) collectStrings(item, out, depth + 1);
167
+ return out;
168
+ }
169
+
170
+ for (const key of Object.keys(value)) {
171
+ collectStrings(value[key], out, depth + 1);
172
+ }
173
+ return out;
174
+ }
175
+
176
+ function detectCliActor(payload) {
177
+ const lines = collectStrings(payload).join("\n");
178
+ const match = lines.match(/\bcli\s*[:=]\s*(claude|codex|gemini)\b/i);
179
+ return match ? match[1].toLowerCase() : "";
180
+ }
181
+
182
+ function detectAuthor(payload) {
183
+ const actor = detectCliActor(payload);
184
+ if (actor) return actor;
185
+ return "claude";
186
+ }
187
+
188
+ function expectedReviewer(author) {
189
+ if (author === "claude") return "codex";
190
+ if (author === "codex") return "claude";
191
+ if (author === "gemini") return "claude";
192
+ return "";
193
+ }
194
+
195
+ function applyReviewer(state, reviewer, ts) {
196
+ for (const [filePath, meta] of Object.entries(state.files)) {
197
+ if (!meta || typeof meta !== "object") continue;
198
+ if (!shouldTrackPath(filePath)) continue;
199
+
200
+ const author = String(meta.author || "").toLowerCase();
201
+ const expected = expectedReviewer(author);
202
+
203
+ if (expected && reviewer === expected) {
204
+ meta.reviewed = true;
205
+ meta.reviewer = reviewer;
206
+ meta.reviewed_ts = ts;
207
+ delete meta.self_approved;
208
+ continue;
209
+ }
210
+
211
+ if (reviewer === author) {
212
+ meta.reviewed = false;
213
+ meta.reviewer = reviewer;
214
+ meta.reviewed_ts = ts;
215
+ meta.self_approved = true;
216
+ }
217
+ }
218
+ }
219
+
220
+ async function main() {
221
+ if (process.env.TFX_SKIP_CROSS_REVIEW === "1") {
222
+ process.exit(0);
223
+ }
224
+
225
+ const raw = await readStdin();
226
+ if (!raw.trim()) {
227
+ process.exit(0);
228
+ }
229
+
230
+ const payload = parseJson(raw);
231
+ if (!payload) {
232
+ process.exit(0);
233
+ }
234
+
235
+ const baseDir = resolveBaseDir(payload);
236
+ const statePath = resolveStatePath(baseDir);
237
+ const state = loadState(statePath);
238
+ const toolName = payload.tool_name || "";
239
+ const ts = nowSec();
240
+ let changed = false;
241
+
242
+ if (toolName === "Edit" || toolName === "Write") {
243
+ const toolInput = payload.tool_input || {};
244
+ const normalizedPath = normalizePath(extractFilePath(toolInput), baseDir);
245
+ if (shouldTrackPath(normalizedPath)) {
246
+ state.files[normalizedPath] = {
247
+ author: detectAuthor(payload),
248
+ ts,
249
+ reviewed: false,
250
+ };
251
+ changed = true;
252
+ }
253
+ } else if (toolName === "Bash") {
254
+ const actor = detectCliActor(payload);
255
+ if (actor) {
256
+ const paths = extractCandidatePaths(payload, baseDir);
257
+ if (paths.length > 0) {
258
+ for (const path of paths) {
259
+ state.files[path] = {
260
+ author: actor,
261
+ ts,
262
+ reviewed: false,
263
+ };
264
+ }
265
+ } else {
266
+ applyReviewer(state, actor, ts);
267
+ }
268
+ changed = true;
269
+ }
270
+ }
271
+
272
+ if (changed) {
273
+ saveState(statePath, state);
274
+ }
275
+
276
+ process.exit(0);
277
+ }
278
+
279
+ main().catch(() => process.exit(0));
@@ -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
+ }
@@ -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
  ## 용도
@@ -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,7 @@ argument-hint: "\"작업 설명\" | N:codex \"작업 설명\""
8
8
 
9
9
  # tfx-codex — Codex-Only 오케스트레이터
10
10
 
11
+ > **래퍼**: tfx-auto의 Codex 전용 바로가기. TFX_CLI_MODE=codex.
11
12
  > **HARD RULE**: Claude는 이 스킬에서 Edit/Write를 사용하면 안 된다. 모든 코드 수정은 Codex CLI를 통해 수행한다.
12
13
  > Codex CLI만 사용하여 모든 외부 CLI 작업을 라우팅합니다.
13
14
  > Gemini CLI가 없는 환경에서 사용합니다.
@@ -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,33 +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
- > **레이아웃 규칙:**
301
- > - 세션 1-3개: split-pane only (탭 불필요)
302
- > - 세션 4개+: **테마/웨이브별 탭 그룹핑** + 탭 내부 split-pane
303
- > - 같은 카테고리/웨이브는 하나의 탭에 묶는다
304
- > - 탭 내부 2-4개 split (가로 `-H` / 세로 `-V` 적절 배분)
305
- > - 사용자가 탭/split 레이아웃을 명시하면 해당 지시를 우선한다
315
+ > 기본은 **split-pane 직접 attach**. 4개 이하 2x2 그리드, 5개 이상은 사용자 확인.
306
316
 
307
317
  ```bash
308
- # 세션 1-3개: split-pane only
309
- wt.exe -w new \
310
- -p triflux --title "{title1}" psmux attach-session -t codex-swarm-{id1} \; \
311
- split-pane -H -p triflux --title "{title2}" psmux attach-session -t codex-swarm-{id2} \; \
312
- 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}
313
322
 
314
- # 세션 4개+: 테마별 탭 + 탭 내부 split (예: Wave1=3개, Wave2=4개)
323
+ # 4개: 2x2 그리드
315
324
  wt.exe -w 0 \
316
- -p triflux --title "W1:{title1}" psmux attach-session -t {id1} \; \
317
- split-pane -H -p triflux --title "W1:{title2}" psmux attach-session -t {id2} \; \
318
- split-pane -V -p triflux --title "W1:{title3}" psmux attach-session -t {id3} \; \
319
- new-tab -p triflux --title "W2:{title4}" psmux attach-session -t {id4} \; \
320
- split-pane -H -p triflux --title "W2:{title5}" psmux attach-session -t {id5} \; \
321
- split-pane -V -p triflux --title "W2:{title6}" psmux attach-session -t {id6} \; \
322
- move-focus up \; split-pane -V -p triflux --title "W2:{title7}" psmux attach-session -t {id7}
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
+ '
323
345
  ```
324
346
 
325
347
  ### Step 9: 상태 보고
@@ -439,6 +461,21 @@ for bin in codex psmux git; do
439
461
  exit 1
440
462
  }
441
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
442
479
  ```
443
480
 
444
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."
@@ -7,6 +7,7 @@ argument-hint: "(내부 전용 — Deep 스킬이 자동 호출)"
7
7
 
8
8
  # tfx-consensus — Tri-CLI Consensus Engine
9
9
 
10
+ > **인프라**: 다른 스킬이 내부적으로 사용. 직접 호출할 필요 없음.
10
11
  > 모든 Deep 스킬의 공통 기반. 3개 CLI의 독립 결과를 교차검증하여 합의 도출.
11
12
 
12
13
  ## HARD RULES
@@ -10,6 +10,7 @@ argument-hint: "<분석 대상 — 파일, 디렉토리, 또는 주제>"
10
10
 
11
11
  # tfx-deep-analysis — Tri-CLI Deep Analysis
12
12
 
13
+ > **Light 버전**: tfx-analysis. 기본값. 깊이 수정자 없으면 Light 선택.
13
14
  > Claude(아키텍처) + Codex(구현/보안) + Gemini(UX/문서화) → Tri-Debate → 합의.
14
15
  > 3자 전문 관점의 편향 없는 분석.
15
16
 
@@ -13,6 +13,7 @@ argument-hint: "<구현할 기능 설명>"
13
13
 
14
14
  # tfx-deep-plan — Tri-Model Consensus Planning
15
15
 
16
+ > **Light 버전**: tfx-plan. 기본값. 깊이 수정자 없으면 Light 선택.
16
17
  3개 모델(Opus 4.6 · GPT 5.4 · Gemini)이 독립 설계 후 교차검증하여 합의된 계획을 도출한다.
17
18
 
18
19
  ---
@@ -11,6 +11,7 @@ argument-hint: "[테스트 대상 경로 또는 기능 설명]"
11
11
 
12
12
  # tfx-deep-qa — Tri-CLI Deep Verification
13
13
 
14
+ > **Light 버전**: tfx-qa. 기본값. 깊이 수정자 없으면 Light 선택.
14
15
  > 3-CLI 독립 검증 → 교차검증 → 2+ 합의 항목만 보고. false-positive 87% 감소.
15
16
 
16
17
  ## 핵심 원리
@@ -14,6 +14,7 @@ argument-hint: "[--depth quick|standard|deep] <리서치 주제>"
14
14
 
15
15
  # tfx-deep-research — Multi-Source Deep Research with Tri-CLI Consensus
16
16
 
17
+ > **Light 버전**: tfx-research. 기본값. 깊이 수정자 없으면 Light 선택.
17
18
  > 쿼리 분해 → 3-CLI 독립 병렬 검색 → 교차검증 → 합의 기반 종합 보고서.
18
19
  > STORM(Stanford) perspective-guided + GPT-Researcher recursive tree + Tavily deep research pipeline 영감.
19
20
 
@@ -12,6 +12,7 @@ argument-hint: "[파일 경로 또는 변경 설명]"
12
12
 
13
13
  # tfx-deep-review — Tri-CLI Deep Code Review
14
14
 
15
+ > **Light 버전**: tfx-review. 기본값. 깊이 수정자 없으면 Light 선택.
15
16
  > 3-CLI 독립 리뷰 → 교차검증 → 2+ 합의 항목만 보고. Diffray + Calimero 영감.
16
17
 
17
18
  ## 핵심 원리
@@ -8,6 +8,7 @@ argument-hint: "\"작업 설명\" | N:gemini \"작업 설명\""
8
8
 
9
9
  # tfx-gemini — Gemini-Only 오케스트레이터
10
10
 
11
+ > **래퍼**: tfx-auto의 Gemini 전용 바로가기. TFX_CLI_MODE=gemini.
11
12
  > Gemini CLI만 사용하여 모든 외부 CLI 작업을 라우팅합니다.
12
13
  > Codex CLI가 없는 환경에서 사용합니다.
13
14
 
@@ -11,6 +11,7 @@ argument-hint: "<start|stop|status|자유형 작업 설명>"
11
11
 
12
12
  # tfx-hub — MCP 메시지 버스 관리 + 개방형 작업
13
13
 
14
+ > **인프라**: 다른 스킬이 내부적으로 사용. 직접 호출할 필요 없음.
14
15
  > CLI 에이전트(Codex/Gemini/Claude) 간 실시간 메시지 허브를 관리합니다.
15
16
  > **커맨드 매칭 + fallthrough**: start/stop/status에 매칭되면 즉시 실행,
16
17
  > 매칭 안 되면 **hub 도메인 컨텍스트를 활용한 범용 작업**으로 처리합니다.
@@ -8,6 +8,7 @@ argument-hint: '"작업 설명" | --agents codex,gemini "작업" | --tmux "작
8
8
 
9
9
  # tfx-multi v3 — 파이프라인 기반 멀티-CLI 팀 오케스트레이터
10
10
 
11
+ > **인프라**: 다른 스킬이 내부적으로 사용. 직접 호출할 필요 없음.
11
12
  > Claude Code Native Teams의 Shift+Down 네비게이션을 복원한다.
12
13
  > Codex/Gemini 워커마다 최소 프롬프트(~100 토큰)의 슬림 Agent 래퍼를 spawn하여 네비게이션에 등록하고,
13
14
  > 실제 작업은 `tfx-route.sh`가 수행한다. task 상태는 `team_task_list`를 truth source로 검증한다.
@@ -11,6 +11,7 @@ argument-hint: "<구현할 기능 설명>"
11
11
 
12
12
  # tfx-plan — Light Implementation Plan
13
13
 
14
+ > **Deep 버전**: tfx-deep-plan. "제대로/꼼꼼히" 수정자로 자동 에스컬레이션.
14
15
  > Claude Opus 단독 빠른 계획. 복잡한 합의 없이 즉시 태스크 분해.
15
16
 
16
17
  ## 워크플로우
@@ -11,6 +11,7 @@ argument-hint: "[테스트 명령 또는 파일 경로]"
11
11
 
12
12
  # tfx-qa — Light Test-Fix Cycle
13
13
 
14
+ > **Deep 버전**: tfx-deep-qa. "제대로/꼼꼼히" 수정자로 자동 에스컬레이션.
14
15
  > 테스트 실행 → 실패 분석 → 자동 수정 → 재실행. 최대 3회. OMC ultraqa 영감.
15
16
 
16
17
  ## 용도
@@ -2,16 +2,13 @@
2
2
  name: tfx-ralph
3
3
  description: "작업이 완전히 끝날 때까지 멈추지 않고 반복 실행해야 할 때 사용한다. 'ralph', '끝까지 해', '멈추지 마', 'don't stop', '완료될 때까지', '다 될 때까지 계속' 같은 요청에 반드시 사용. 여러 기준을 모두 충족해야 하는 복잡한 구현 작업에 적극 활용."
4
4
  triggers:
5
- - ralph
6
- - don't stop
7
- - 끝까지
8
- - until done
9
- - 멈추지 마
5
+ - tfx-ralph
10
6
  argument-hint: "<완료할 작업 설명>"
11
7
  ---
12
8
 
13
9
  # tfx-ralph — Alias for tfx-persist
14
10
 
11
+ > **래퍼**: tfx-persist의 alias. 동일 기능.
15
12
  > `tfx-ralph`는 `tfx-persist`의 alias이다. 동일한 Tri-Verified Persistence Loop을 실행한다.
16
13
  > The boulder never stops — but it stops being wrong.
17
14
 
@@ -13,6 +13,7 @@ argument-hint: "<검색 주제>"
13
13
 
14
14
  # tfx-research — Light Web Research
15
15
 
16
+ > **Deep 버전**: tfx-deep-research. "제대로/꼼꼼히" 수정자로 자동 에스컬레이션.
16
17
  > 빠른 단일 소스 검색 + 요약. 토큰 최소화, 즉시 결과.
17
18
 
18
19
  ## 용도
@@ -11,6 +11,7 @@ argument-hint: "[파일 경로 또는 변경 설명]"
11
11
 
12
12
  # tfx-review — Light Code Review
13
13
 
14
+ > **Deep 버전**: tfx-deep-review. "제대로/꼼꼼히" 수정자로 자동 에스컬레이션.
14
15
  > **HARD RULE**: 리뷰 결과를 생성할 때 Claude가 직접 git log/diff를 실행하지 마라. Codex code-reviewer에게 위임하라.
15
16
  > Codex 단일 리뷰로 빠른 피드백. 토큰 최소화.
16
17