triflux 7.1.4 → 7.2.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.
Files changed (73) hide show
  1. package/.claude-plugin/marketplace.json +31 -31
  2. package/.claude-plugin/plugin.json +22 -23
  3. package/bin/triflux.mjs +18 -5
  4. package/hooks/keyword-rules.json +393 -361
  5. package/hub/bridge.mjs +799 -786
  6. package/hub/delegator/contracts.mjs +37 -38
  7. package/hub/delegator/schema/delegator-tools.schema.json +250 -250
  8. package/hub/delegator/service.mjs +307 -302
  9. package/hub/intent.mjs +108 -11
  10. package/hub/lib/process-utils.mjs +20 -0
  11. package/hub/pipe.mjs +589 -589
  12. package/hub/pipeline/gates/confidence.mjs +1 -1
  13. package/hub/pipeline/gates/selfcheck.mjs +2 -4
  14. package/hub/pipeline/state.mjs +191 -187
  15. package/hub/pipeline/transitions.mjs +124 -120
  16. package/hub/public/dashboard.html +355 -349
  17. package/hub/quality/deslop.mjs +5 -3
  18. package/hub/reflexion.mjs +5 -1
  19. package/hub/research.mjs +6 -1
  20. package/hub/router.mjs +791 -782
  21. package/hub/server.mjs +893 -822
  22. package/hub/store.mjs +807 -778
  23. package/hub/team/agent-map.json +10 -0
  24. package/hub/team/ansi.mjs +3 -4
  25. package/hub/team/cli/commands/control.mjs +43 -43
  26. package/hub/team/cli/commands/interrupt.mjs +36 -36
  27. package/hub/team/cli/commands/kill.mjs +3 -3
  28. package/hub/team/cli/commands/send.mjs +37 -37
  29. package/hub/team/cli/commands/start/index.mjs +18 -8
  30. package/hub/team/cli/commands/start/parse-args.mjs +3 -1
  31. package/hub/team/cli/commands/start/start-headless.mjs +4 -1
  32. package/hub/team/cli/commands/status.mjs +87 -87
  33. package/hub/team/cli/commands/stop.mjs +1 -1
  34. package/hub/team/cli/commands/task.mjs +1 -1
  35. package/hub/team/cli/index.mjs +41 -39
  36. package/hub/team/cli/manifest.mjs +29 -28
  37. package/hub/team/cli/services/hub-client.mjs +37 -0
  38. package/hub/team/cli/services/state-store.mjs +26 -12
  39. package/hub/team/dashboard.mjs +11 -4
  40. package/hub/team/handoff.mjs +12 -0
  41. package/hub/team/headless.mjs +202 -200
  42. package/hub/team/native-supervisor.mjs +386 -346
  43. package/hub/team/nativeProxy.mjs +680 -692
  44. package/hub/team/staleState.mjs +361 -369
  45. package/hub/team/tui-viewer.mjs +27 -3
  46. package/hub/team/tui.mjs +1 -0
  47. package/hub/token-mode.mjs +114 -24
  48. package/hub/workers/delegator-mcp.mjs +1059 -1057
  49. package/hud/colors.mjs +88 -0
  50. package/hud/constants.mjs +78 -0
  51. package/hud/hud-qos-status.mjs +206 -1872
  52. package/hud/providers/claude.mjs +309 -0
  53. package/hud/providers/codex.mjs +151 -0
  54. package/hud/providers/gemini.mjs +320 -0
  55. package/hud/renderers.mjs +424 -0
  56. package/hud/terminal.mjs +140 -0
  57. package/hud/utils.mjs +271 -0
  58. package/package.json +1 -2
  59. package/scripts/__tests__/keyword-detector.test.mjs +234 -234
  60. package/scripts/headless-guard-fast.sh +21 -0
  61. package/scripts/headless-guard.mjs +26 -6
  62. package/scripts/lib/keyword-rules.mjs +166 -168
  63. package/scripts/setup.mjs +725 -690
  64. package/scripts/tfx-route-post.mjs +424 -424
  65. package/scripts/tfx-route.sh +1671 -1650
  66. package/scripts/tmp-cleanup.mjs +74 -0
  67. package/skills/tfx-auto/SKILL.md +279 -278
  68. package/skills/tfx-auto-codex/SKILL.md +98 -77
  69. package/skills/tfx-codex/SKILL.md +65 -65
  70. package/skills/tfx-gemini/SKILL.md +83 -82
  71. package/skills/tfx-hub/SKILL.md +205 -136
  72. package/skills/tfx-multi/SKILL.md +11 -5
  73. package/.mcp.json +0 -8
