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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/bin/triflux.mjs +64 -0
- package/hub/team/backend.mjs +2 -1
- package/hub/team/cli/commands/start/index.mjs +2 -2
- package/hub/team/cli/commands/start/parse-args.mjs +10 -0
- package/hub/workers/delegator-mcp.mjs +2 -5
- package/package.json +1 -1
- package/scripts/cache-buildup.mjs +24 -395
- package/scripts/cache-doctor.mjs +149 -0
- package/scripts/cache-warmup.mjs +514 -0
- package/scripts/cross-review-gate.mjs +180 -0
- package/scripts/cross-review-tracker.mjs +279 -0
- package/scripts/headless-guard.mjs +38 -0
- package/scripts/lib/env-probe.mjs +130 -0
- package/scripts/lib/mcp-filter.mjs +730 -720
- package/scripts/lib/mcp-manifest.mjs +79 -0
- package/scripts/mcp-gateway-config.mjs +104 -7
- package/scripts/mcp-gateway-start.mjs +7 -0
- package/scripts/mcp-gateway-verify.mjs +15 -1
- package/scripts/preflight-cache.mjs +68 -137
- package/scripts/session-spawn-helper.mjs +184 -0
- package/scripts/setup.mjs +7 -8
- package/scripts/tfx-route-worker.mjs +59 -1
- package/skills/merge-worktree/SKILL.md +144 -0
- package/skills/tfx-analysis/SKILL.md +1 -0
- package/skills/tfx-auto/SKILL.md +1 -0
- package/skills/tfx-auto-codex/SKILL.md +1 -0
- package/skills/tfx-autopilot/SKILL.md +1 -2
- package/skills/tfx-codex/SKILL.md +2 -0
- package/skills/tfx-codex-swarm/SKILL.md +62 -18
- package/skills/tfx-codex-swarm/mcp-daemon/start-daemons.ps1 +54 -0
- package/skills/tfx-codex-swarm/mcp-daemon/stop-daemons.ps1 +15 -0
- package/skills/tfx-consensus/SKILL.md +1 -0
- package/skills/tfx-deep-analysis/SKILL.md +1 -0
- package/skills/tfx-deep-plan/SKILL.md +1 -0
- package/skills/tfx-deep-qa/SKILL.md +1 -0
- package/skills/tfx-deep-research/SKILL.md +1 -0
- package/skills/tfx-deep-review/SKILL.md +1 -0
- package/skills/tfx-doctor/SKILL.md +5 -0
- package/skills/tfx-gemini/SKILL.md +1 -0
- package/skills/tfx-hub/SKILL.md +1 -0
- package/skills/tfx-multi/SKILL.md +1 -0
- package/skills/tfx-plan/SKILL.md +1 -0
- package/skills/tfx-qa/SKILL.md +1 -0
- package/skills/tfx-ralph/SKILL.md +2 -5
- package/skills/tfx-research/SKILL.md +1 -0
- package/skills/tfx-review/SKILL.md +2 -0
- 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
|
-
// ── 캐시
|
|
828
|
+
// ── Step 6. 캐시 웜업 Phase 1 ──
|
|
828
829
|
|
|
829
|
-
|
|
830
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
+
* 예상치 못한 상황이면 추측하지 말고 **중지 후 설명**
|
package/skills/tfx-auto/SKILL.md
CHANGED
|
@@ -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', '자동으로', '알아서 해', '그냥 해줘'
|
|
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
|
|
312
|
+
### Step 8: WT attach
|
|
297
313
|
|
|
298
314
|
> WT `triflux` 프로파일 사용 필수 (`commandline: "psmux"`, One Half Dark, acrylic).
|
|
299
|
-
>
|
|
300
|
-
> 탭 대신 split-pane 사용 (feedback: WT 새탭 금지, split+dashboard 기본).
|
|
315
|
+
> 기본은 **split-pane 직접 attach**. 4개 이하 2x2 그리드, 5개 이상은 사용자 확인.
|
|
301
316
|
|
|
302
317
|
```bash
|
|
303
|
-
#
|
|
304
|
-
wt.exe -w
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
-p triflux --title "{
|
|
314
|
-
|
|
315
|
-
|
|
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."
|