triflux 5.1.2 → 6.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/bin/tfx-doctor.mjs +3 -7
- package/bin/tfx-setup.mjs +3 -7
- package/bin/triflux.mjs +2470 -2463
- package/hub/team/cli/commands/start/index.mjs +6 -3
- package/hub/team/cli/commands/start/start-headless.mjs +76 -0
- package/hub/team/cli/services/runtime-mode.mjs +1 -0
- package/hub/team/headless.mjs +448 -0
- package/hub/team/psmux.mjs +61 -9
- package/package.json +1 -1
- package/scripts/lib/logger.mjs +4 -4
- package/scripts/lib/mcp-filter.mjs +4 -1
- package/scripts/tfx-route.sh +15 -9
- package/skills/tfx-multi/SKILL.md +53 -3
package/hub/team/psmux.mjs
CHANGED
|
@@ -37,6 +37,10 @@ function sleepMs(ms) {
|
|
|
37
37
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.max(0, ms));
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function sleepMsAsync(ms) {
|
|
41
|
+
return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
42
|
+
}
|
|
43
|
+
|
|
40
44
|
function tokenizeCommand(command) {
|
|
41
45
|
const source = String(command || "").trim();
|
|
42
46
|
if (!source) return [];
|
|
@@ -637,23 +641,68 @@ export function dispatchCommand(sessionName, paneNameOrTarget, commandText) {
|
|
|
637
641
|
* @param {string} paneNameOrTarget
|
|
638
642
|
* @param {string|RegExp} pattern
|
|
639
643
|
* @param {number} timeoutSec
|
|
644
|
+
* @param {object} [opts]
|
|
645
|
+
* @param {(snapshot: {content: string, paneId: string, paneName: string, elapsed: number}) => void} [opts.onPoll] — 각 폴링 주기마다 호출
|
|
640
646
|
* @returns {{ matched: boolean, paneId: string, paneName: string, logPath: string, match: string|null }}
|
|
641
647
|
*/
|
|
642
|
-
export function waitForPattern(sessionName, paneNameOrTarget, pattern, timeoutSec = 300) {
|
|
648
|
+
export async function waitForPattern(sessionName, paneNameOrTarget, pattern, timeoutSec = 300, opts = {}) {
|
|
643
649
|
ensurePsmuxInstalled();
|
|
644
|
-
|
|
650
|
+
|
|
651
|
+
// E4 크래시 복구: 초기 resolvePane도 세션 사망을 감지
|
|
652
|
+
let pane;
|
|
653
|
+
try {
|
|
654
|
+
pane = resolvePane(sessionName, paneNameOrTarget);
|
|
655
|
+
} catch (resolveError) {
|
|
656
|
+
if (!psmuxSessionExists(sessionName)) {
|
|
657
|
+
return {
|
|
658
|
+
matched: false,
|
|
659
|
+
paneId: "",
|
|
660
|
+
paneName: String(paneNameOrTarget),
|
|
661
|
+
logPath: "",
|
|
662
|
+
match: null,
|
|
663
|
+
sessionDead: true,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
throw resolveError; // 세션은 살아있지만 pane을 못 찾음 → 원래 에러 전파
|
|
667
|
+
}
|
|
668
|
+
|
|
645
669
|
const paneName = pane.title || paneNameOrTarget;
|
|
646
670
|
const logPath = getCaptureLogPath(sessionName, paneName);
|
|
647
671
|
if (!existsSync(logPath)) {
|
|
648
672
|
throw new Error(`캡처 로그가 없습니다. 먼저 startCapture(${sessionName}, ${paneName})를 호출하세요.`);
|
|
649
673
|
}
|
|
650
674
|
|
|
651
|
-
const
|
|
675
|
+
const startTime = Date.now();
|
|
676
|
+
const deadline = startTime + Math.max(0, Math.trunc(timeoutSec * 1000));
|
|
652
677
|
const regex = toPatternRegExp(pattern);
|
|
653
678
|
|
|
654
679
|
while (Date.now() <= deadline) {
|
|
655
|
-
|
|
680
|
+
// E4 크래시 복구: capture 실패 시 세션 생존 체크
|
|
681
|
+
try {
|
|
682
|
+
refreshCaptureSnapshot(sessionName, pane.paneId);
|
|
683
|
+
} catch {
|
|
684
|
+
if (!psmuxSessionExists(sessionName)) {
|
|
685
|
+
return {
|
|
686
|
+
matched: false,
|
|
687
|
+
paneId: pane.paneId,
|
|
688
|
+
paneName,
|
|
689
|
+
logPath,
|
|
690
|
+
match: null,
|
|
691
|
+
sessionDead: true,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
// 일시적 오류 — 다음 폴링에서 재시도
|
|
695
|
+
}
|
|
696
|
+
|
|
656
697
|
const content = readCaptureLog(logPath);
|
|
698
|
+
|
|
699
|
+
// onPoll 콜백 — 각 폴링 주기마다 중간 상태 전달
|
|
700
|
+
if (opts.onPoll) {
|
|
701
|
+
try {
|
|
702
|
+
opts.onPoll({ content, paneId: pane.paneId, paneName, elapsed: Date.now() - startTime });
|
|
703
|
+
} catch { /* 콜백 예외는 삼킴 — 폴링 루프 보호 */ }
|
|
704
|
+
}
|
|
705
|
+
|
|
657
706
|
const match = regex.exec(content);
|
|
658
707
|
if (match) {
|
|
659
708
|
return {
|
|
@@ -668,7 +717,7 @@ export function waitForPattern(sessionName, paneNameOrTarget, pattern, timeoutSe
|
|
|
668
717
|
if (Date.now() > deadline) {
|
|
669
718
|
break;
|
|
670
719
|
}
|
|
671
|
-
|
|
720
|
+
await sleepMsAsync(POLL_INTERVAL_MS);
|
|
672
721
|
}
|
|
673
722
|
|
|
674
723
|
return {
|
|
@@ -686,14 +735,15 @@ export function waitForPattern(sessionName, paneNameOrTarget, pattern, timeoutSe
|
|
|
686
735
|
* @param {string} paneNameOrTarget
|
|
687
736
|
* @param {string} token
|
|
688
737
|
* @param {number} timeoutSec
|
|
738
|
+
* @param {object} [opts] — waitForPattern에 전달할 옵션 (onPoll 등)
|
|
689
739
|
* @returns {{ matched: boolean, paneId: string, paneName: string, logPath: string, match: string|null, token: string, exitCode: number|null }}
|
|
690
740
|
*/
|
|
691
|
-
export function waitForCompletion(sessionName, paneNameOrTarget, token, timeoutSec = 300) {
|
|
741
|
+
export async function waitForCompletion(sessionName, paneNameOrTarget, token, timeoutSec = 300, opts = {}) {
|
|
692
742
|
const completionRegex = new RegExp(
|
|
693
743
|
`${escapeRegExp(COMPLETION_PREFIX)}${escapeRegExp(token)}:(\\d+)`,
|
|
694
744
|
"m",
|
|
695
745
|
);
|
|
696
|
-
const result = waitForPattern(sessionName, paneNameOrTarget, completionRegex, timeoutSec);
|
|
746
|
+
const result = await waitForPattern(sessionName, paneNameOrTarget, completionRegex, timeoutSec, opts);
|
|
697
747
|
const exitMatch = result.match ? completionRegex.exec(result.match) : null;
|
|
698
748
|
return {
|
|
699
749
|
...result,
|
|
@@ -840,6 +890,7 @@ export function captureWorkerOutput(sessionName, workerName, lines = 50) {
|
|
|
840
890
|
// ─── CLI 진입점 ───
|
|
841
891
|
|
|
842
892
|
if (process.argv[1] && process.argv[1].endsWith("psmux.mjs")) {
|
|
893
|
+
(async () => {
|
|
843
894
|
const [, , cmd, ...args] = process.argv;
|
|
844
895
|
|
|
845
896
|
// CLI 인자 파싱 헬퍼
|
|
@@ -922,7 +973,7 @@ if (process.argv[1] && process.argv[1].endsWith("psmux.mjs")) {
|
|
|
922
973
|
console.error("사용법: node psmux.mjs wait-pattern --session <세션> --name <pane> --pattern <정규식> [--timeout <초>]");
|
|
923
974
|
process.exit(1);
|
|
924
975
|
}
|
|
925
|
-
const result = waitForPattern(session, name, pattern, timeoutSec);
|
|
976
|
+
const result = await waitForPattern(session, name, pattern, timeoutSec);
|
|
926
977
|
console.log(JSON.stringify(result, null, 2));
|
|
927
978
|
if (!result.matched) process.exit(2);
|
|
928
979
|
break;
|
|
@@ -936,7 +987,7 @@ if (process.argv[1] && process.argv[1].endsWith("psmux.mjs")) {
|
|
|
936
987
|
console.error("사용법: node psmux.mjs wait-completion --session <세션> --name <pane> --token <토큰> [--timeout <초>]");
|
|
937
988
|
process.exit(1);
|
|
938
989
|
}
|
|
939
|
-
const result = waitForCompletion(session, name, token, timeoutSec);
|
|
990
|
+
const result = await waitForCompletion(session, name, token, timeoutSec);
|
|
940
991
|
console.log(JSON.stringify(result, null, 2));
|
|
941
992
|
if (!result.matched) process.exit(2);
|
|
942
993
|
break;
|
|
@@ -958,4 +1009,5 @@ if (process.argv[1] && process.argv[1].endsWith("psmux.mjs")) {
|
|
|
958
1009
|
console.error(`오류: ${err.message}`);
|
|
959
1010
|
process.exit(1);
|
|
960
1011
|
}
|
|
1012
|
+
})();
|
|
961
1013
|
}
|
package/package.json
CHANGED
package/scripts/lib/logger.mjs
CHANGED
|
@@ -93,13 +93,13 @@ export function createModuleLogger(module) {
|
|
|
93
93
|
|
|
94
94
|
// 정상 종료 시 버퍼 flush 보장
|
|
95
95
|
process.on('uncaughtException', (err) => {
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
logger.fatal({ err }, 'process.uncaught_exception');
|
|
97
|
+
logger.flush();
|
|
98
98
|
process.exit(1);
|
|
99
99
|
});
|
|
100
100
|
|
|
101
101
|
process.on('unhandledRejection', (reason) => {
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
logger.fatal({ reason: String(reason) }, 'process.unhandled_rejection');
|
|
103
|
+
logger.flush();
|
|
104
104
|
process.exit(1);
|
|
105
105
|
});
|
|
@@ -176,7 +176,10 @@ function normalizeProfileName(profile) {
|
|
|
176
176
|
if (raw === 'auto') return raw;
|
|
177
177
|
if (PROFILE_DEFINITIONS[raw]) return raw;
|
|
178
178
|
if (LEGACY_PROFILE_ALIASES[raw]) return LEGACY_PROFILE_ALIASES[raw];
|
|
179
|
-
|
|
179
|
+
// graceful fallback: --flag나 잘못된 프로필 → 'auto'로 폴백 (hard crash 방지)
|
|
180
|
+
if (raw.startsWith('-') || raw.startsWith('/')) return 'auto';
|
|
181
|
+
console.error(`[mcp-filter] 경고: 알 수 없는 프로필 '${raw}', 'auto'로 폴백`);
|
|
182
|
+
return 'auto';
|
|
180
183
|
}
|
|
181
184
|
|
|
182
185
|
function resolveAutoProfile(agentType = '') {
|
package/scripts/tfx-route.sh
CHANGED
|
@@ -121,14 +121,7 @@ MCP_PROFILE="${3:-auto}"
|
|
|
121
121
|
USER_TIMEOUT="${4:-}"
|
|
122
122
|
CONTEXT_FILE="${5:-}"
|
|
123
123
|
|
|
124
|
-
# ──
|
|
125
|
-
case "$AGENT_TYPE" in
|
|
126
|
-
codex|gemini|claude|claude-native)
|
|
127
|
-
echo "ERROR: '$AGENT_TYPE'는 CLI 이름이지 에이전트 역할이 아닙니다." >&2
|
|
128
|
-
echo "올바른 사용법: TFX_CLI_MODE=$AGENT_TYPE bash tfx-route.sh <역할> \"프롬프트\"" >&2
|
|
129
|
-
echo "사용 가능한 역할: executor, code-reviewer, scientist, designer, architect, verifier 등" >&2
|
|
130
|
-
exit 64 ;;
|
|
131
|
-
esac
|
|
124
|
+
# ── CLI 이름은 route_agent()에서 기본 역할 alias로 처리됨 (codex→executor, gemini→designer, claude→explore) ──
|
|
132
125
|
|
|
133
126
|
# ── 인자 검증: MCP_PROFILE이 --flag 형태인 경우 거절 ──
|
|
134
127
|
if [[ "$MCP_PROFILE" == --* ]]; then
|
|
@@ -668,11 +661,24 @@ route_agent() {
|
|
|
668
661
|
CLI_TYPE="codex"; CLI_CMD="codex"
|
|
669
662
|
CLI_ARGS="exec --profile spark_fast ${codex_base}"
|
|
670
663
|
CLI_EFFORT="spark_fast"; DEFAULT_TIMEOUT=180; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
|
|
664
|
+
# ─── CLI 이름 alias (사용자 편의) ───
|
|
665
|
+
codex)
|
|
666
|
+
CLI_TYPE="codex"; CLI_CMD="codex"
|
|
667
|
+
CLI_ARGS="exec ${codex_base}"
|
|
668
|
+
CLI_EFFORT="high"; DEFAULT_TIMEOUT=1080; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
|
|
669
|
+
gemini)
|
|
670
|
+
CLI_TYPE="gemini"; CLI_CMD="gemini"
|
|
671
|
+
CLI_ARGS="-m gemini-3.1-pro-preview -y --prompt"
|
|
672
|
+
CLI_EFFORT="pro"; DEFAULT_TIMEOUT=900; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
|
|
673
|
+
claude)
|
|
674
|
+
CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
|
|
675
|
+
CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=600; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
|
|
671
676
|
*)
|
|
672
677
|
echo "ERROR: 알 수 없는 에이전트 타입: $agent" >&2
|
|
673
678
|
echo "사용 가능: executor, build-fixer, debugger, deep-executor, architect, planner, critic, analyst," >&2
|
|
674
679
|
echo " code-reviewer, security-reviewer, quality-reviewer, scientist, document-specialist," >&2
|
|
675
|
-
echo " designer, writer, explore, verifier, test-engineer, qa-tester, spark" >&2
|
|
680
|
+
echo " designer, writer, explore, verifier, test-engineer, qa-tester, spark," >&2
|
|
681
|
+
echo " codex, gemini, claude (CLI alias)" >&2
|
|
676
682
|
exit 1 ;;
|
|
677
683
|
esac
|
|
678
684
|
}
|
|
@@ -148,10 +148,60 @@ status는 "completed"만 사용. 실패 여부는 `metadata.result`로 구분.
|
|
|
148
148
|
3. 실패 시 `forceCleanupTeam(teamName)` → 그래도 실패 시 `rm -rf ~/.claude/teams/{teamName}/` 안내
|
|
149
149
|
4. 종합 보고서 출력
|
|
150
150
|
|
|
151
|
-
### Phase 3-
|
|
151
|
+
### Phase 3-direct: Lead-Direct Headless 실행 (v6.0.0, 기본)
|
|
152
|
+
|
|
153
|
+
CLI 워커(Codex/Gemini/Claude)를 Agent 래퍼 없이 Lead가 headless.mjs로 직접 실행.
|
|
154
|
+
Windows Terminal에 psmux 세션이 자동 팝업되어 사용자가 실시간으로 CLI 출력을 확인.
|
|
155
|
+
|
|
156
|
+
**핵심 기능:**
|
|
157
|
+
- `progressive: true` (기본) — pane이 하나씩 split-window로 추가 (실시간 스플릿)
|
|
158
|
+
- `autoAttach: true` — 세션 생성 즉시 Windows Terminal 자동 팝업
|
|
159
|
+
- `progressIntervalSec` — N초마다 각 pane 스냅샷을 onProgress로 전달
|
|
160
|
+
- `applyTrifluxTheme()` — status bar + pane border 테마 자동 적용
|
|
161
|
+
- 피드백 재실행 — 같은 pane에 후속 명령 dispatch (세션 유지)
|
|
162
|
+
|
|
163
|
+
**Lead 오케스트레이션 패턴:**
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
// headless.mjs의 runHeadlessInteractive()를 Bash 내에서 호출
|
|
167
|
+
// Lead는 Bash의 결과를 직접 파싱 — Agent 래퍼 불필요
|
|
168
|
+
const handle = await runHeadlessInteractive("tfx-session", [
|
|
169
|
+
{ cli: "codex", prompt: "코드 리뷰", role: "reviewer" },
|
|
170
|
+
{ cli: "gemini", prompt: "문서 작성", role: "writer" },
|
|
171
|
+
{ cli: "claude", prompt: "테스트 실행", role: "tester" },
|
|
172
|
+
], {
|
|
173
|
+
timeoutSec: 300,
|
|
174
|
+
autoAttach: true, // WT 자동 팝업
|
|
175
|
+
progressive: true, // 실시간 스플릿 (기본)
|
|
176
|
+
progressIntervalSec: 10, // 10초마다 진행 스냅샷
|
|
177
|
+
});
|
|
178
|
+
// handle: { results, dispatch(), capture(), snapshots(), waitFor(), kill() }
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**결정 로직:**
|
|
182
|
+
```
|
|
183
|
+
Phase 3 선택:
|
|
184
|
+
assignments.every(a => a.cli !== 'claude')
|
|
185
|
+
→ Phase 3-direct (headless, 전부 CLI)
|
|
186
|
+
assignments.some(a => a.cli === 'claude') AND Claude 워커가 Read/Edit 필요
|
|
187
|
+
→ Claude 워커: Agent(subagent_type), CLI 워커: Phase 3-direct
|
|
188
|
+
fallback (psmux 미설치)
|
|
189
|
+
→ Phase 3 Native Teams (기존 slim wrapper)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**CLI 헤드리스 명령 패턴:**
|
|
193
|
+
| CLI | 명령 | 출력 |
|
|
194
|
+
|-----|-------|------|
|
|
195
|
+
| Codex | `codex exec 'prompt' -o result.txt --color never` | 파일 |
|
|
196
|
+
| Gemini | `gemini -p 'prompt' -o text > result.txt 2>result.txt.err` | 리다이렉트 |
|
|
197
|
+
| Claude | `claude -p 'prompt' --output-format text > result.txt 2>&1` | 리다이렉트 |
|
|
198
|
+
|
|
199
|
+
**E4 크래시 복구:** `waitForCompletion`이 세션 사망 시 `{sessionDead: true}` 반환 (throw 대신).
|
|
200
|
+
**elevation 불필요:** psmux IPC는 TCP 기반. 비-elevated 환경에서 정상 실행. (v5.2.0 검증 완료)
|
|
201
|
+
**시각적 확인:** Windows Terminal 자동 팝업 + pane 타이틀 `codex (reviewer)` + triflux 테마.
|
|
202
|
+
**실수로 닫아도:** psmux 세션은 독립적. `psmux attach -t 세션이름`으로 재연결.
|
|
152
203
|
|
|
153
|
-
|
|
154
|
-
`Bash("node {PKG_ROOT}/bin/triflux.mjs multi --no-attach --agents {agents} \\\"{task}\\\"")`
|
|
204
|
+
**레거시 인터랙티브 모드:** `Bash("node {PKG_ROOT}/bin/triflux.mjs multi --no-attach --agents {agents} \\\"{task}\\\"")`
|
|
155
205
|
|
|
156
206
|
## 전제 조건
|
|
157
207
|
|