triflux 7.3.0 → 7.3.2
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/plugin.json +1 -1
- package/hub/intent.mjs +4 -3
- package/hub/routing/complexity.mjs +12 -0
- package/hub/routing/q-learning.mjs +20 -4
- package/hub/team/backend.mjs +2 -1
- package/hub/team/cli/commands/start/index.mjs +103 -102
- package/hub/team/cli/commands/start/parse-args.mjs +41 -5
- package/hub/team/cli/commands/start/start-headless.mjs +3 -3
- package/hub/team/headless.mjs +4 -4
- package/package.json +1 -1
package/hub/intent.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// hub/intent.mjs — Intent Classification Engine
|
|
2
2
|
// 사용자 요청의 "진짜 의도"를 분석 → 카테고리 분류 → 최적 에이전트/모델 자동 선택
|
|
3
3
|
|
|
4
|
-
import { execFileSync
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
6
|
|
|
7
7
|
/** 캐시 엔트리: { category, confidence, ts } */
|
|
@@ -39,8 +39,9 @@ function _getCached(hash) {
|
|
|
39
39
|
|
|
40
40
|
function _tryCodexClassify(prompt) {
|
|
41
41
|
try {
|
|
42
|
-
const raw =
|
|
43
|
-
|
|
42
|
+
const raw = execFileSync(
|
|
43
|
+
'codex',
|
|
44
|
+
['exec', `Classify intent: ${prompt}. Reply JSON: {intent, confidence}`],
|
|
44
45
|
{ timeout: 8000, encoding: 'utf8' }
|
|
45
46
|
);
|
|
46
47
|
// JSON 블록 추출 (응답에 다른 텍스트가 섞일 수 있음)
|
|
@@ -15,6 +15,11 @@ const COMPLEXITY_INDICATORS = {
|
|
|
15
15
|
'cryptograph', 'encryption', 'authentication', 'authorization',
|
|
16
16
|
'database schema', 'data model', 'state machine', 'event-driven',
|
|
17
17
|
'microservice', 'orchestrat', 'pipeline', 'workflow',
|
|
18
|
+
// 한국어
|
|
19
|
+
'리팩터링', '리팩토링', '아키텍처', '보안', '마이그레이션', '분산',
|
|
20
|
+
'동시성', '병렬', '최적화', '성능', '확장성',
|
|
21
|
+
'암호화', '인증', '인가', '데이터베이스 스키마', '데이터 모델',
|
|
22
|
+
'상태 머신', '이벤트 드리븐', '마이크로서비스', '오케스트레이션',
|
|
18
23
|
],
|
|
19
24
|
weight: 0.85,
|
|
20
25
|
},
|
|
@@ -25,6 +30,10 @@ const COMPLEXITY_INDICATORS = {
|
|
|
25
30
|
'validation', 'error handling', 'testing', 'debug', 'fix bug',
|
|
26
31
|
'configuration', 'deploy', 'ci/cd', 'docker', 'container',
|
|
27
32
|
'cache', 'queue', 'webhook', 'notification', 'logging',
|
|
33
|
+
// 한국어
|
|
34
|
+
'구현', '통합', '엔드포인트', '미들웨어', '유효성 검사',
|
|
35
|
+
'에러 처리', '오류 처리', '테스트', '디버깅', '버그 수정',
|
|
36
|
+
'설정', '배포', '컨테이너', '캐시', '알림', '로깅',
|
|
28
37
|
],
|
|
29
38
|
weight: 0.55,
|
|
30
39
|
},
|
|
@@ -34,6 +43,9 @@ const COMPLEXITY_INDICATORS = {
|
|
|
34
43
|
'readme', 'comment', 'typo', 'rename', 'format', 'lint',
|
|
35
44
|
'update version', 'bump', 'add dependency', 'install',
|
|
36
45
|
'simple', 'trivial', 'minor', 'small change', 'one-liner',
|
|
46
|
+
// 한국어
|
|
47
|
+
'문서화', '주석', '오타', '이름 변경', '포맷', '버전 업데이트',
|
|
48
|
+
'의존성 추가', '설치', '간단', '사소한', '소규모', '한 줄',
|
|
37
49
|
],
|
|
38
50
|
weight: 0.2,
|
|
39
51
|
},
|
|
@@ -22,7 +22,7 @@ try {
|
|
|
22
22
|
/** 사용 가능한 CLI 액션 */
|
|
23
23
|
const ACTIONS = ['codex', 'gemini', 'claude', 'haiku', 'sonnet'];
|
|
24
24
|
|
|
25
|
-
/** 특성 벡터 키워드 (
|
|
25
|
+
/** 특성 벡터 키워드 (48차원, 에이전트 타입 기반) */
|
|
26
26
|
const FEATURE_KEYWORDS = [
|
|
27
27
|
// 실행/구현 (codex 친화)
|
|
28
28
|
'implement', 'execute', 'build', 'fix', 'debug', 'code', 'refactor', 'test',
|
|
@@ -32,16 +32,32 @@ const FEATURE_KEYWORDS = [
|
|
|
32
32
|
'design', 'ui', 'ux', 'frontend', 'visual', 'document', 'write', 'explain',
|
|
33
33
|
// 간단/빠른 (haiku 친화)
|
|
34
34
|
'simple', 'quick', 'trivial', 'rename', 'format', 'lint', 'typo', 'minor',
|
|
35
|
+
// 한국어 — 실행/구현 (codex 친화)
|
|
36
|
+
'구현', '빌드', '수정', '디버깅', '리팩터링', '테스트',
|
|
37
|
+
// 한국어 — 분석/설계 (claude/sonnet 친화)
|
|
38
|
+
'분석', '아키텍처', '설계', '검토', '보안', '최적화',
|
|
39
|
+
// 한국어 — 디자인/문서 (gemini 친화)
|
|
40
|
+
'디자인', '문서화',
|
|
41
|
+
// 한국어 — 간단/빠른 (haiku 친화)
|
|
42
|
+
'간단', '사소한',
|
|
35
43
|
];
|
|
36
44
|
|
|
37
45
|
/**
|
|
38
|
-
* 텍스트에서
|
|
46
|
+
* 텍스트에서 48차원 특성 벡터 추출
|
|
47
|
+
* 단어 경계를 기준으로 매칭하여 부분 문자열 오탐을 방지한다.
|
|
39
48
|
* @param {string} text
|
|
40
|
-
* @returns {number[]}
|
|
49
|
+
* @returns {number[]} 48-dim binary feature vector
|
|
41
50
|
*/
|
|
42
51
|
function extractFeatures(text) {
|
|
43
52
|
const lower = text.toLowerCase();
|
|
44
|
-
return FEATURE_KEYWORDS.map((kw) =>
|
|
53
|
+
return FEATURE_KEYWORDS.map((kw) => {
|
|
54
|
+
// 영문 단일 단어: 단어 경계(\b) 적용
|
|
55
|
+
// 한국어 또는 다중 단어 구문: 공백/문장 경계 기반 포함 여부 확인
|
|
56
|
+
if (/^[a-z]+$/.test(kw)) {
|
|
57
|
+
return new RegExp(`\\b${kw}\\b`).test(lower) ? 1 : 0;
|
|
58
|
+
}
|
|
59
|
+
return lower.includes(kw) ? 1 : 0;
|
|
60
|
+
});
|
|
45
61
|
}
|
|
46
62
|
|
|
47
63
|
/**
|
package/hub/team/backend.mjs
CHANGED
|
@@ -18,7 +18,8 @@ export class CodexBackend {
|
|
|
18
18
|
* @returns {string} PowerShell 명령 (cls 제외)
|
|
19
19
|
*/
|
|
20
20
|
buildArgs(prompt, resultFile, opts = {}) {
|
|
21
|
-
|
|
21
|
+
const modelFlag = opts.model ? ` --model '${opts.model}'` : "";
|
|
22
|
+
return `codex exec ${prompt} -o '${resultFile}' --color never${modelFlag}`;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
env() { return {}; }
|
|
@@ -1,102 +1,103 @@
|
|
|
1
|
-
import { decomposeTask } from "../../../orchestrator.mjs";
|
|
2
|
-
import { hasWindowsTerminal, hasWindowsTerminalSession } from "../../../session.mjs";
|
|
3
|
-
import { AMBER, BOLD, DIM, GREEN, RED, RESET, WHITE } from "../../../shared.mjs";
|
|
4
|
-
import { getDefaultHubUrl, getHubInfo, startHubDaemon } from "../../services/hub-client.mjs";
|
|
5
|
-
import { ensureTmuxOrExit } from "../../services/runtime-mode.mjs";
|
|
6
|
-
import { saveTeamState } from "../../services/state-store.mjs";
|
|
7
|
-
import { fail, ok, warn } from "../../render.mjs";
|
|
8
|
-
import { parseTeamArgs } from "./parse-args.mjs";
|
|
9
|
-
import { startInProcessTeam } from "./start-in-process.mjs";
|
|
10
|
-
import { startMuxTeam } from "./start-mux.mjs";
|
|
11
|
-
import { startHeadlessTeam } from "./start-headless.mjs";
|
|
12
|
-
import { startWtTeam } from "./start-wt.mjs";
|
|
13
|
-
|
|
14
|
-
function printStartUsage() {
|
|
15
|
-
console.log(`\n ${AMBER}${BOLD}⬡ tfx multi${RESET}\n`);
|
|
16
|
-
console.log(` 사용법: ${WHITE}tfx multi "작업 설명"${RESET}`);
|
|
17
|
-
console.log(` ${WHITE}tfx multi --agents codex,gemini --lead claude "작업"${RESET}`);
|
|
18
|
-
console.log(` ${WHITE}tfx multi --teammate-mode headless "작업"${RESET} ${DIM}(psmux 헤드리스, 기본)${RESET}`);
|
|
19
|
-
console.log(` ${WHITE}tfx multi --teammate-mode wt "작업"${RESET} ${DIM}(Windows Terminal split-pane)${RESET}`);
|
|
20
|
-
console.log(` ${WHITE}tfx multi --teammate-mode in-process "작업"${RESET} ${DIM}(mux 불필요)${RESET}\n`);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function printWorkerPreview(agents, subtasks) {
|
|
24
|
-
for (let index = 0; index < subtasks.length; index += 1) {
|
|
25
|
-
const preview = subtasks[index].length > 44 ? `${subtasks[index].slice(0, 44)}…` : subtasks[index];
|
|
26
|
-
console.log(` ${DIM}[${agents[index]}-${index + 1}] ${preview}${RESET}`);
|
|
27
|
-
}
|
|
28
|
-
console.log("");
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function renderTmuxInstallHelp() {
|
|
32
|
-
console.log(`\n ${RED}${BOLD}tmux 미발견${RESET}\n`);
|
|
33
|
-
console.log(" 현재 선택한 모드는 tmux 기반 팀세션이 필요합니다.\n");
|
|
34
|
-
console.log(` 설치:\n WSL2: ${WHITE}wsl sudo apt install tmux${RESET}\n macOS: ${WHITE}brew install tmux${RESET}\n Linux: ${WHITE}apt install tmux${RESET}\n`);
|
|
35
|
-
console.log(` Windows에서는 WSL2를 권장합니다:\n 1. ${WHITE}wsl --install${RESET}\n 2. ${WHITE}wsl sudo apt install tmux${RESET}\n 3. ${WHITE}tfx multi "작업"${RESET}\n`);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export { parseTeamArgs };
|
|
39
|
-
|
|
40
|
-
export async function teamStart(args = []) {
|
|
41
|
-
const { agents, lead, layout, teammateMode, task: rawTask, assigns, autoAttach, progressive, timeoutSec, verbose, dashboard, mcpProfile } = parseTeamArgs(args);
|
|
42
|
-
// --assign 사용 시 task를 자동 생성
|
|
43
|
-
const task = rawTask || (assigns.length > 0 ? assigns.map(a => a.prompt).join(" + ") : "");
|
|
44
|
-
if (!task) return printStartUsage();
|
|
45
|
-
|
|
46
|
-
console.log(`\n ${AMBER}${BOLD}⬡ tfx multi${RESET}\n`);
|
|
47
|
-
|
|
48
|
-
// P1b: 워커 수 계산 — 단일 워커 headless에는 Hub 불필요
|
|
49
|
-
const workerCount = assigns.length > 0 ? assigns.length : agents.length;
|
|
50
|
-
const needsHub = workerCount >= 2 || teammateMode !== "headless";
|
|
51
|
-
|
|
52
|
-
let hub = null;
|
|
53
|
-
if (needsHub) {
|
|
54
|
-
hub = await getHubInfo();
|
|
55
|
-
if (!hub) {
|
|
56
|
-
process.stdout.write(" Hub 시작 중...");
|
|
57
|
-
try { hub = await startHubDaemon(); } catch (error) { if (error?.code === "HUB_SERVER_MISSING") fail("hub/server.mjs 없음 — hub 모듈이 설치되지 않음"); }
|
|
58
|
-
console.log(` ${hub ? `${GREEN}✓${RESET}` : `${RED}✗${RESET}`}`);
|
|
59
|
-
if (!hub) warn("Hub 시작 실패 — 수동으로 실행: tfx hub start");
|
|
60
|
-
} else ok(`Hub: ${DIM}${hub.url}${RESET}`);
|
|
61
|
-
} else {
|
|
62
|
-
ok(`Hub: ${DIM}건너뜀 (단일 워커 headless)${RESET}`);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const sessionId = `tfx-multi-${Date.now().toString(36).slice(-4)}${Math.random().toString(36).slice(2, 6)}`;
|
|
66
|
-
const subtasks = decomposeTask(task, agents.length);
|
|
67
|
-
const hubUrl = hub?.url || getDefaultHubUrl();
|
|
68
|
-
let effectiveMode = teammateMode;
|
|
69
|
-
if (effectiveMode === "wt" && !hasWindowsTerminal()) { warn("wt.exe 미발견 — in-process 모드로 자동 fallback"); effectiveMode = "in-process"; }
|
|
70
|
-
if (effectiveMode === "wt" && !hasWindowsTerminalSession()) { warn("WT_SESSION 미감지(Windows Terminal 외부) — in-process 모드로 자동 fallback"); effectiveMode = "in-process"; }
|
|
71
|
-
|
|
72
|
-
console.log(` 세션: ${WHITE}${sessionId}${RESET}`);
|
|
73
|
-
console.log(` 모드: ${effectiveMode}`);
|
|
74
|
-
console.log(` 리드: ${AMBER}${lead}${RESET}`);
|
|
75
|
-
console.log(` 워커: ${agents.map((agent) => `${AMBER}${agent}${RESET}`).join(", ")}`);
|
|
76
|
-
printWorkerPreview(agents, subtasks);
|
|
77
|
-
|
|
78
|
-
if (effectiveMode === "tmux") {
|
|
79
|
-
try { ensureTmuxOrExit(); } catch { return renderTmuxInstallHelp(); }
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const state = effectiveMode === "in-process"
|
|
83
|
-
? await startInProcessTeam({ sessionId, task, lead, agents, subtasks, hubUrl })
|
|
84
|
-
: effectiveMode === "headless"
|
|
85
|
-
? await startHeadlessTeam({ sessionId, task, lead, agents, subtasks, layout, assigns, autoAttach, progressive, timeoutSec, verbose, dashboard, mcpProfile })
|
|
86
|
-
: effectiveMode === "wt"
|
|
87
|
-
? await startWtTeam({ sessionId, task, lead, agents, subtasks, layout, hubUrl })
|
|
88
|
-
: await startMuxTeam({ sessionId, task, lead, agents, subtasks, layout, hubUrl, teammateMode: effectiveMode });
|
|
89
|
-
|
|
90
|
-
if (!state) return fail("in-process supervisor 시작 실패");
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
console.log(` ${DIM}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
console.log(` ${DIM}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
1
|
+
import { decomposeTask } from "../../../orchestrator.mjs";
|
|
2
|
+
import { hasWindowsTerminal, hasWindowsTerminalSession } from "../../../session.mjs";
|
|
3
|
+
import { AMBER, BOLD, DIM, GREEN, RED, RESET, WHITE } from "../../../shared.mjs";
|
|
4
|
+
import { getDefaultHubUrl, getHubInfo, startHubDaemon } from "../../services/hub-client.mjs";
|
|
5
|
+
import { ensureTmuxOrExit } from "../../services/runtime-mode.mjs";
|
|
6
|
+
import { saveTeamState } from "../../services/state-store.mjs";
|
|
7
|
+
import { fail, ok, warn } from "../../render.mjs";
|
|
8
|
+
import { parseTeamArgs } from "./parse-args.mjs";
|
|
9
|
+
import { startInProcessTeam } from "./start-in-process.mjs";
|
|
10
|
+
import { startMuxTeam } from "./start-mux.mjs";
|
|
11
|
+
import { startHeadlessTeam } from "./start-headless.mjs";
|
|
12
|
+
import { startWtTeam } from "./start-wt.mjs";
|
|
13
|
+
|
|
14
|
+
function printStartUsage() {
|
|
15
|
+
console.log(`\n ${AMBER}${BOLD}⬡ tfx multi${RESET}\n`);
|
|
16
|
+
console.log(` 사용법: ${WHITE}tfx multi "작업 설명"${RESET}`);
|
|
17
|
+
console.log(` ${WHITE}tfx multi --agents codex,gemini --lead claude "작업"${RESET}`);
|
|
18
|
+
console.log(` ${WHITE}tfx multi --teammate-mode headless "작업"${RESET} ${DIM}(psmux 헤드리스, 기본)${RESET}`);
|
|
19
|
+
console.log(` ${WHITE}tfx multi --teammate-mode wt "작업"${RESET} ${DIM}(Windows Terminal split-pane)${RESET}`);
|
|
20
|
+
console.log(` ${WHITE}tfx multi --teammate-mode in-process "작업"${RESET} ${DIM}(mux 불필요)${RESET}\n`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function printWorkerPreview(agents, subtasks) {
|
|
24
|
+
for (let index = 0; index < subtasks.length; index += 1) {
|
|
25
|
+
const preview = subtasks[index].length > 44 ? `${subtasks[index].slice(0, 44)}…` : subtasks[index];
|
|
26
|
+
console.log(` ${DIM}[${agents[index]}-${index + 1}] ${preview}${RESET}`);
|
|
27
|
+
}
|
|
28
|
+
console.log("");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function renderTmuxInstallHelp() {
|
|
32
|
+
console.log(`\n ${RED}${BOLD}tmux 미발견${RESET}\n`);
|
|
33
|
+
console.log(" 현재 선택한 모드는 tmux 기반 팀세션이 필요합니다.\n");
|
|
34
|
+
console.log(` 설치:\n WSL2: ${WHITE}wsl sudo apt install tmux${RESET}\n macOS: ${WHITE}brew install tmux${RESET}\n Linux: ${WHITE}apt install tmux${RESET}\n`);
|
|
35
|
+
console.log(` Windows에서는 WSL2를 권장합니다:\n 1. ${WHITE}wsl --install${RESET}\n 2. ${WHITE}wsl sudo apt install tmux${RESET}\n 3. ${WHITE}tfx multi "작업"${RESET}\n`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { parseTeamArgs };
|
|
39
|
+
|
|
40
|
+
export async function teamStart(args = []) {
|
|
41
|
+
const { agents, lead, layout, teammateMode, task: rawTask, assigns, autoAttach, progressive, timeoutSec, verbose, dashboard, mcpProfile, model } = parseTeamArgs(args);
|
|
42
|
+
// --assign 사용 시 task를 자동 생성
|
|
43
|
+
const task = rawTask || (assigns.length > 0 ? assigns.map(a => a.prompt).join(" + ") : "");
|
|
44
|
+
if (!task) return printStartUsage();
|
|
45
|
+
|
|
46
|
+
console.log(`\n ${AMBER}${BOLD}⬡ tfx multi${RESET}\n`);
|
|
47
|
+
|
|
48
|
+
// P1b: 워커 수 계산 — 단일 워커 headless에는 Hub 불필요
|
|
49
|
+
const workerCount = assigns.length > 0 ? assigns.length : agents.length;
|
|
50
|
+
const needsHub = workerCount >= 2 || teammateMode !== "headless";
|
|
51
|
+
|
|
52
|
+
let hub = null;
|
|
53
|
+
if (needsHub) {
|
|
54
|
+
hub = await getHubInfo();
|
|
55
|
+
if (!hub) {
|
|
56
|
+
process.stdout.write(" Hub 시작 중...");
|
|
57
|
+
try { hub = await startHubDaemon(); } catch (error) { if (error?.code === "HUB_SERVER_MISSING") fail("hub/server.mjs 없음 — hub 모듈이 설치되지 않음"); }
|
|
58
|
+
console.log(` ${hub ? `${GREEN}✓${RESET}` : `${RED}✗${RESET}`}`);
|
|
59
|
+
if (!hub) warn("Hub 시작 실패 — 수동으로 실행: tfx hub start");
|
|
60
|
+
} else ok(`Hub: ${DIM}${hub.url}${RESET}`);
|
|
61
|
+
} else {
|
|
62
|
+
ok(`Hub: ${DIM}건너뜀 (단일 워커 headless)${RESET}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const sessionId = `tfx-multi-${Date.now().toString(36).slice(-4)}${Math.random().toString(36).slice(2, 6)}`;
|
|
66
|
+
const subtasks = decomposeTask(task, agents.length);
|
|
67
|
+
const hubUrl = hub?.url || getDefaultHubUrl();
|
|
68
|
+
let effectiveMode = teammateMode;
|
|
69
|
+
if (effectiveMode === "wt" && !hasWindowsTerminal()) { warn("wt.exe 미발견 — in-process 모드로 자동 fallback"); effectiveMode = "in-process"; }
|
|
70
|
+
if (effectiveMode === "wt" && !hasWindowsTerminalSession()) { warn("WT_SESSION 미감지(Windows Terminal 외부) — in-process 모드로 자동 fallback"); effectiveMode = "in-process"; }
|
|
71
|
+
|
|
72
|
+
console.log(` 세션: ${WHITE}${sessionId}${RESET}`);
|
|
73
|
+
console.log(` 모드: ${effectiveMode}`);
|
|
74
|
+
console.log(` 리드: ${AMBER}${lead}${RESET}`);
|
|
75
|
+
console.log(` 워커: ${agents.map((agent) => `${AMBER}${agent}${RESET}`).join(", ")}`);
|
|
76
|
+
printWorkerPreview(agents, subtasks);
|
|
77
|
+
|
|
78
|
+
if (effectiveMode === "tmux") {
|
|
79
|
+
try { ensureTmuxOrExit(); } catch { return renderTmuxInstallHelp(); }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const state = effectiveMode === "in-process"
|
|
83
|
+
? await startInProcessTeam({ sessionId, task, lead, agents, subtasks, hubUrl })
|
|
84
|
+
: effectiveMode === "headless"
|
|
85
|
+
? await startHeadlessTeam({ sessionId, task, lead, agents, subtasks, layout, assigns, autoAttach, progressive, timeoutSec, verbose, dashboard, mcpProfile, model })
|
|
86
|
+
: effectiveMode === "wt"
|
|
87
|
+
? await startWtTeam({ sessionId, task, lead, agents, subtasks, layout, hubUrl })
|
|
88
|
+
: await startMuxTeam({ sessionId, task, lead, agents, subtasks, layout, hubUrl, teammateMode: effectiveMode });
|
|
89
|
+
|
|
90
|
+
if (!state) return fail("in-process supervisor 시작 실패");
|
|
91
|
+
state.sessionId = sessionId;
|
|
92
|
+
saveTeamState(state, sessionId);
|
|
93
|
+
if (typeof state.postSave === "function") state.postSave();
|
|
94
|
+
if (effectiveMode === "in-process") {
|
|
95
|
+
ok("네이티브 in-process 팀 시작 완료");
|
|
96
|
+
console.log(` ${DIM}tmux 없이 실행됨 (직접 CLI 프로세스)${RESET}`);
|
|
97
|
+
console.log(` ${DIM}제어: tfx multi send/control/tasks/status${RESET}\n`);
|
|
98
|
+
} else if (effectiveMode === "wt") {
|
|
99
|
+
ok("Windows Terminal wt 팀 시작 완료");
|
|
100
|
+
console.log(` ${DIM}현재 pane 기준으로 ${state.layout} 분할 생성됨${RESET}`);
|
|
101
|
+
console.log(` ${DIM}wt 모드는 자동 프롬프트 주입/Hub direct 제어(send/control)가 제한됩니다.${RESET}\n`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
import { normalizeLayout, normalizeTeammateMode } from "../../services/runtime-mode.mjs";
|
|
2
2
|
|
|
3
|
+
// --assign 파싱 시 마지막 콜론 뒤를 role로 인식할 알려진 역할/CLI 이름
|
|
4
|
+
const KNOWN_ROLES = new Set([
|
|
5
|
+
"codex", "gemini", "claude",
|
|
6
|
+
"executor", "architect", "planner", "analyst", "critic",
|
|
7
|
+
"debugger", "verifier", "code-reviewer", "security-reviewer",
|
|
8
|
+
"test-engineer", "designer", "writer", "scientist",
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* --assign "cli:prompt:role" 형식을 콜론-안전하게 파싱한다.
|
|
13
|
+
* 프롬프트 내부의 콜론(:)은 구분자로 취급하지 않는다.
|
|
14
|
+
*
|
|
15
|
+
* 규칙:
|
|
16
|
+
* 1. 첫 번째 콜론 앞 = CLI 이름
|
|
17
|
+
* 2. 마지막 콜론 뒤가 KNOWN_ROLES에 있으면 role, 나머지가 prompt
|
|
18
|
+
* 3. 그 외에는 첫 콜론 뒤 전체가 prompt, role은 빈 문자열
|
|
19
|
+
*/
|
|
20
|
+
function parseAssignValue(raw) {
|
|
21
|
+
const firstColon = raw.indexOf(":");
|
|
22
|
+
if (firstColon < 0) return null;
|
|
23
|
+
|
|
24
|
+
const cli = raw.slice(0, firstColon).trim();
|
|
25
|
+
const rest = raw.slice(firstColon + 1);
|
|
26
|
+
|
|
27
|
+
const lastColon = rest.lastIndexOf(":");
|
|
28
|
+
if (lastColon > 0) {
|
|
29
|
+
const candidate = rest.slice(lastColon + 1).trim().toLowerCase();
|
|
30
|
+
if (KNOWN_ROLES.has(candidate)) {
|
|
31
|
+
return { cli, prompt: rest.slice(0, lastColon).trim(), role: candidate };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { cli, prompt: rest.trim(), role: "" };
|
|
36
|
+
}
|
|
37
|
+
|
|
3
38
|
export function parseTeamArgs(args = []) {
|
|
4
39
|
let agents = ["codex", "gemini"];
|
|
5
40
|
let lead = "claude";
|
|
@@ -13,6 +48,7 @@ export function parseTeamArgs(args = []) {
|
|
|
13
48
|
let verbose = false;
|
|
14
49
|
let dashboard = false;
|
|
15
50
|
let mcpProfile = "";
|
|
51
|
+
let model = "";
|
|
16
52
|
|
|
17
53
|
for (let index = 0; index < args.length; index += 1) {
|
|
18
54
|
const current = args[index];
|
|
@@ -25,11 +61,8 @@ export function parseTeamArgs(args = []) {
|
|
|
25
61
|
} else if ((current === "--teammate-mode" || current === "--mode") && args[index + 1]) {
|
|
26
62
|
teammateMode = args[++index];
|
|
27
63
|
} else if (current === "--assign" && args[index + 1]) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (parts.length >= 2) {
|
|
31
|
-
assigns.push({ cli: parts[0].trim(), prompt: parts.slice(1, -1).join(":").trim() || parts[1].trim(), role: parts[parts.length - 1]?.trim() || "" });
|
|
32
|
-
}
|
|
64
|
+
const parsed = parseAssignValue(args[++index]);
|
|
65
|
+
if (parsed) assigns.push(parsed);
|
|
33
66
|
} else if (current === "--auto-attach") {
|
|
34
67
|
autoAttach = true;
|
|
35
68
|
} else if (current === "--no-auto-attach") {
|
|
@@ -44,6 +77,8 @@ export function parseTeamArgs(args = []) {
|
|
|
44
77
|
timeoutSec = Number(args[++index]) || 300;
|
|
45
78
|
} else if (current === "--mcp-profile" && args[index + 1]) {
|
|
46
79
|
mcpProfile = args[++index].trim();
|
|
80
|
+
} else if ((current === "--model" || current === "-m") && args[index + 1]) {
|
|
81
|
+
model = args[++index].trim();
|
|
47
82
|
} else if (current.startsWith("-")) {
|
|
48
83
|
console.warn(` ⚠ 미인식 플래그 무시: ${current}`);
|
|
49
84
|
} else {
|
|
@@ -64,5 +99,6 @@ export function parseTeamArgs(args = []) {
|
|
|
64
99
|
verbose,
|
|
65
100
|
dashboard,
|
|
66
101
|
mcpProfile,
|
|
102
|
+
model,
|
|
67
103
|
};
|
|
68
104
|
}
|
|
@@ -4,11 +4,11 @@ import { ok, warn } from "../../render.mjs";
|
|
|
4
4
|
import { buildTasks } from "../../services/task-model.mjs";
|
|
5
5
|
import { clearTeamState } from "../../services/state-store.mjs";
|
|
6
6
|
|
|
7
|
-
export async function startHeadlessTeam({ sessionId, task, lead, agents, subtasks, layout, assigns, autoAttach, progressive, timeoutSec, verbose, dashboard, mcpProfile }) {
|
|
7
|
+
export async function startHeadlessTeam({ sessionId, task, lead, agents, subtasks, layout, assigns, autoAttach, progressive, timeoutSec, verbose, dashboard, mcpProfile, model }) {
|
|
8
8
|
// --assign이 있으면 그것을 사용, 없으면 agents+subtasks 조합
|
|
9
9
|
const assignments = assigns && assigns.length > 0
|
|
10
|
-
? assigns.map((a, i) => ({ cli: resolveCliType(a.cli), prompt: a.prompt, role: a.role || `worker-${i + 1}`, mcp: mcpProfile }))
|
|
11
|
-
: subtasks.map((subtask, i) => ({ cli: resolveCliType(agents[i] || agents[0]), prompt: subtask, role: `worker-${i + 1}`, mcp: mcpProfile }));
|
|
10
|
+
? assigns.map((a, i) => ({ cli: resolveCliType(a.cli), prompt: a.prompt, role: a.role || `worker-${i + 1}`, mcp: mcpProfile, model }))
|
|
11
|
+
: subtasks.map((subtask, i) => ({ cli: resolveCliType(agents[i] || agents[0]), prompt: subtask, role: `worker-${i + 1}`, mcp: mcpProfile, model }));
|
|
12
12
|
|
|
13
13
|
const startedAt = Date.now();
|
|
14
14
|
ok(`headless ${assignments.length}워커 시작`);
|
package/hub/team/headless.mjs
CHANGED
|
@@ -67,7 +67,7 @@ const MCP_PROFILE_HINTS = {
|
|
|
67
67
|
* @returns {string} PowerShell 명령
|
|
68
68
|
*/
|
|
69
69
|
export function buildHeadlessCommand(cli, prompt, resultFile, opts = {}) {
|
|
70
|
-
const { handoff = true, mcp, contextFile } = opts;
|
|
70
|
+
const { handoff = true, mcp, contextFile, model } = opts;
|
|
71
71
|
const resolvedCli = resolveCliType(cli);
|
|
72
72
|
|
|
73
73
|
// contextFile 처리: 32KB(32768 bytes) 초과 시 UTF-8 안전 절단
|
|
@@ -96,7 +96,7 @@ export function buildHeadlessCommand(cli, prompt, resultFile, opts = {}) {
|
|
|
96
96
|
|
|
97
97
|
const backend = getBackend(resolvedCli);
|
|
98
98
|
const promptExpr = `(Get-Content -Raw '${promptFile}')`;
|
|
99
|
-
return `${cls}${backend.buildArgs(promptExpr, resultFile, opts)}`;
|
|
99
|
+
return `${cls}${backend.buildArgs(promptExpr, resultFile, { ...opts, model })}`;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
/**
|
|
@@ -151,7 +151,7 @@ async function dispatchProgressive(sessionName, assignments, layout, safeProgres
|
|
|
151
151
|
|
|
152
152
|
// 캡처 시작 + 컬러 배너 + 명령 dispatch
|
|
153
153
|
const resultFile = join(RESULT_DIR, `${sessionName}-${paneName}.txt`).replace(/\\/g, "/");
|
|
154
|
-
const cmd = buildHeadlessCommand(assignment.cli, assignment.prompt, resultFile, { mcp: assignment.mcp });
|
|
154
|
+
const cmd = buildHeadlessCommand(assignment.cli, assignment.prompt, resultFile, { mcp: assignment.mcp, model: assignment.model });
|
|
155
155
|
startCapture(sessionName, newPaneId);
|
|
156
156
|
// pane 간 pipe-pane EBUSY 방지 — 이벤트 루프 해방하며 순차 대기
|
|
157
157
|
if (i > 0) await new Promise(r => setTimeout(r, 300));
|
|
@@ -182,7 +182,7 @@ function dispatchBatch(sessionName, assignments, layout, safeProgress) {
|
|
|
182
182
|
return assignments.map((assignment, i) => {
|
|
183
183
|
const paneName = `worker-${i + 1}`;
|
|
184
184
|
const resultFile = join(RESULT_DIR, `${sessionName}-${paneName}.txt`).replace(/\\/g, "/");
|
|
185
|
-
const cmd = buildHeadlessCommand(assignment.cli, assignment.prompt, resultFile, { mcp: assignment.mcp });
|
|
185
|
+
const cmd = buildHeadlessCommand(assignment.cli, assignment.prompt, resultFile, { mcp: assignment.mcp, model: assignment.model });
|
|
186
186
|
const scriptDir = join(RESULT_DIR, sessionName);
|
|
187
187
|
const dispatch = dispatchCommand(sessionName, paneName, cmd, { scriptDir, scriptName: paneName });
|
|
188
188
|
|