triflux 7.3.1 → 7.4.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "7.2.1",
3
+ "version": "7.4.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"
@@ -5,6 +5,7 @@
5
5
  "scientist": "codex", "scientist-deep": "codex", "document-specialist": "codex",
6
6
  "spark": "codex",
7
7
  "designer": "gemini", "writer": "gemini",
8
- "explore": "claude", "verifier": "claude", "test-engineer": "claude", "qa-tester": "claude",
8
+ "explore": "claude",
9
+ "verifier": "codex", "test-engineer": "codex", "qa-tester": "codex",
9
10
  "codex": "codex", "gemini": "gemini", "claude": "claude"
10
11
  }
@@ -18,7 +18,8 @@ export class CodexBackend {
18
18
  * @returns {string} PowerShell 명령 (cls 제외)
19
19
  */
20
20
  buildArgs(prompt, resultFile, opts = {}) {
21
- return `codex exec ${prompt} -o '${resultFile}' --color never`;
21
+ const modelFlag = opts.model ? ` --model '${opts.model}'` : "";
22
+ return `codex exec --dangerously-bypass-approvals-and-sandbox ${prompt} --output-last-message '${resultFile}' --color never${modelFlag}`;
22
23
  }
23
24
 
24
25
  env() { return {}; }
@@ -29,7 +30,7 @@ export class GeminiBackend {
29
30
  command() { return "gemini"; }
30
31
 
31
32
  buildArgs(prompt, resultFile, opts = {}) {
32
- return `gemini -p ${prompt} -o text > '${resultFile}' 2>'${resultFile}.err'`;
33
+ return `gemini --prompt ${prompt} -o text > '${resultFile}' 2>'${resultFile}.err'`;
33
34
  }
34
35
 
35
36
  env() { return {}; }
@@ -40,7 +41,7 @@ export class ClaudeBackend {
40
41
  command() { return "claude"; }
41
42
 
42
43
  buildArgs(prompt, resultFile, opts = {}) {
43
- return `claude -p ${prompt} --output-format text > '${resultFile}' 2>&1`;
44
+ return `claude --print ${prompt} --output-format text > '${resultFile}' 2>&1`;
44
45
  }
45
46
 
46
47
  env() { return {}; }
@@ -38,7 +38,7 @@ function renderTmuxInstallHelp() {
38
38
  export { parseTeamArgs };
39
39
 
40
40
  export async function teamStart(args = []) {
41
- const { agents, lead, layout, teammateMode, task: rawTask, assigns, autoAttach, progressive, timeoutSec, verbose, dashboard, mcpProfile } = parseTeamArgs(args);
41
+ const { agents, lead, layout, teammateMode, task: rawTask, assigns, autoAttach, progressive, timeoutSec, verbose, dashboard, mcpProfile, model } = parseTeamArgs(args);
42
42
  // --assign 사용 시 task를 자동 생성
43
43
  const task = rawTask || (assigns.length > 0 ? assigns.map(a => a.prompt).join(" + ") : "");
44
44
  if (!task) return printStartUsage();
@@ -82,7 +82,7 @@ export async function teamStart(args = []) {
82
82
  const state = effectiveMode === "in-process"
83
83
  ? await startInProcessTeam({ sessionId, task, lead, agents, subtasks, hubUrl })
84
84
  : effectiveMode === "headless"
85
- ? await startHeadlessTeam({ sessionId, task, lead, agents, subtasks, layout, assigns, autoAttach, progressive, timeoutSec, verbose, dashboard, mcpProfile })
85
+ ? await startHeadlessTeam({ sessionId, task, lead, agents, subtasks, layout, assigns, autoAttach, progressive, timeoutSec, verbose, dashboard, mcpProfile, model })
86
86
  : effectiveMode === "wt"
87
87
  ? await startWtTeam({ sessionId, task, lead, agents, subtasks, layout, hubUrl })
88
88
  : await startMuxTeam({ sessionId, task, lead, agents, subtasks, layout, hubUrl, teammateMode: effectiveMode });
@@ -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
- // "cli:prompt:role" 형식 파싱
29
- const parts = args[++index].split(":");
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}워커 시작`);
@@ -1,115 +1,118 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { spawn } from "node:child_process";
4
-
5
- import { buildLeadPrompt, buildPrompt } from "../../orchestrator.mjs";
6
- import { HUB_PID_DIR, PKG_ROOT } from "./state-store.mjs";
7
-
8
- export function buildNativeCliCommand(cli) {
9
- switch (cli) {
10
- case "codex":
11
- return "codex --dangerously-bypass-approvals-and-sandbox --no-alt-screen";
12
- case "gemini":
13
- return "gemini";
14
- case "claude":
15
- return "claude";
16
- default:
17
- return cli;
18
- }
19
- }
20
-
21
- export async function startNativeSupervisor({ sessionId, task, lead, agents, subtasks, hubUrl }) {
22
- const configPath = join(HUB_PID_DIR, `team-native-${sessionId}.config.json`);
23
- const runtimePath = join(HUB_PID_DIR, `team-native-${sessionId}.runtime.json`);
24
- const logsDir = join(HUB_PID_DIR, "team-logs", sessionId);
25
- mkdirSync(logsDir, { recursive: true });
26
-
27
- const leadMember = {
28
- role: "lead",
29
- name: "lead",
30
- cli: lead,
31
- agentId: `${lead}-lead`,
32
- command: buildNativeCliCommand(lead),
33
- };
34
- const workers = agents.map((cli, index) => ({
35
- role: "worker",
36
- name: `${cli}-${index + 1}`,
37
- cli,
38
- agentId: `${cli}-w${index + 1}`,
39
- command: buildNativeCliCommand(cli),
40
- subtask: subtasks[index],
41
- }));
42
- const members = [
43
- {
44
- ...leadMember,
45
- prompt: buildLeadPrompt(task, {
46
- agentId: leadMember.agentId,
47
- hubUrl,
48
- teammateMode: "in-process",
49
- workers: workers.map((worker) => ({
50
- agentId: worker.agentId,
51
- cli: worker.cli,
52
- subtask: worker.subtask,
53
- })),
54
- }),
55
- },
56
- ...workers.map((worker) => ({
57
- ...worker,
58
- prompt: buildPrompt(worker.subtask, { cli: worker.cli, agentId: worker.agentId, hubUrl }),
59
- })),
60
- ];
61
-
62
- writeFileSync(configPath, JSON.stringify({
63
- sessionName: sessionId,
64
- hubUrl,
65
- startupDelayMs: 3000,
66
- logsDir,
67
- runtimeFile: runtimePath,
68
- members,
69
- }, null, 2) + "\n");
70
-
71
- const child = spawn(process.execPath, [join(PKG_ROOT, "hub", "team", "native-supervisor.mjs"), "--config", configPath], {
72
- detached: true,
73
- stdio: "ignore",
74
- env: { ...process.env },
75
- windowsHide: true,
76
- });
77
- child.unref();
78
-
79
- const deadline = Date.now() + 5000;
80
- while (Date.now() < deadline) {
81
- if (existsSync(runtimePath)) {
82
- try {
83
- const runtime = JSON.parse(readFileSync(runtimePath, "utf8"));
84
- return { runtime, members };
85
- } catch {}
86
- }
87
- await new Promise((resolve) => setTimeout(resolve, 100));
88
- }
89
-
90
- return { runtime: null, members };
91
- }
92
-
93
- export async function nativeRequest(state, path, body = {}) {
94
- if (!state?.native?.controlUrl) return null;
95
- try {
96
- const res = await fetch(`${state.native.controlUrl}${path}`, {
97
- method: "POST",
98
- headers: { "Content-Type": "application/json" },
99
- body: JSON.stringify(body),
100
- });
101
- return await res.json();
102
- } catch {
103
- return null;
104
- }
105
- }
106
-
107
- export async function nativeGetStatus(state) {
108
- if (!state?.native?.controlUrl) return null;
109
- try {
110
- const res = await fetch(`${state.native.controlUrl}/status`);
111
- return await res.json();
112
- } catch {
113
- return null;
114
- }
115
- }
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { spawn } from "node:child_process";
4
+
5
+ import { buildLeadPrompt, buildPrompt } from "../../orchestrator.mjs";
6
+ import { HUB_PID_DIR, PKG_ROOT } from "./state-store.mjs";
7
+ import { FEATURES } from "../../codex-compat.mjs";
8
+
9
+ export function buildNativeCliCommand(cli) {
10
+ switch (cli) {
11
+ case "codex":
12
+ return FEATURES.execSubcommand
13
+ ? "codex exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check"
14
+ : "codex --dangerously-bypass-approvals-and-sandbox";
15
+ case "gemini":
16
+ return "gemini";
17
+ case "claude":
18
+ return "claude";
19
+ default:
20
+ return cli;
21
+ }
22
+ }
23
+
24
+ export async function startNativeSupervisor({ sessionId, task, lead, agents, subtasks, hubUrl }) {
25
+ const configPath = join(HUB_PID_DIR, `team-native-${sessionId}.config.json`);
26
+ const runtimePath = join(HUB_PID_DIR, `team-native-${sessionId}.runtime.json`);
27
+ const logsDir = join(HUB_PID_DIR, "team-logs", sessionId);
28
+ mkdirSync(logsDir, { recursive: true });
29
+
30
+ const leadMember = {
31
+ role: "lead",
32
+ name: "lead",
33
+ cli: lead,
34
+ agentId: `${lead}-lead`,
35
+ command: buildNativeCliCommand(lead),
36
+ };
37
+ const workers = agents.map((cli, index) => ({
38
+ role: "worker",
39
+ name: `${cli}-${index + 1}`,
40
+ cli,
41
+ agentId: `${cli}-w${index + 1}`,
42
+ command: buildNativeCliCommand(cli),
43
+ subtask: subtasks[index],
44
+ }));
45
+ const members = [
46
+ {
47
+ ...leadMember,
48
+ prompt: buildLeadPrompt(task, {
49
+ agentId: leadMember.agentId,
50
+ hubUrl,
51
+ teammateMode: "in-process",
52
+ workers: workers.map((worker) => ({
53
+ agentId: worker.agentId,
54
+ cli: worker.cli,
55
+ subtask: worker.subtask,
56
+ })),
57
+ }),
58
+ },
59
+ ...workers.map((worker) => ({
60
+ ...worker,
61
+ prompt: buildPrompt(worker.subtask, { cli: worker.cli, agentId: worker.agentId, hubUrl }),
62
+ })),
63
+ ];
64
+
65
+ writeFileSync(configPath, JSON.stringify({
66
+ sessionName: sessionId,
67
+ hubUrl,
68
+ startupDelayMs: 3000,
69
+ logsDir,
70
+ runtimeFile: runtimePath,
71
+ members,
72
+ }, null, 2) + "\n");
73
+
74
+ const child = spawn(process.execPath, [join(PKG_ROOT, "hub", "team", "native-supervisor.mjs"), "--config", configPath], {
75
+ detached: true,
76
+ stdio: "ignore",
77
+ env: { ...process.env },
78
+ windowsHide: true,
79
+ });
80
+ child.unref();
81
+
82
+ const deadline = Date.now() + 5000;
83
+ while (Date.now() < deadline) {
84
+ if (existsSync(runtimePath)) {
85
+ try {
86
+ const runtime = JSON.parse(readFileSync(runtimePath, "utf8"));
87
+ return { runtime, members };
88
+ } catch {}
89
+ }
90
+ await new Promise((resolve) => setTimeout(resolve, 100));
91
+ }
92
+
93
+ return { runtime: null, members };
94
+ }
95
+
96
+ export async function nativeRequest(state, path, body = {}) {
97
+ if (!state?.native?.controlUrl) return null;
98
+ try {
99
+ const res = await fetch(`${state.native.controlUrl}${path}`, {
100
+ method: "POST",
101
+ headers: { "Content-Type": "application/json" },
102
+ body: JSON.stringify(body),
103
+ });
104
+ return await res.json();
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
109
+
110
+ export async function nativeGetStatus(state) {
111
+ if (!state?.native?.controlUrl) return null;
112
+ try {
113
+ const res = await fetch(`${state.native.controlUrl}/status`);
114
+ return await res.json();
115
+ } catch {
116
+ return null;
117
+ }
118
+ }
@@ -0,0 +1,78 @@
1
+ // hub/team/codex-compat.mjs — Codex CLI 버전 어댑터
2
+ // Codex 0.117.0+ (Rust 리라이트): exec 서브커맨드 기반
3
+ import { execSync } from "node:child_process";
4
+
5
+ let _cachedVersion = null;
6
+
7
+ /**
8
+ * `codex --version` 실행 결과를 파싱하여 마이너 버전 숫자 반환.
9
+ * 파싱 실패 시 0 반환 (구버전으로 간주).
10
+ * @returns {number} 마이너 버전 (예: 0.117.0 → 117)
11
+ */
12
+ export function getCodexVersion() {
13
+ if (_cachedVersion !== null) return _cachedVersion;
14
+ try {
15
+ const out = execSync("codex --version", { encoding: "utf8", timeout: 5000 }).trim();
16
+ // "codex 0.117.0" 또는 "0.117.0" 형식 대응
17
+ const m = out.match(/(\d+)\.(\d+)\.(\d+)/);
18
+ _cachedVersion = m ? parseInt(m[2], 10) : 0;
19
+ } catch {
20
+ _cachedVersion = 0;
21
+ }
22
+ return _cachedVersion;
23
+ }
24
+
25
+ /**
26
+ * 최소 마이너 버전 이상인지 확인.
27
+ * @param {number} minMinor
28
+ * @returns {boolean}
29
+ */
30
+ export function gte(minMinor) {
31
+ return getCodexVersion() >= minMinor;
32
+ }
33
+
34
+ /**
35
+ * Codex CLI 기능별 분기 객체.
36
+ * 117 = 0.117.0 (Rust 리라이트, exec 서브커맨드 도입)
37
+ */
38
+ export const FEATURES = {
39
+ /** exec 서브커맨드 사용 가능 여부 */
40
+ get execSubcommand() { return gte(117); },
41
+ /** --output-last-message 플래그 지원 여부 */
42
+ get outputLastMessage() { return gte(117); },
43
+ /** --color never 플래그 지원 여부 */
44
+ get colorNever() { return gte(117); },
45
+ /** 플러그인 시스템 지원 여부 (향후 확장용) */
46
+ get pluginSystem() { return gte(120); },
47
+ };
48
+
49
+ /**
50
+ * long-form 플래그 기반 명령 빌더.
51
+ * @param {string} prompt
52
+ * @param {string|null} resultFile — null이면 --output-last-message 생략
53
+ * @param {{ profile?: string, skipGitRepoCheck?: boolean, sandboxBypass?: boolean }} [opts]
54
+ * @returns {string} 실행할 셸 커맨드
55
+ */
56
+ export function buildExecCommand(prompt, resultFile = null, opts = {}) {
57
+ const { profile, skipGitRepoCheck = true, sandboxBypass = true } = opts;
58
+
59
+ const parts = ["codex"];
60
+ if (profile) parts.push("--profile", profile);
61
+
62
+ if (FEATURES.execSubcommand) {
63
+ parts.push("exec");
64
+ if (sandboxBypass) parts.push("--dangerously-bypass-approvals-and-sandbox");
65
+ if (skipGitRepoCheck) parts.push("--skip-git-repo-check");
66
+ if (resultFile && FEATURES.outputLastMessage) {
67
+ parts.push("--output-last-message", resultFile);
68
+ }
69
+ if (FEATURES.colorNever) parts.push("--color", "never");
70
+ } else {
71
+ // 구버전 fallback
72
+ parts.push("--dangerously-bypass-approvals-and-sandbox");
73
+ if (skipGitRepoCheck) parts.push("--skip-git-repo-check");
74
+ }
75
+
76
+ parts.push(JSON.stringify(prompt));
77
+ return parts.join(" ");
78
+ }
@@ -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