@triflux/remote 10.50.1 → 10.51.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.
Files changed (120) hide show
  1. package/cto/brief.mjs +0 -12
  2. package/cto/collect.mjs +16 -247
  3. package/cto/current.schema.json +4 -20
  4. package/cto/events.mjs +13 -294
  5. package/cto/hygiene.mjs +26 -346
  6. package/cto/lake-root.mjs +114 -16
  7. package/cto/status.mjs +33 -128
  8. package/hub/index.mjs +0 -8
  9. package/hub/store-adapter.mjs +1 -1
  10. package/hub/team/agent-map.json +0 -1
  11. package/hub/team/ansi.mjs +1 -35
  12. package/hub/team/backend.mjs +9 -145
  13. package/hub/team/cli/commands/attach.mjs +1 -17
  14. package/hub/team/cli/commands/control.mjs +12 -38
  15. package/hub/team/cli/commands/debug.mjs +1 -37
  16. package/hub/team/cli/commands/focus.mjs +5 -31
  17. package/hub/team/cli/commands/interrupt.mjs +6 -25
  18. package/hub/team/cli/commands/kill.mjs +1 -30
  19. package/hub/team/cli/commands/list.mjs +0 -16
  20. package/hub/team/cli/commands/send.mjs +6 -26
  21. package/hub/team/cli/commands/start/index.mjs +41 -133
  22. package/hub/team/cli/commands/start/parse-args.mjs +15 -40
  23. package/hub/team/cli/commands/start/start-headless.mjs +138 -105
  24. package/hub/team/cli/commands/start/start-mux.mjs +0 -3
  25. package/hub/team/cli/commands/status.mjs +2 -57
  26. package/hub/team/cli/commands/stop.mjs +14 -19
  27. package/hub/team/cli/help.mjs +4 -5
  28. package/hub/team/cli/manifest.mjs +5 -1
  29. package/hub/team/cli/render.mjs +0 -12
  30. package/hub/team/cli/services/hub-client.mjs +2 -88
  31. package/hub/team/cli/services/runtime-mode.mjs +27 -98
  32. package/hub/team/handoff.mjs +3 -34
  33. package/hub/team/headless.mjs +14 -755
  34. package/hub/team/orchestrator.mjs +4 -30
  35. package/hub/team/pane.mjs +6 -28
  36. package/hub/team/psmux.mjs +16 -403
  37. package/hub/team/remote-session.mjs +1 -2
  38. package/hub/team/session.mjs +8 -119
  39. package/hub/team/swarm-intent.mjs +0 -198
  40. package/hub/team/terminal-opener.mjs +11 -35
  41. package/hub/team/tui-core.mjs +1 -1
  42. package/hub/team/tui-viewer.mjs +2 -2
  43. package/hub/team/tui.mjs +8 -269
  44. package/hub/team/wt-manager.mjs +2 -2
  45. package/hub/workers/factory.mjs +4 -37
  46. package/hub/workers/lib/jsonrpc-ws-uds.mjs +0 -2
  47. package/package.json +1 -1
  48. package/scripts/lib/agent-route-policy.mjs +162 -14
  49. package/scripts/lib/claudemd-scanner.mjs +2 -199
  50. package/scripts/lib/cli-claude.mjs +0 -2
  51. package/scripts/lib/context.mjs +3 -19
  52. package/scripts/lib/env-probe.mjs +5 -64
  53. package/scripts/lib/gemini-profiles.mjs +2 -69
  54. package/scripts/lib/legacy-mcp-cleanup.mjs +14 -3
  55. package/scripts/lib/machine-profile.mjs +3 -17
  56. package/scripts/lib/mcp-filter.mjs +109 -53
  57. package/scripts/lib/mcp-health.mjs +2 -2
  58. package/scripts/lib/mcp-server-catalog.mjs +2 -2
  59. package/scripts/lib/psmux-info.mjs +25 -104
  60. package/scripts/lib/skill-template.mjs +0 -271
  61. package/cto/hygiene-actions.mjs +0 -355
  62. package/cto/hygiene-notify.mjs +0 -170
  63. package/hub/team/build-worker-prompt.mjs +0 -92
  64. package/hub/team/check-mcp-hub.mjs +0 -49
  65. package/hub/team/claude-native-bridge.mjs +0 -987
  66. package/hub/team/cli/commands/start/start-in-process.mjs +0 -50
  67. package/hub/team/cli/commands/start/start-wt.mjs +0 -86
  68. package/hub/team/cli/services/native-control.mjs +0 -145
  69. package/hub/team/codex-review.mjs +0 -520
  70. package/hub/team/conductor-mesh-bridge.mjs +0 -122
  71. package/hub/team/conductor.mjs +0 -1529
  72. package/hub/team/consensus-meta.mjs +0 -176
  73. package/hub/team/cto-pull.mjs +0 -31
  74. package/hub/team/daemon-pty-tmux-bridge.mjs +0 -146
  75. package/hub/team/dashboard.mjs +0 -322
  76. package/hub/team/event-log.mjs +0 -84
  77. package/hub/team/execution-mode.mjs +0 -324
  78. package/hub/team/extract-completion-payload.mjs +0 -110
  79. package/hub/team/headless-bridge-session.mjs +0 -17
  80. package/hub/team/health-probe.mjs +0 -435
  81. package/hub/team/interactive-native-launcher.mjs +0 -119
  82. package/hub/team/interactive-native-worker.mjs +0 -80
  83. package/hub/team/interactive-tui-transport.mjs +0 -479
  84. package/hub/team/launcher-template.mjs +0 -144
  85. package/hub/team/mcp-selector.mjs +0 -145
  86. package/hub/team/native-supervisor.mjs +0 -460
  87. package/hub/team/native.mjs +0 -727
  88. package/hub/team/recovery-store.mjs +0 -149
  89. package/hub/team/remote-probe.mjs +0 -292
  90. package/hub/team/remote-watcher.mjs +0 -506
  91. package/hub/team/runtime-strategy.mjs +0 -200
  92. package/hub/team/schemas/worker-completion.json +0 -39
  93. package/hub/team/sentinel-capture.mjs +0 -127
  94. package/hub/team/swarm-cli.mjs +0 -390
  95. package/hub/team/swarm-hypervisor.mjs +0 -2206
  96. package/hub/team/swarm-locks.mjs +0 -335
  97. package/hub/team/swarm-planner.mjs +0 -371
  98. package/hub/team/swarm-preflight.mjs +0 -366
  99. package/hub/team/swarm-reconciler.mjs +0 -242
  100. package/hub/team/tui-remote-adapter.mjs +0 -377
  101. package/hub/team/worker-completion-validator.mjs +0 -79
  102. package/hub/team/worktree-lifecycle.mjs +0 -639
  103. package/hub/team/wt-templates.json +0 -43
  104. package/hub/workers/antigravity-route-worker.mjs +0 -179
  105. package/hub/workers/gemini-worker.mjs +0 -217
  106. package/scripts/lib/agent-json.mjs +0 -27
  107. package/scripts/lib/async.mjs +0 -174
  108. package/scripts/lib/cli-agy.mjs +0 -85
  109. package/scripts/lib/cli-codex.mjs +0 -62
  110. package/scripts/lib/cli-gemini.mjs +0 -37
  111. package/scripts/lib/dynamic-route-cli.mjs +0 -107
  112. package/scripts/lib/env.mjs +0 -14
  113. package/scripts/lib/hub.mjs +0 -48
  114. package/scripts/lib/pid.mjs +0 -63
  115. package/scripts/lib/quota.mjs +0 -47
  116. package/scripts/lib/skill-state.mjs +0 -220
  117. package/scripts/lib/team.mjs +0 -75
  118. package/scripts/lib/timeout.mjs +0 -66
  119. package/scripts/lib/tmp.mjs +0 -26
  120. package/scripts/lib/toml.mjs +0 -132