@@ -0,0 +1,10 @@
1
+ {
2
+ "executor": "codex", "build-fixer": "codex", "debugger": "codex", "deep-executor": "codex",
3
+ "architect": "codex", "planner": "codex", "critic": "codex", "analyst": "codex",
4
+ "code-reviewer": "codex", "security-reviewer": "codex", "quality-reviewer": "codex",
5
+ "scientist": "codex", "scientist-deep": "codex", "document-specialist": "codex",
6
+ "spark": "codex",
7
+ "designer": "gemini", "writer": "gemini",
8
+ "explore": "claude", "verifier": "claude", "test-engineer": "claude", "qa-tester": "claude",
9
+ "codex": "codex", "gemini": "gemini", "claude": "claude"
10
+ }
package/hub/team/ansi.mjs CHANGED
@@ -84,17 +84,16 @@ export function padRight(str, len) {
84
84
  export function truncate(str, maxLen) {
85
85
  const visible = stripAnsi(str);
86
86
  if (visible.length <= maxLen) return str;
87
- // 간단한 잘라내기 (ANSI 코드 포함 시 근사치)
88
- return str.slice(0, maxLen - 1) + "…";
87
+ return visible.slice(0, maxLen - 1) + "…";
89
88
  }
90
89
 
91
90
  export function stripAnsi(str) {
92
- return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
91
+ return str.replace(/\x1b\[[0-9;]*[a-zA-Z]|\x1b\].*?(\x07|\x1b\\)/g, "");
93
92
  }
94
93
 
95
94
  // ── 진행률 바 ──
96
95
  export function progressBar(ratio, width = 20) {
97
- const filled = Math.round(ratio * width);
96
+ const filled = Math.max(0, Math.min(width, Math.round(ratio * width)));
98
97
  const empty = width - filled;
99
98
  return `${FG.accent}${"█".repeat(filled)}${FG.muted}${"░".repeat(empty)}${RESET}`;
100
99
  }
@@ -1,43 +1,43 @@
1
- import { injectPrompt, sendKeys } from "../../pane.mjs";
2
- import { DIM, RESET, WHITE } from "../../shared.mjs";
3
- import { publishLeadControl } from "../services/hub-client.mjs";
4
- import { resolveMember } from "../services/member-selector.mjs";
5
- import { nativeRequest } from "../services/native-control.mjs";
6
- import { isNativeMode, isTeamAlive, isWtMode } from "../services/runtime-mode.mjs";
7
- import { loadTeamState } from "../services/state-store.mjs";
8
- import { ok, warn } from "../render.mjs";
9
-
10
- export async function teamControl(args = []) {
11
- const state = loadTeamState();
12
- if (!state || !isTeamAlive(state)) {
13
- console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
14
- return;
15
- }
16
-
17
- const member = resolveMember(state, args[0]);
18
- const command = String(args[1] || "").toLowerCase();
19
- const reason = args.slice(2).join(" ");
20
- if (!member || !new Set(["interrupt", "stop", "pause", "resume"]).has(command)) {
21
- console.log(`\n 사용법: ${WHITE}tfx multi control <lead|이름|번호> <interrupt|stop|pause|resume> [사유]${RESET}\n`);
22
- return;
23
- }
24
- if (isWtMode(state)) {
25
- console.log(`\n \x1b[33m⚠\x1b[0m wt 모드는 Hub direct/control 주입 경로가 비활성입니다.\n ${DIM}수동 제어: 해당 pane에서 직접 명령/인터럽트를 수행하세요.${RESET}\n`);
26
- return;
27
- }
28
-
29
- let directOk = false;
30
- if (isNativeMode(state)) {
31
- directOk = !!(await nativeRequest(state, "/control", { member: member.name, command, reason }))?.ok;
32
- } else {
33
- injectPrompt(member.pane, `[LEAD CONTROL] command=${command}${reason ? ` reason=${reason}` : ""}`);
34
- if (command === "interrupt") sendKeys(member.pane, "C-c");
35
- directOk = true;
36
- }
37
-
38
- const published = await publishLeadControl(state, member, command, reason);
39
- if (directOk && published) ok(`${member.name} 제어 전송 (${command}, direct + hub)`);
40
- else if (directOk) ok(`${member.name} 제어 전송 (${command}, direct only)`);
41
- else warn(`${member.name} 제어 전송 실패 (${command})`);
42
- console.log("");
43
- }
1
+ import { injectPrompt, sendKeys } from "../../pane.mjs";
2
+ import { DIM, RESET, WHITE, YELLOW } from "../../shared.mjs";
3
+ import { publishLeadControl } from "../services/hub-client.mjs";
4
+ import { resolveMember } from "../services/member-selector.mjs";
5
+ import { nativeRequest } from "../services/native-control.mjs";
6
+ import { isNativeMode, isTeamAlive, isWtMode } from "../services/runtime-mode.mjs";
7
+ import { loadTeamState } from "../services/state-store.mjs";
8
+ import { ok, warn } from "../render.mjs";
9
+
10
+ export async function teamControl(args = []) {
11
+ const state = loadTeamState();
12
+ if (!state || !isTeamAlive(state)) {
13
+ console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
14
+ return;
15
+ }
16
+
17
+ const member = resolveMember(state, args[0]);
18
+ const command = String(args[1] || "").toLowerCase();
19
+ const reason = args.slice(2).join(" ");
20
+ if (!member || !new Set(["interrupt", "stop", "pause", "resume"]).has(command)) {
21
+ console.log(`\n 사용법: ${WHITE}tfx multi control <lead|이름|번호> <interrupt|stop|pause|resume> [사유]${RESET}\n`);
22
+ return;
23
+ }
24
+ if (isWtMode(state)) {
25
+ console.log(`\n ${YELLOW}⚠${RESET} wt 모드는 Hub direct/control 주입 경로가 비활성입니다.\n ${DIM}수동 제어: 해당 pane에서 직접 명령/인터럽트를 수행하세요.${RESET}\n`);
26
+ return;
27
+ }
28
+
29
+ let directOk = false;
30
+ if (isNativeMode(state)) {
31
+ directOk = !!(await nativeRequest(state, "/control", { member: member.name, command, reason }))?.ok;
32
+ } else {
33
+ injectPrompt(member.pane, `[LEAD CONTROL] command=${command}${reason ? ` reason=${reason}` : ""}`);
34
+ if (command === "interrupt") sendKeys(member.pane, "C-c");
35
+ directOk = true;
36
+ }
37
+
38
+ const published = await publishLeadControl(state, member, command, reason);
39
+ if (directOk && published) ok(`${member.name} 제어 전송 (${command}, direct + hub)`);
40
+ else if (directOk) ok(`${member.name} 제어 전송 (${command}, direct only)`);
41
+ else warn(`${member.name} 제어 전송 실패 (${command})`);
42
+ console.log("");
43
+ }
@@ -1,36 +1,36 @@
1
- import { sendKeys } from "../../pane.mjs";
2
- import { DIM, RESET, WHITE } from "../../shared.mjs";
3
- import { resolveMember } from "../services/member-selector.mjs";
4
- import { nativeRequest } from "../services/native-control.mjs";
5
- import { isNativeMode, isTeamAlive, isWtMode } from "../services/runtime-mode.mjs";
6
- import { loadTeamState } from "../services/state-store.mjs";
7
- import { ok, warn } from "../render.mjs";
8
-
9
- export async function teamInterrupt(args = []) {
10
- const state = loadTeamState();
11
- if (!state || !isTeamAlive(state)) {
12
- console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
13
- return;
14
- }
15
-
16
- const member = resolveMember(state, args[0] || "lead");
17
- if (!member) {
18
- console.log(`\n 사용법: ${WHITE}tfx multi interrupt <lead|이름|번호>${RESET}\n`);
19
- return;
20
- }
21
- if (isWtMode(state)) {
22
- console.log(`\n \x1b[33m⚠\x1b[0m wt 모드에서는 pane stdin 주입이 지원되지 않아 interrupt를 자동 전송할 수 없습니다.\n ${DIM}수동으로 해당 pane에서 Ctrl+C를 입력하세요.${RESET}\n`);
23
- return;
24
- }
25
-
26
- if (isNativeMode(state)) {
27
- const result = await nativeRequest(state, "/interrupt", { member: member.name });
28
- (result?.ok ? ok : warn)(`${member.name} ${result?.ok ? "인터럽트 전송" : "인터럽트 실패"}`);
29
- console.log("");
30
- return;
31
- }
32
-
33
- sendKeys(member.pane, "C-c");
34
- ok(`${member.name} 인터럽트 전송`);
35
- console.log("");
36
- }
1
+ import { sendKeys } from "../../pane.mjs";
2
+ import { DIM, RESET, WHITE, YELLOW } from "../../shared.mjs";
3
+ import { resolveMember } from "../services/member-selector.mjs";
4
+ import { nativeRequest } from "../services/native-control.mjs";
5
+ import { isNativeMode, isTeamAlive, isWtMode } from "../services/runtime-mode.mjs";
6
+ import { loadTeamState } from "../services/state-store.mjs";
7
+ import { ok, warn } from "../render.mjs";
8
+
9
+ export async function teamInterrupt(args = []) {
10
+ const state = loadTeamState();
11
+ if (!state || !isTeamAlive(state)) {
12
+ console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
13
+ return;
14
+ }
15
+
16
+ const member = resolveMember(state, args[0] || "lead");
17
+ if (!member) {
18
+ console.log(`\n 사용법: ${WHITE}tfx multi interrupt <lead|이름|번호>${RESET}\n`);
19
+ return;
20
+ }
21
+ if (isWtMode(state)) {
22
+ console.log(`\n ${YELLOW}⚠${RESET} wt 모드에서는 pane stdin 주입이 지원되지 않아 interrupt를 자동 전송할 수 없습니다.\n ${DIM}수동으로 해당 pane에서 Ctrl+C를 입력하세요.${RESET}\n`);
23
+ return;
24
+ }
25
+
26
+ if (isNativeMode(state)) {
27
+ const result = await nativeRequest(state, "/interrupt", { member: member.name });
28
+ (result?.ok ? ok : warn)(`${member.name} ${result?.ok ? "인터럽트 전송" : "인터럽트 실패"}`);
29
+ console.log("");
30
+ return;
31
+ }
32
+
33
+ sendKeys(member.pane, "C-c");
34
+ ok(`${member.name} 인터럽트 전송`);
35
+ console.log("");
36
+ }
@@ -10,14 +10,14 @@ export async function teamKill() {
10
10
  if (state && isNativeMode(state) && isTeamAlive(state)) {
11
11
  await nativeRequest(state, "/stop", {});
12
12
  try { process.kill(state.native.supervisorPid, "SIGTERM"); } catch {}
13
- clearTeamState();
13
+ clearTeamState(state.sessionId);
14
14
  ok(`종료: ${state.sessionName}`);
15
15
  console.log("");
16
16
  return;
17
17
  }
18
18
  if (state && isWtMode(state)) {
19
19
  const closed = closeWtSession({ layout: state?.wt?.layout || state?.layout || "1xN", paneCount: state?.wt?.paneCount ?? (state.members || []).length });
20
- clearTeamState();
20
+ clearTeamState(state.sessionId);
21
21
  ok(`종료: ${state.sessionName}${closed ? ` (${closed} panes closed)` : ""}`);
22
22
  console.log("");
23
23
  return;
@@ -32,6 +32,6 @@ export async function teamKill() {
32
32
  killSession(session);
33
33
  ok(`종료: ${session}`);
34
34
  }
35
- clearTeamState();
35
+ clearTeamState(state?.sessionId);
36
36
  console.log("");
37
37
  }
@@ -1,37 +1,37 @@
1
- import { injectPrompt } from "../../pane.mjs";
2
- import { DIM, RESET, WHITE } from "../../shared.mjs";
3
- import { resolveMember } from "../services/member-selector.mjs";
4
- import { nativeRequest } from "../services/native-control.mjs";
5
- import { isNativeMode, isTeamAlive, isWtMode } from "../services/runtime-mode.mjs";
6
- import { loadTeamState } from "../services/state-store.mjs";
7
- import { ok, warn } from "../render.mjs";
8
-
9
- export async function teamSend(args = []) {
10
- const state = loadTeamState();
11
- if (!state || !isTeamAlive(state)) {
12
- console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
13
- return;
14
- }
15
-
16
- const member = resolveMember(state, args[0]);
17
- const message = args.slice(1).join(" ");
18
- if (!member || !message) {
19
- console.log(`\n 사용법: ${WHITE}tfx multi send <lead|이름|번호> "메시지"${RESET}\n`);
20
- return;
21
- }
22
- if (isWtMode(state)) {
23
- console.log(`\n \x1b[33m⚠\x1b[0m wt 모드는 pane 프롬프트 자동 주입(send)이 지원되지 않습니다.\n ${DIM}수동 전달: 선택한 pane에 직접 붙여넣으세요.${RESET}\n`);
24
- return;
25
- }
26
-
27
- if (isNativeMode(state)) {
28
- const result = await nativeRequest(state, "/send", { member: member.name, text: message });
29
- (result?.ok ? ok : warn)(`${member.name}${result?.ok ? "에 메시지 주입 완료" : " 메시지 주입 실패"}`);
30
- console.log("");
31
- return;
32
- }
33
-
34
- injectPrompt(member.pane, message);
35
- ok(`${member.name}에 메시지 주입 완료`);
36
- console.log("");
37
- }
1
+ import { injectPrompt } from "../../pane.mjs";
2
+ import { DIM, RESET, WHITE, YELLOW } from "../../shared.mjs";
3
+ import { resolveMember } from "../services/member-selector.mjs";
4
+ import { nativeRequest } from "../services/native-control.mjs";
5
+ import { isNativeMode, isTeamAlive, isWtMode } from "../services/runtime-mode.mjs";
6
+ import { loadTeamState } from "../services/state-store.mjs";
7
+ import { ok, warn } from "../render.mjs";
8
+
9
+ export async function teamSend(args = []) {
10
+ const state = loadTeamState();
11
+ if (!state || !isTeamAlive(state)) {
12
+ console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
13
+ return;
14
+ }
15
+
16
+ const member = resolveMember(state, args[0]);
17
+ const message = args.slice(1).join(" ");
18
+ if (!member || !message) {
19
+ console.log(`\n 사용법: ${WHITE}tfx multi send <lead|이름|번호> "메시지"${RESET}\n`);
20
+ return;
21
+ }
22
+ if (isWtMode(state)) {
23
+ console.log(`\n ${YELLOW}⚠${RESET} wt 모드는 pane 프롬프트 자동 주입(send)이 지원되지 않습니다.\n ${DIM}수동 전달: 선택한 pane에 직접 붙여넣으세요.${RESET}\n`);
24
+ return;
25
+ }
26
+
27
+ if (isNativeMode(state)) {
28
+ const result = await nativeRequest(state, "/send", { member: member.name, text: message });
29
+ (result?.ok ? ok : warn)(`${member.name}${result?.ok ? "에 메시지 주입 완료" : " 메시지 주입 실패"}`);
30
+ console.log("");
31
+ return;
32
+ }
33
+
34
+ injectPrompt(member.pane, message);
35
+ ok(`${member.name}에 메시지 주입 완료`);
36
+ console.log("");
37
+ }
@@ -44,13 +44,23 @@ export async function teamStart(args = []) {
44
44
  if (!task) return printStartUsage();
45
45
 
46
46
  console.log(`\n ${AMBER}${BOLD}⬡ tfx multi${RESET}\n`);
47
- let hub = await getHubInfo();
48
- if (!hub) {
49
- process.stdout.write(" Hub 시작 중...");
50
- try { hub = await startHubDaemon(); } catch (error) { if (error?.code === "HUB_SERVER_MISSING") fail("hub/server.mjs 없음 — hub 모듈이 설치되지 않음"); }
51
- console.log(` ${hub ? `${GREEN}✓${RESET}` : `${RED}✗${RESET}`}`);
52
- if (!hub) warn("Hub 시작 실패 — 수동으로 실행: tfx hub start");
53
- } else ok(`Hub: ${DIM}${hub.url}${RESET}`);
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
+ }
54
64
 
55
65
  const sessionId = `tfx-multi-${Date.now().toString(36).slice(-4)}${Math.random().toString(36).slice(2, 6)}`;
56
66
  const subtasks = decomposeTask(task, agents.length);
@@ -78,7 +88,7 @@ export async function teamStart(args = []) {
78
88
  : await startMuxTeam({ sessionId, task, lead, agents, subtasks, layout, hubUrl, teammateMode: effectiveMode });
79
89
 
80
90
  if (!state) return fail("in-process supervisor 시작 실패");
81
- saveTeamState(state);
91
+ saveTeamState(state, sessionId);
82
92
  if (typeof state.postSave === "function") state.postSave();
83
93
  if (effectiveMode === "in-process") {
84
94
  ok("네이티브 in-process 팀 시작 완료");
@@ -44,7 +44,9 @@ export function parseTeamArgs(args = []) {
44
44
  timeoutSec = Number(args[++index]) || 300;
45
45
  } else if (current === "--mcp-profile" && args[index + 1]) {
46
46
  mcpProfile = args[++index].trim();
47
- } else if (!current.startsWith("-")) {
47
+ } else if (current.startsWith("-")) {
48
+ console.warn(` ⚠ 미인식 플래그 무시: ${current}`);
49
+ } else {
48
50
  taskParts.push(current);
49
51
  }
50
52
  }
@@ -73,6 +73,9 @@ export async function startHeadlessTeam({ sessionId, task, lead, agents, subtask
73
73
  }
74
74
  }
75
75
 
76
+ // dashboard 모드: tui-viewer가 최종 상태를 캡처할 시간 확보
77
+ if (dashboard) await new Promise(r => setTimeout(r, 2000));
78
+
76
79
  // 세션 정리
77
80
  handle.kill();
78
81
 
@@ -95,7 +98,7 @@ export async function startHeadlessTeam({ sessionId, task, lead, agents, subtask
95
98
  tasks: buildTasks(assignments.map(a => a.prompt), members.filter((m) => m.role === "worker")),
96
99
  postSave() {
97
100
  // headless는 실행 완료 후 즉시 정리 — HUD에 잔존 방지
98
- clearTeamState();
101
+ clearTeamState(sessionId);
99
102
  console.log(`\n ${DIM}세션 정리 완료.${RESET}\n`);
100
103
  },
101
104
  };
@@ -1,87 +1,87 @@
1
- import { AMBER, BOLD, DIM, GRAY, GREEN, RED, RESET } from "../../shared.mjs";
2
- import { hasWindowsTerminalSession } from "../../session.mjs";
3
- import { fetchHubTaskList, nativeGetStatus } from "../services/hub-client.mjs";
4
- import { isNativeMode, isTeamAlive, isWtMode } from "../services/runtime-mode.mjs";
5
- import { loadTeamState } from "../services/state-store.mjs";
6
- import { formatCompletionSuffix } from "../render.mjs";
7
-
8
- export async function teamStatus(args = []) {
9
- const json = process.env.TFX_OUTPUT_JSON === "1" || args.includes("--json");
10
- const state = loadTeamState();
11
- if (!state) {
12
- if (json) {
13
- process.stdout.write(`${JSON.stringify({ status: "offline", sessionName: null, alive: false }, null, 2)}\n`);
14
- return;
15
- }
16
- console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
17
- return;
18
- }
19
-
20
- const alive = isTeamAlive(state);
21
- const payload = {
22
- status: alive ? "active" : "dead",
23
- alive,
24
- sessionName: state.sessionName,
25
- teammateMode: state.teammateMode || "tmux",
26
- lead: state.lead || "claude",
27
- agents: state.agents || [],
28
- startedAt: state.startedAt || null,
29
- taskCount: (state.tasks || []).length,
30
- members: (state.members || []).map((member) => ({
31
- name: member.name,
32
- cli: member.cli,
33
- role: member.role,
34
- pane: member.pane,
35
- })),
36
- };
37
-
38
- if (isNativeMode(state) && alive) {
39
- payload.nativeMembers = (await nativeGetStatus(state))?.data?.members || [];
40
- }
41
-
42
- if (alive) {
43
- payload.hubTasks = await fetchHubTaskList(state);
44
- }
45
-
46
- if (json) {
47
- process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
48
- return;
49
- }
50
-
51
- console.log(`\n ${AMBER}${BOLD}⬡ tfx multi${RESET} ${alive ? `${GREEN}● active${RESET}` : `${RED}● dead${RESET}`}\n`);
52
- console.log(` 세션: ${state.sessionName}`);
53
- console.log(` 모드: ${state.teammateMode || "tmux"}`);
54
- console.log(` 리드: ${state.lead || "claude"}`);
55
- console.log(` 워커: ${(state.agents || []).join(", ")}`);
56
- console.log(` Uptime: ${alive ? `${Math.round((Date.now() - state.startedAt) / 60000)}분` : "-"}`);
57
- console.log(` 태스크: ${(state.tasks || []).length}`);
58
- if (isWtMode(state) && !hasWindowsTerminalSession()) {
59
- console.log(` ${DIM}WT_SESSION 미감지: 생존성은 heuristics로 판정됨${RESET}`);
60
- }
61
-
62
- for (const member of state.members || []) {
63
- console.log(` - ${member.name} (${member.cli}) ${DIM}${member.role}${RESET} ${DIM}${member.pane}${RESET}`);
64
- }
65
-
66
- if (isNativeMode(state) && alive) {
67
- for (const member of (await nativeGetStatus(state))?.data?.members || []) {
68
- console.log(` • ${member.name}: ${member.status}${formatCompletionSuffix(member)}${member.lastPreview ? ` ${DIM}${member.lastPreview}${RESET}` : ""}`);
69
- }
70
- }
71
-
72
- if (alive) {
73
- const hubTasks = payload.hubTasks || await fetchHubTaskList(state);
74
- if (hubTasks.length) {
75
- const completed = hubTasks.filter((task) => task.status === "completed").length;
76
- const failed = hubTasks.filter((task) => task.status === "failed").length;
77
- console.log(`\n ${BOLD}Hub Tasks${RESET} ${DIM}(${completed}/${hubTasks.length} done)${RESET}`);
78
- for (const task of hubTasks) {
79
- const icon = task.status === "completed" ? `${GREEN}✓${RESET}` : task.status === "in_progress" ? `${AMBER}●${RESET}` : task.status === "failed" ? `${RED}✗${RESET}` : `${GRAY}○${RESET}`;
80
- const owner = task.owner ? ` ${GRAY}[${task.owner}]${RESET}` : "";
81
- console.log(` ${icon} ${task.subject || task.description?.slice(0, 50) || ""}${owner}`);
82
- }
83
- if (failed > 0) console.log(` ${RED}⚠ ${failed}건 실패${RESET}`);
84
- }
85
- }
86
- console.log("");
87
- }
1
+ import { AMBER, BOLD, DIM, GRAY, GREEN, RED, RESET } from "../../shared.mjs";
2
+ import { hasWindowsTerminalSession } from "../../session.mjs";
3
+ import { fetchHubTaskList, nativeGetStatus } from "../services/hub-client.mjs";
4
+ import { isNativeMode, isTeamAlive, isWtMode } from "../services/runtime-mode.mjs";
5
+ import { loadTeamState } from "../services/state-store.mjs";
6
+ import { formatCompletionSuffix } from "../render.mjs";
7
+
8
+ export async function teamStatus(args = []) {
9
+ const json = process.env.TFX_OUTPUT_JSON === "1" || args.includes("--json");
10
+ const state = loadTeamState();
11
+ if (!state) {
12
+ if (json) {
13
+ process.stdout.write(`${JSON.stringify({ status: "offline", sessionName: null, alive: false }, null, 2)}\n`);
14
+ return;
15
+ }
16
+ console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
17
+ return;
18
+ }
19
+
20
+ const alive = isTeamAlive(state);
21
+ const payload = {
22
+ status: alive ? "active" : "dead",
23
+ alive,
24
+ sessionName: state.sessionName,
25
+ teammateMode: state.teammateMode || "tmux",
26
+ lead: state.lead || "claude",
27
+ agents: state.agents || [],
28
+ startedAt: state.startedAt || null,
29
+ taskCount: (state.tasks || []).length,
30
+ members: (state.members || []).map((member) => ({
31
+ name: member.name,
32
+ cli: member.cli,
33
+ role: member.role,
34
+ pane: member.pane,
35
+ })),
36
+ };
37
+
38
+ if (isNativeMode(state) && alive) {
39
+ payload.nativeMembers = (await nativeGetStatus(state))?.data?.members || [];
40
+ }
41
+
42
+ if (alive) {
43
+ payload.hubTasks = await fetchHubTaskList(state);
44
+ }
45
+
46
+ if (json) {
47
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
48
+ return;
49
+ }
50
+
51
+ console.log(`\n ${AMBER}${BOLD}⬡ tfx multi${RESET} ${alive ? `${GREEN}● active${RESET}` : `${RED}● dead${RESET}`}\n`);
52
+ console.log(` 세션: ${state.sessionName}`);
53
+ console.log(` 모드: ${state.teammateMode || "tmux"}`);
54
+ console.log(` 리드: ${state.lead || "claude"}`);
55
+ console.log(` 워커: ${(state.agents || []).join(", ")}`);
56
+ console.log(` Uptime: ${alive ? `${Math.round((Date.now() - state.startedAt) / 60000)}분` : "-"}`);
57
+ console.log(` 태스크: ${(state.tasks || []).length}`);
58
+ if (isWtMode(state) && !hasWindowsTerminalSession()) {
59
+ console.log(` ${DIM}WT_SESSION 미감지: 생존성은 heuristics로 판정됨${RESET}`);
60
+ }
61
+
62
+ for (const member of state.members || []) {
63
+ console.log(` - ${member.name} (${member.cli}) ${DIM}${member.role}${RESET} ${DIM}${member.pane}${RESET}`);
64
+ }
65
+
66
+ if (isNativeMode(state) && alive) {
67
+ for (const member of payload.nativeMembers) {
68
+ console.log(` • ${member.name}: ${member.status}${formatCompletionSuffix(member)}${member.lastPreview ? ` ${DIM}${member.lastPreview}${RESET}` : ""}`);
69
+ }
70
+ }
71
+
72
+ if (alive) {
73
+ const hubTasks = payload.hubTasks || await fetchHubTaskList(state);
74
+ if (hubTasks.length) {
75
+ const completed = hubTasks.filter((task) => task.status === "completed").length;
76
+ const failed = hubTasks.filter((task) => task.status === "failed").length;
77
+ console.log(`\n ${BOLD}Hub Tasks${RESET} ${DIM}(${completed}/${hubTasks.length} done)${RESET}`);
78
+ for (const task of hubTasks) {
79
+ const icon = task.status === "completed" ? `${GREEN}✓${RESET}` : task.status === "in_progress" ? `${AMBER}●${RESET}` : task.status === "failed" ? `${RED}✗${RESET}` : `${GRAY}○${RESET}`;
80
+ const owner = task.owner ? ` ${GRAY}[${task.owner}]${RESET}` : "";
81
+ console.log(` ${icon} ${task.subject || task.description?.slice(0, 50) || ""}${owner}`);
82
+ }
83
+ if (failed > 0) console.log(` ${RED}⚠ ${failed}건 실패${RESET}`);
84
+ }
85
+ }
86
+ console.log("");
87
+ }
@@ -26,6 +26,6 @@ export async function teamStop() {
26
26
  console.log(` ${DIM}세션 이미 종료됨${RESET}`);
27
27
  }
28
28
 
29
- clearTeamState();
29
+ clearTeamState(state.sessionId);
30
30
  console.log("");
31
31
  }
@@ -24,7 +24,7 @@ export function teamTaskUpdate(args = []) {
24
24
  return;
25
25
  }
26
26
 
27
- saveTeamState({ ...state, tasks: updated.tasks });
27
+ saveTeamState({ ...state, tasks: updated.tasks }, state.sessionId);
28
28
  ok(`${updated.target.id} 상태 갱신: ${nextStatus}`);
29
29
  console.log("");
30
30
  }
@@ -1,39 +1,41 @@
1
- import { renderTeamHelp } from "./help.mjs";
2
- import { resolveTeamCommand } from "./manifest.mjs";
3
- import { teamAttach } from "./commands/attach.mjs";
4
- import { teamControl } from "./commands/control.mjs";
5
- import { teamDebug } from "./commands/debug.mjs";
6
- import { teamFocus } from "./commands/focus.mjs";
7
- import { teamInterrupt } from "./commands/interrupt.mjs";
8
- import { teamKill } from "./commands/kill.mjs";
9
- import { teamList } from "./commands/list.mjs";
10
- import { teamSend } from "./commands/send.mjs";
11
- import { teamStart } from "./commands/start/index.mjs";
12
- import { teamStatus } from "./commands/status.mjs";
13
- import { teamStop } from "./commands/stop.mjs";
14
- import { teamTaskUpdate } from "./commands/task.mjs";
15
- import { teamTasks } from "./commands/tasks.mjs";
16
-
17
- const handlers = {
18
- attach: teamAttach,
19
- control: teamControl,
20
- debug: teamDebug,
21
- focus: teamFocus,
22
- help: renderTeamHelp,
23
- interrupt: teamInterrupt,
24
- kill: teamKill,
25
- list: teamList,
26
- send: teamSend,
27
- status: teamStatus,
28
- stop: teamStop,
29
- task: teamTaskUpdate,
30
- tasks: teamTasks,
31
- };
32
-
33
- export async function cmdTeam() {
34
- const args = process.argv.slice(3);
35
- const command = resolveTeamCommand(args[0]);
36
- if (!args.length) return renderTeamHelp();
37
- if (!command) return teamStart(args);
38
- return handlers[command](args.slice(1));
39
- }
1
+ import { renderTeamHelp } from "./help.mjs";
2
+ import { resolveTeamCommand } from "./manifest.mjs";
3
+ import { teamAttach } from "./commands/attach.mjs";
4
+ import { teamControl } from "./commands/control.mjs";
5
+ import { teamDebug } from "./commands/debug.mjs";
6
+ import { teamFocus } from "./commands/focus.mjs";
7
+ import { teamInterrupt } from "./commands/interrupt.mjs";
8
+ import { teamKill } from "./commands/kill.mjs";
9
+ import { teamList } from "./commands/list.mjs";
10
+ import { teamSend } from "./commands/send.mjs";
11
+ import { teamStart } from "./commands/start/index.mjs";
12
+ import { teamStatus } from "./commands/status.mjs";
13
+ import { teamStop } from "./commands/stop.mjs";
14
+ import { teamTaskUpdate } from "./commands/task.mjs";
15
+ import { teamTasks } from "./commands/tasks.mjs";
16
+
17
+ const handlers = {
18
+ attach: teamAttach,
19
+ control: teamControl,
20
+ debug: teamDebug,
21
+ focus: teamFocus,
22
+ help: renderTeamHelp,
23
+ interrupt: teamInterrupt,
24
+ kill: teamKill,
25
+ list: teamList,
26
+ send: teamSend,
27
+ start: teamStart,
28
+ status: teamStatus,
29
+ stop: teamStop,
30
+ task: teamTaskUpdate,
31
+ tasks: teamTasks,
32
+ };
33
+
34
+ export async function cmdTeam() {
35
+ const args = process.argv.slice(3);
36
+ const command = resolveTeamCommand(args[0]);
37
+ if (!args.length) return renderTeamHelp();
38
+ // 미등록 커맨드는 teamStart로 fallthrough (팀 생성 기본값)
39
+ if (!command) return teamStart(args);
40
+ return handlers[command](args.slice(1));
41
+ }