@@ -1,176 +0,0 @@
1
- const VALID_SHAPES = new Set(["consensus", "debate", "panel"]);
2
- const VALID_ACTIONS = new Set([
3
- "merge",
4
- "FIX_FIRST",
5
- "close",
6
- "defer",
7
- "split",
8
- ]);
9
- const DEFAULT_SHAPE = "consensus";
10
-
11
- export const CONSENSUS_SHAPE_COMPLEXITY = Object.freeze({
12
- consensus: 1.0,
13
- debate: 1.2,
14
- panel: 1.4,
15
- });
16
-
17
- export const CONSENSUS_MARKDOWN_SECTIONS = Object.freeze({
18
- consensus: Object.freeze([
19
- "## 합의 결과: {topic}",
20
- "### Consensus Score",
21
- "### 합의 항목",
22
- "### disputed items",
23
- "### resolved items",
24
- "### user decision needed",
25
- "### meta judgment",
26
- ]),
27
- debate: Object.freeze([
28
- "## 토론 결과: {topic}",
29
- "### 비교 대상",
30
- "### 평가 기준",
31
- "### 합의 사항",
32
- "### 최종 추천",
33
- "### 리스크 및 완화 방안",
34
- "### meta judgment",
35
- ]),
36
- panel: Object.freeze([
37
- "## 전문가 패널 보고서: {topic}",
38
- "### 패널 구성",
39
- "### 패널 합의",
40
- "### 소수 견해",
41
- "### 핵심 추천",
42
- "### 미해결 쟁점",
43
- "### 다음 단계",
44
- "### meta judgment",
45
- ]),
46
- });
47
-
48
- function toArray(value) {
49
- if (Array.isArray(value)) return value.filter(Boolean);
50
- if (value == null) return [];
51
- return [value].filter(Boolean);
52
- }
53
-
54
- function normalizeShape(shape) {
55
- return VALID_SHAPES.has(shape) ? shape : DEFAULT_SHAPE;
56
- }
57
-
58
- function normalizeRecommendedAction(action) {
59
- return VALID_ACTIONS.has(action) ? action : "defer";
60
- }
61
-
62
- function normalizeStringList(items) {
63
- return [
64
- ...new Set(
65
- toArray(items)
66
- .map((item) => String(item).trim())
67
- .filter(Boolean),
68
- ),
69
- ];
70
- }
71
-
72
- function normalizeSeverityBucket(items) {
73
- return toArray(items).map((item) =>
74
- typeof item === "string" ? { id: item, summary: item } : { ...item },
75
- );
76
- }
77
-
78
- function buildSeverityClassification({ p1 = [], p2 = [], p3 = [] } = {}) {
79
- return {
80
- p1: normalizeSeverityBucket(p1),
81
- p2: normalizeSeverityBucket(p2),
82
- p3: normalizeSeverityBucket(p3),
83
- };
84
- }
85
-
86
- function buildConsensusVsDispute({ agreements = [], conflicts = [] } = {}) {
87
- return {
88
- agreements: toArray(agreements).map((item) =>
89
- typeof item === "string" ? { summary: item } : { ...item },
90
- ),
91
- conflicts: toArray(conflicts).map((item) =>
92
- typeof item === "string"
93
- ? { summary: item, parties: [] }
94
- : { parties: [], ...item },
95
- ),
96
- };
97
- }
98
-
99
- function inferStatus(participants) {
100
- const statuses = participants.map((participant) => participant.status);
101
- if (statuses.length === 0) return "needs_user_input";
102
- if (statuses.every((status) => status === "success")) return "complete";
103
- if (statuses.some((status) => status === "needs_user_input"))
104
- return "needs_user_input";
105
- return "partial";
106
- }
107
-
108
- export function normalizeParticipants(participants = []) {
109
- return toArray(participants).map((participant) => {
110
- if (typeof participant === "string") {
111
- return { name: participant, status: "success" };
112
- }
113
-
114
- return {
115
- name: participant?.name ?? "unknown",
116
- status: participant?.status ?? "success",
117
- ...participant,
118
- };
119
- });
120
- }
121
-
122
- export function buildMetaJudgment({
123
- severity = {},
124
- consensus = {},
125
- recommendedAction = "defer",
126
- followupIssues = [],
127
- modeSpecificMeta = {},
128
- } = {}) {
129
- return {
130
- severity_classification: buildSeverityClassification(severity),
131
- consensus_vs_dispute: buildConsensusVsDispute(consensus),
132
- recommended_action: normalizeRecommendedAction(recommendedAction),
133
- followup_issues: normalizeStringList(followupIssues),
134
- mode_specific_meta: { ...modeSpecificMeta },
135
- };
136
- }
137
-
138
- export function buildConsensusArtifactMeta({
139
- shape = DEFAULT_SHAPE,
140
- topic = "",
141
- cliSet = "triad",
142
- participants = [],
143
- status,
144
- } = {}) {
145
- const normalizedParticipants = normalizeParticipants(participants);
146
- return {
147
- mode: "consensus",
148
- shape: normalizeShape(shape),
149
- topic,
150
- cli_set: cliSet,
151
- participants: normalizedParticipants,
152
- status: status ?? inferStatus(normalizedParticipants),
153
- };
154
- }
155
-
156
- export function buildConsensusEnvelope({
157
- shape = DEFAULT_SHAPE,
158
- topic = "",
159
- cliSet = "triad",
160
- participants = [],
161
- metaJudgment = {},
162
- } = {}) {
163
- return {
164
- ...buildConsensusArtifactMeta({ shape, topic, cliSet, participants }),
165
- meta_judgment: buildMetaJudgment(metaJudgment),
166
- };
167
- }
168
-
169
- export function getShapeContract(shape = DEFAULT_SHAPE) {
170
- const normalizedShape = normalizeShape(shape);
171
- return {
172
- shape: normalizedShape,
173
- complexity: CONSENSUS_SHAPE_COMPLEXITY[normalizedShape],
174
- required_markdown_sections: CONSENSUS_MARKDOWN_SECTIONS[normalizedShape],
175
- };
176
- }
@@ -1,31 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import { join } from "node:path";
3
-
4
- import { resolveRoleControlSnapshot } from "../lib/cto-env.mjs";
5
-
6
- function defaultLakeRoot() {
7
- return join(process.cwd(), ".triflux", "lake");
8
- }
9
-
10
- /**
11
- * Read the CTO north-star lake snapshot without affecting runtime control flow.
12
- * @param {object} [opts]
13
- * @param {string} [opts.lakeRoot] Directory containing current.json.
14
- * @param {object} [opts.env] Environment values used for the CTO kill-switch.
15
- * @returns {object|null}
16
- */
17
- export function readCtoSnapshot(opts = {}) {
18
- if (!resolveRoleControlSnapshot(opts.env).north_star_enabled) return null;
19
- try {
20
- const lakeRoot = opts.lakeRoot || defaultLakeRoot();
21
- const parsed = JSON.parse(
22
- readFileSync(join(lakeRoot, "current.json"), "utf8"),
23
- );
24
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
25
- return null;
26
- }
27
- return parsed;
28
- } catch {
29
- return null;
30
- }
31
- }
@@ -1,146 +0,0 @@
1
- import { pathToFileURL } from "node:url";
2
-
3
- import { createInteractiveTuiTransport } from "./interactive-tui-transport.mjs";
4
-
5
- const DEFAULT_COLS = 120;
6
- const DEFAULT_ROWS = 40;
7
- const SIGNAL_EXIT_CODES = new Map([
8
- ["SIGHUP", 129],
9
- ["SIGINT", 130],
10
- ["SIGTERM", 143],
11
- ]);
12
-
13
- export function encodeBridgeConfig(config) {
14
- return Buffer.from(JSON.stringify(config), "utf8").toString("base64");
15
- }
16
-
17
- export function parseBridgeConfig(argv = []) {
18
- const index = argv.indexOf("--config");
19
- if (index < 0 || !argv[index + 1]) {
20
- throw new Error("--config <base64-json> is required");
21
- }
22
- return JSON.parse(Buffer.from(argv[index + 1], "base64").toString("utf8"));
23
- }
24
-
25
- export async function runBridge({
26
- config,
27
- stdin,
28
- stdout,
29
- signals,
30
- createTransport = createInteractiveTuiTransport,
31
- } = {}) {
32
- if (!config?.sessionName) throw new Error("config.sessionName is required");
33
- if (!stdin) throw new Error("stdin is required");
34
- if (!stdout) throw new Error("stdout is required");
35
- if (!signals) throw new Error("signals is required");
36
-
37
- const transport = createTransport({
38
- sessionName: config.sessionName,
39
- cwd: config.cwd,
40
- launchCmd: config.launchCmd,
41
- env: config.env || {},
42
- onData: (chunk) => stdout.write(chunk),
43
- });
44
- let shutdownPromise = null;
45
-
46
- const currentSize = () => ({
47
- cols: stdout.columns || config.cols || DEFAULT_COLS,
48
- rows: stdout.rows || config.rows || DEFAULT_ROWS,
49
- });
50
-
51
- const shutdown = async ({ exitCode, exitProcess = false } = {}) => {
52
- if (!shutdownPromise) {
53
- shutdownPromise = (async () => {
54
- unregister();
55
- await transport.stop();
56
- })();
57
- }
58
- await shutdownPromise;
59
- if (exitProcess && typeof signals.exit === "function") {
60
- signals.exit(exitCode ?? 0);
61
- }
62
- };
63
-
64
- const writeInput = (chunk) => {
65
- void transport.writeInput(chunk).catch(() => {
66
- void shutdown({ exitCode: 1, exitProcess: true });
67
- });
68
- };
69
- const resize = () => {
70
- void transport.resize(currentSize()).catch(() => {
71
- void shutdown({ exitCode: 1, exitProcess: true });
72
- });
73
- };
74
- const stdinEnd = () => {
75
- void shutdown({ exitCode: 0, exitProcess: true });
76
- };
77
- const signalHandlers = new Map(
78
- [...SIGNAL_EXIT_CODES].map(([signal, exitCode]) => [
79
- signal,
80
- () => {
81
- void shutdown({ exitCode, exitProcess: true });
82
- },
83
- ]),
84
- );
85
- const uncaughtException = () => {
86
- void shutdown({ exitCode: 1, exitProcess: true });
87
- };
88
- const beforeExit = () => {
89
- void shutdown();
90
- };
91
- const exit = () => {
92
- void shutdown();
93
- };
94
-
95
- function unregister() {
96
- stdin.off?.("data", writeInput);
97
- stdin.off?.("end", stdinEnd);
98
- stdout.off?.("resize", resize);
99
- signals.off?.("uncaughtException", uncaughtException);
100
- signals.off?.("beforeExit", beforeExit);
101
- signals.off?.("exit", exit);
102
- for (const [signal, handler] of signalHandlers) {
103
- signals.off?.(signal, handler);
104
- }
105
- }
106
-
107
- stdin.on("data", writeInput);
108
- stdin.on("end", stdinEnd);
109
- stdout.on?.("resize", resize);
110
- signals.on?.("uncaughtException", uncaughtException);
111
- signals.on?.("beforeExit", beforeExit);
112
- signals.on?.("exit", exit);
113
- for (const [signal, handler] of signalHandlers) {
114
- signals.on?.(signal, handler);
115
- }
116
-
117
- try {
118
- await transport.start();
119
- await transport.resize(currentSize());
120
- } catch (error) {
121
- await shutdown().catch(() => {});
122
- throw error;
123
- }
124
-
125
- return { shutdown };
126
- }
127
-
128
- async function main() {
129
- const config = parseBridgeConfig(process.argv.slice(2));
130
- await runBridge({
131
- config,
132
- stdin: process.stdin,
133
- stdout: process.stdout,
134
- signals: process,
135
- });
136
- }
137
-
138
- if (
139
- process.argv[1] &&
140
- import.meta.url === pathToFileURL(process.argv[1]).href
141
- ) {
142
- main().catch((error) => {
143
- console.error(error?.stack || error?.message || String(error));
144
- process.exit(1);
145
- });
146
- }
@@ -1,322 +0,0 @@
1
- #!/usr/bin/env node
2
- // hub/team/dashboard.mjs — 실시간 팀 상태 표시 (v2.2)
3
- // tmux 의존 제거 — Hub task-list + native-supervisor 기반
4
- //
5
- // 실행:
6
- // node hub/team/dashboard.mjs --session <세션이름> [--interval 2]
7
- // node hub/team/dashboard.mjs --team <팀이름> [--interval 2]
8
- import { get } from "node:http";
9
- import { AMBER, BOLD, DIM, GRAY, GREEN, RED, RESET } from "./shared.mjs";
10
-
11
- /**
12
- * HTTP GET JSON
13
- * @param {string} url
14
- * @returns {Promise<object|null>}
15
- */
16
- function fetchJson(url) {
17
- return new Promise((resolve) => {
18
- const req = get(url, { timeout: 2000 }, (res) => {
19
- let data = "";
20
- res.on("data", (chunk) => (data += chunk));
21
- res.on("end", () => {
22
- try {
23
- resolve(JSON.parse(data));
24
- } catch {
25
- resolve(null);
26
- }
27
- });
28
- });
29
- req.on("error", () => resolve(null));
30
- req.on("timeout", () => {
31
- req.destroy();
32
- resolve(null);
33
- });
34
- });
35
- }
36
-
37
- /**
38
- * HTTP POST JSON (Hub bridge 용)
39
- * @param {string} url
40
- * @param {object} body
41
- * @returns {Promise<object|null>}
42
- */
43
- async function fetchPost(url, body = {}) {
44
- try {
45
- const res = await fetch(url, {
46
- method: "POST",
47
- headers: { "Content-Type": "application/json" },
48
- body: JSON.stringify(body),
49
- signal: AbortSignal.timeout(2000),
50
- });
51
- return await res.json();
52
- } catch {
53
- return null;
54
- }
55
- }
56
-
57
- /**
58
- * 진행률 바 생성
59
- * @param {number} pct — 0~100
60
- * @param {number} width — 바 너비 (기본 8)
61
- * @returns {string}
62
- */
63
- function progressBar(pct, width = 8) {
64
- const filled = Math.round((pct / 100) * width);
65
- const empty = width - filled;
66
- return `${GREEN}${"█".repeat(filled)}${GRAY}${"░".repeat(empty)}${RESET}`;
67
- }
68
-
69
- /**
70
- * 업타임 포맷
71
- * @param {number} ms
72
- * @returns {string}
73
- */
74
- function formatUptime(ms) {
75
- if (ms < 60000) return `${Math.round(ms / 1000)}초`;
76
- if (ms < 3600000) return `${Math.round(ms / 60000)}분`;
77
- return `${Math.round(ms / 3600000)}시간`;
78
- }
79
-
80
- /**
81
- * task 상태 아이콘
82
- * @param {string} status
83
- * @returns {string}
84
- */
85
- function statusIcon(status) {
86
- switch (status) {
87
- case "completed":
88
- return `${GREEN}✓${RESET}`;
89
- case "in_progress":
90
- return `${AMBER}●${RESET}`;
91
- case "failed":
92
- return `${RED}✗${RESET}`;
93
- default:
94
- return `${GRAY}○${RESET}`;
95
- }
96
- }
97
-
98
- /**
99
- * 멤버 목록 구성: Hub tasks + supervisor + teamState 통합
100
- * @param {Array} hubTasks — Hub bridge task-list 결과
101
- * @param {Array} supervisorMembers — native-supervisor 멤버 상태
102
- * @param {object} teamState — team-state.json 내용
103
- * @returns {Array<{name: string, cli: string, status: string, subject: string, preview: string}>}
104
- */
105
- function buildMemberList(hubTasks, supervisorMembers, teamState) {
106
- const members = [];
107
- const supervisorByName = new Map(supervisorMembers.map((m) => [m.name, m]));
108
-
109
- // Hub tasks가 있으면 주 데이터 소스
110
- if (hubTasks.length > 0) {
111
- for (const task of hubTasks) {
112
- const owner = task.owner || task.subject || "";
113
- const sup = supervisorByName.get(owner);
114
- members.push({
115
- name: owner,
116
- cli: task.metadata?.cli || sup?.cli || "",
117
- status: task.status || "pending",
118
- subject: task.subject || "",
119
- preview: sup?.lastPreview || task.description?.slice(0, 80) || "",
120
- });
121
- }
122
- return members;
123
- }
124
-
125
- // Supervisor 데이터 폴백
126
- if (supervisorMembers.length > 0) {
127
- for (const m of supervisorMembers) {
128
- if (m.role === "lead") continue;
129
- members.push({
130
- name: m.name,
131
- cli: m.cli || "",
132
- status:
133
- m.status === "running"
134
- ? "in_progress"
135
- : m.status === "exited"
136
- ? "completed"
137
- : m.status,
138
- subject: "",
139
- preview: m.lastPreview || "",
140
- });
141
- }
142
- return members;
143
- }
144
-
145
- // teamState 폴백 (하위 호환)
146
- const panes = teamState?.panes || {};
147
- for (const [, paneInfo] of Object.entries(panes).filter(
148
- ([, v]) => v.role !== "dashboard" && v.role !== "lead",
149
- )) {
150
- members.push({
151
- name: paneInfo.agentId || paneInfo.name || "?",
152
- cli: paneInfo.cli || "",
153
- status: "unknown",
154
- subject: paneInfo.subtask || "",
155
- preview: "",
156
- });
157
- }
158
- return members;
159
- }
160
-
161
- /**
162
- * 대시보드 렌더링 (v2.2: Hub/supervisor 기반)
163
- * @param {string} sessionName — 세션 또는 팀 이름
164
- * @param {object} opts
165
- * @param {string} opts.hubUrl — Hub URL (기본 http://127.0.0.1:27888)
166
- * @param {string} [opts.teamName] — Hub task-list 조회용 팀 이름
167
- * @param {string} [opts.supervisorUrl] — native-supervisor 제어 URL
168
- * @param {object} [opts.teamState] — team-state.json 내용 (하위 호환)
169
- */
170
- export async function renderDashboard(sessionName, opts = {}) {
171
- const {
172
- hubUrl = "http://127.0.0.1:27888",
173
- teamName,
174
- supervisorUrl,
175
- teamState = {},
176
- } = opts;
177
- const W = 50;
178
- const border = "─".repeat(W);
179
-
180
- // 데이터 수집 (병렬)
181
- const [hubStatus, taskListRes, supervisorRes] = await Promise.all([
182
- fetchJson(`${hubUrl}/status`),
183
- teamName
184
- ? fetchPost(`${hubUrl}/bridge/team/task-list`, { team_name: teamName })
185
- : null,
186
- supervisorUrl ? fetchJson(`${supervisorUrl}/status`) : null,
187
- ]);
188
-
189
- const hubOnline = !!hubStatus;
190
- const hubState = hubOnline
191
- ? `${GREEN}● online${RESET}`
192
- : `${RED}● offline${RESET}`;
193
- const uptime = hubStatus?.hub?.uptime
194
- ? formatUptime(hubStatus.hub.uptime)
195
- : "-";
196
- const queueSize = hubStatus?.hub?.queue_depth ?? 0;
197
-
198
- // Hub task 데이터
199
- const hubTasks = taskListRes?.ok ? taskListRes.data?.tasks || [] : [];
200
- const completedCount = hubTasks.filter(
201
- (t) => t.status === "completed",
202
- ).length;
203
- const totalCount = hubTasks.length;
204
-
205
- // Supervisor 멤버 데이터
206
- const supervisorMembers = supervisorRes?.ok
207
- ? supervisorRes.data?.members || []
208
- : [];
209
-
210
- // 헤더
211
- const progress = totalCount > 0 ? ` ${completedCount}/${totalCount}` : "";
212
- console.log(
213
- `${AMBER}┌─ ${sessionName}${progress} ${GRAY}${"─".repeat(Math.max(0, W - sessionName.length - progress.length - 3))}${AMBER}┐${RESET}`,
214
- );
215
- console.log(
216
- `${AMBER}│${RESET} Hub: ${hubState} Uptime: ${DIM}${uptime}${RESET} Queue: ${DIM}${queueSize}${RESET}`,
217
- );
218
- console.log(`${AMBER}│${RESET}`);
219
-
220
- // 멤버/워커 렌더링
221
- const members = buildMemberList(hubTasks, supervisorMembers, teamState);
222
-
223
- if (members.length === 0) {
224
- console.log(`${AMBER}│${RESET} ${DIM}에이전트 정보 없음${RESET}`);
225
- } else {
226
- for (const m of members) {
227
- const icon = statusIcon(m.status);
228
- const label = `[${m.name}]`;
229
- const cliTag = m.cli
230
- ? m.cli.charAt(0).toUpperCase() + m.cli.slice(1)
231
- : "";
232
-
233
- // 진행률 추정
234
- const pct =
235
- m.status === "completed"
236
- ? 100
237
- : m.status === "in_progress"
238
- ? 50
239
- : m.status === "failed"
240
- ? 100
241
- : 0;
242
-
243
- console.log(
244
- `${AMBER}│${RESET} ${BOLD}${label}${RESET} ${cliTag} ${icon} ${m.status || "pending"} ${progressBar(pct)}`,
245
- );
246
-
247
- // 미리보기: supervisor lastPreview > task subject
248
- const preview = m.preview || m.subject || "";
249
- if (preview) {
250
- const truncated =
251
- preview.length > W - 8 ? preview.slice(0, W - 11) + "..." : preview;
252
- console.log(`${AMBER}│${RESET} ${DIM}> ${truncated}${RESET}`);
253
- }
254
- console.log(`${AMBER}│${RESET}`);
255
- }
256
- }
257
-
258
- // 푸터
259
- console.log(`${AMBER}└${GRAY}${border}${AMBER}┘${RESET}`);
260
- }
261
-
262
- /** team-state.json 로드 (세션별 파일 우선, fallback: team-state.json) */
263
- async function loadTeamState() {
264
- try {
265
- const { existsSync, readFileSync } = await import("node:fs");
266
- const { join } = await import("node:path");
267
- const { homedir } = await import("node:os");
268
- const hubDir = join(homedir(), ".claude", "cache", "tfx-hub");
269
- const sessionId = process.env.CLAUDE_SESSION_ID;
270
- if (sessionId) {
271
- const sessionPath = join(hubDir, `team-state-${sessionId}.json`);
272
- if (existsSync(sessionPath))
273
- return JSON.parse(readFileSync(sessionPath, "utf8"));
274
- }
275
- const legacyPath = join(hubDir, "team-state.json");
276
- if (existsSync(legacyPath))
277
- return JSON.parse(readFileSync(legacyPath, "utf8"));
278
- return {};
279
- } catch {
280
- return {};
281
- }
282
- }
283
-
284
- // ── CLI 실행 ──
285
- if (process.argv[1]?.includes("dashboard.mjs")) {
286
- const sessionIdx = process.argv.indexOf("--session");
287
- const teamIdx = process.argv.indexOf("--team");
288
- const sessionName = sessionIdx !== -1 ? process.argv[sessionIdx + 1] : null;
289
- const teamName = teamIdx !== -1 ? process.argv[teamIdx + 1] : null;
290
- const intervalSec = parseInt(
291
- process.argv[process.argv.indexOf("--interval") + 1] || "2",
292
- 10,
293
- );
294
-
295
- const displayName = sessionName || teamName;
296
- if (!displayName) {
297
- console.error(
298
- "사용법: node dashboard.mjs --session <세션이름> [--team <팀이름>] [--interval 2]",
299
- );
300
- process.exit(1);
301
- }
302
-
303
- // Ctrl+C로 종료
304
- process.on("SIGINT", () => process.exit(0));
305
-
306
- // 갱신 루프
307
- while (true) {
308
- const teamState = await loadTeamState();
309
- const effectiveTeamName = teamName || null;
310
- const supervisorUrl = teamState?.native?.controlUrl || null;
311
-
312
- // 화면 클리어 (ANSI)
313
- process.stdout.write("\x1b[2J\x1b[H");
314
- await renderDashboard(displayName, {
315
- teamName: effectiveTeamName,
316
- supervisorUrl,
317
- teamState,
318
- });
319
- console.log(`${DIM} ${intervalSec}초 간격 갱신 | Ctrl+C로 종료${RESET}`);
320
- await new Promise((r) => setTimeout(r, intervalSec * 1000));
321
- }
322
- }