@triflux/remote 10.1.0 → 10.25.1

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 (155) hide show
  1. package/hub/pipe.mjs +239 -121
  2. package/hub/public/dashboard.html +61 -0
  3. package/hub/server.mjs +2165 -708
  4. package/hub/store-adapter.mjs +59 -26
  5. package/hub/store.mjs +356 -153
  6. package/hub/team/agent-map.json +26 -7
  7. package/hub/team/ansi.mjs +245 -146
  8. package/hub/team/backend.mjs +84 -17
  9. package/hub/team/build-worker-prompt.mjs +42 -0
  10. package/hub/team/check-mcp-hub.mjs +49 -0
  11. package/hub/team/claude-daemon-control.mjs +219 -0
  12. package/hub/team/claude-native-bridge.mjs +784 -0
  13. package/hub/team/claude-session-projection.mjs +65 -0
  14. package/hub/team/cli/commands/attach.mjs +57 -37
  15. package/hub/team/cli/commands/control.mjs +29 -8
  16. package/hub/team/cli/commands/debug.mjs +95 -74
  17. package/hub/team/cli/commands/focus.mjs +80 -53
  18. package/hub/team/cli/commands/interrupt.mjs +18 -6
  19. package/hub/team/cli/commands/kill.mjs +16 -5
  20. package/hub/team/cli/commands/list.mjs +31 -24
  21. package/hub/team/cli/commands/send.mjs +19 -6
  22. package/hub/team/cli/commands/start/index.mjs +154 -33
  23. package/hub/team/cli/commands/start/parse-args.mjs +89 -12
  24. package/hub/team/cli/commands/start/start-headless.mjs +125 -37
  25. package/hub/team/cli/commands/start/start-in-process.mjs +50 -40
  26. package/hub/team/cli/commands/start/start-mux.mjs +122 -73
  27. package/hub/team/cli/commands/start/start-wt.mjs +86 -69
  28. package/hub/team/cli/commands/status.mjs +43 -14
  29. package/hub/team/cli/commands/stop.mjs +11 -4
  30. package/hub/team/cli/commands/task.mjs +8 -3
  31. package/hub/team/cli/commands/tasks.mjs +13 -13
  32. package/hub/team/cli/index.mjs +2 -2
  33. package/hub/team/cli/manifest.mjs +38 -8
  34. package/hub/team/cli/render.mjs +52 -30
  35. package/hub/team/cli/services/attach-fallback.mjs +74 -54
  36. package/hub/team/cli/services/hub-client.mjs +44 -16
  37. package/hub/team/cli/services/member-selector.mjs +37 -30
  38. package/hub/team/cli/services/native-control.mjs +144 -116
  39. package/hub/team/cli/services/runtime-mode.mjs +55 -1
  40. package/hub/team/cli/services/state-store.mjs +66 -9
  41. package/hub/team/cli/services/task-model.mjs +40 -30
  42. package/hub/team/codex-review.mjs +521 -0
  43. package/hub/team/conductor-mesh-bridge.mjs +25 -24
  44. package/hub/team/conductor-registry.mjs +121 -0
  45. package/hub/team/conductor.mjs +1379 -671
  46. package/hub/team/consensus-meta.mjs +176 -0
  47. package/hub/team/dashboard-anchor.mjs +4 -5
  48. package/hub/team/dashboard-layout.mjs +3 -1
  49. package/hub/team/dashboard-open.mjs +20 -118
  50. package/hub/team/dashboard.mjs +76 -28
  51. package/hub/team/event-log.mjs +18 -10
  52. package/hub/team/execution-mode.mjs +241 -0
  53. package/hub/team/extract-completion-payload.mjs +110 -0
  54. package/hub/team/git-preflight.mjs +286 -0
  55. package/hub/team/handoff.mjs +49 -18
  56. package/hub/team/headless-bridge-session.mjs +17 -0
  57. package/hub/team/headless.mjs +2065 -1156
  58. package/hub/team/health-probe.mjs +232 -69
  59. package/hub/team/launcher-template.mjs +23 -13
  60. package/hub/team/lead-control.mjs +91 -4
  61. package/hub/team/mcp-selector.mjs +145 -0
  62. package/hub/team/native-supervisor.mjs +90 -26
  63. package/hub/team/native.mjs +74 -35
  64. package/hub/team/nativeProxy.mjs +184 -113
  65. package/hub/team/notify.mjs +147 -78
  66. package/hub/team/orchestrator.mjs +173 -161
  67. package/hub/team/pane.mjs +43 -15
  68. package/hub/team/process-cleanup.mjs +69 -25
  69. package/hub/team/psmux.mjs +1020 -279
  70. package/hub/team/recovery-store.mjs +149 -0
  71. package/hub/team/remote-probe.mjs +68 -52
  72. package/hub/team/remote-session.mjs +131 -59
  73. package/hub/team/remote-watcher.mjs +61 -33
  74. package/hub/team/retry-state-machine.mjs +267 -0
  75. package/hub/team/routing.mjs +51 -25
  76. package/hub/team/runtime-strategy.mjs +200 -0
  77. package/hub/team/schemas/worker-completion.json +39 -0
  78. package/hub/team/sentinel-capture.mjs +127 -0
  79. package/hub/team/session-sync.mjs +153 -6
  80. package/hub/team/session.mjs +605 -611
  81. package/hub/team/shared.mjs +13 -13
  82. package/hub/team/staleState.mjs +72 -30
  83. package/hub/team/swarm-cli.mjs +390 -0
  84. package/hub/team/swarm-hypervisor.mjs +1522 -193
  85. package/hub/team/swarm-intent.mjs +216 -0
  86. package/hub/team/swarm-locks.mjs +140 -25
  87. package/hub/team/swarm-planner.mjs +121 -21
  88. package/hub/team/swarm-preflight.mjs +317 -0
  89. package/hub/team/swarm-reconciler.mjs +128 -23
  90. package/hub/team/synapse-cli.mjs +458 -0
  91. package/hub/team/synapse-http.mjs +60 -0
  92. package/hub/team/synapse-registry.mjs +329 -0
  93. package/hub/team/terminal-opener.mjs +216 -0
  94. package/hub/team/tui-core.mjs +288 -0
  95. package/hub/team/tui-lite.mjs +234 -146
  96. package/hub/team/tui-remote-adapter.mjs +18 -34
  97. package/hub/team/tui-synapse.mjs +223 -0
  98. package/hub/team/tui-viewer.mjs +104 -44
  99. package/hub/team/tui-widgets.mjs +262 -0
  100. package/hub/team/tui.mjs +927 -437
  101. package/hub/team/worker-completion-validator.mjs +84 -0
  102. package/hub/team/worker-sandbox.mjs +93 -0
  103. package/hub/team/worker-signal.mjs +263 -0
  104. package/hub/team/worker-signal.types.d.ts +52 -0
  105. package/hub/team/worktree-lifecycle.mjs +517 -63
  106. package/hub/team/wt-manager.mjs +297 -26
  107. package/hub/tools.mjs +474 -252
  108. package/hub/tray.mjs +411 -368
  109. package/hub/workers/claude-worker.mjs +162 -118
  110. package/hub/workers/codex-app-server-worker.mjs +1015 -0
  111. package/hub/workers/codex-mcp.mjs +724 -507
  112. package/hub/workers/delegator-mcp.mjs +662 -335
  113. package/hub/workers/factory.mjs +100 -21
  114. package/hub/workers/gemini-worker.mjs +179 -86
  115. package/hub/workers/interface.mjs +23 -2
  116. package/hub/workers/lib/jsonrpc-stdio.mjs +463 -0
  117. package/hub/workers/worker-utils.mjs +22 -16
  118. package/package.json +4 -3
  119. package/scripts/lib/agent-json.mjs +27 -0
  120. package/scripts/lib/async.mjs +174 -0
  121. package/scripts/lib/claudemd-scanner.mjs +223 -0
  122. package/scripts/lib/cli-agy.mjs +62 -0
  123. package/scripts/lib/cli-claude.mjs +78 -0
  124. package/scripts/lib/cli-codex.mjs +199 -0
  125. package/scripts/lib/cli-gemini.mjs +67 -0
  126. package/scripts/lib/codex-recovery.sh +50 -0
  127. package/scripts/lib/context.mjs +67 -0
  128. package/scripts/lib/cross-review-utils.mjs +59 -0
  129. package/scripts/lib/doctor-env-checks.mjs +121 -0
  130. package/scripts/lib/dynamic-route-cli.mjs +107 -0
  131. package/scripts/lib/env-probe.mjs +269 -0
  132. package/scripts/lib/env.mjs +14 -0
  133. package/scripts/lib/gemini-profiles.mjs +119 -0
  134. package/scripts/lib/handoff.mjs +187 -0
  135. package/scripts/lib/hook-utils.mjs +16 -0
  136. package/scripts/lib/hub.mjs +48 -0
  137. package/scripts/lib/keyword-rules.mjs +195 -0
  138. package/scripts/lib/logger.mjs +105 -0
  139. package/scripts/lib/mcp-filter.mjs +883 -0
  140. package/scripts/lib/mcp-gateway-servers.mjs +70 -0
  141. package/scripts/lib/mcp-guard-engine.mjs +2120 -0
  142. package/scripts/lib/mcp-health.mjs +773 -0
  143. package/scripts/lib/mcp-manifest.mjs +89 -0
  144. package/scripts/lib/mcp-server-catalog.mjs +355 -0
  145. package/scripts/lib/pid.mjs +63 -0
  146. package/scripts/lib/process-utils.mjs +26 -0
  147. package/scripts/lib/psmux-info.mjs +179 -0
  148. package/scripts/lib/quota.mjs +47 -0
  149. package/scripts/lib/remote-spawn-transfer.mjs +226 -0
  150. package/scripts/lib/skill-state.mjs +220 -0
  151. package/scripts/lib/skill-template.mjs +375 -0
  152. package/scripts/lib/team.mjs +75 -0
  153. package/scripts/lib/timeout.mjs +66 -0
  154. package/scripts/lib/tmp.mjs +26 -0
  155. package/scripts/lib/toml.mjs +113 -0
@@ -0,0 +1,176 @@
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,10 +1,9 @@
1
- const DASHBOARD_ANCHORS = new Set([
2
- "window",
3
- "tab",
4
- ]);
1
+ const DASHBOARD_ANCHORS = new Set(["window", "tab"]);
5
2
 
6
3
  export function normalizeDashboardAnchor(value) {
7
- const normalized = String(value ?? "").trim().toLowerCase();
4
+ const normalized = String(value ?? "")
5
+ .trim()
6
+ .toLowerCase();
8
7
  if (!normalized) return "window";
9
8
  return DASHBOARD_ANCHORS.has(normalized) ? normalized : "window";
10
9
  }
@@ -12,7 +12,9 @@ const DASHBOARD_LAYOUTS = new Set([
12
12
  ]);
13
13
 
14
14
  export function normalizeDashboardLayout(value, { allowAuto = true } = {}) {
15
- const normalized = String(value ?? "").trim().toLowerCase();
15
+ const normalized = String(value ?? "")
16
+ .trim()
17
+ .toLowerCase();
16
18
  if (!normalized) return "single";
17
19
  if (normalized === "auto" && !allowAuto) return "single";
18
20
  return DASHBOARD_LAYOUTS.has(normalized) ? normalized : "single";
@@ -1,28 +1,9 @@
1
- import { spawn } from "node:child_process";
2
-
3
- import { psmuxExec } from "./psmux.mjs";
4
- import {
5
- detectMultiplexer,
6
- focusWtPane,
7
- hasWindowsTerminal,
8
- resolveAttachCommand,
9
- tmuxExec,
10
- } from "./session.mjs";
11
-
12
- function sanitizeWindowTitle(value, fallback = "triflux") {
13
- const text = String(value || "").replace(/[\r\n]+/g, " ").trim();
14
- return text || fallback;
15
- }
1
+ import { createTerminalOpener } from "./terminal-opener.mjs";
16
2
 
17
3
  function sanitizeSessionName(value) {
18
4
  return String(value || "").replace(/[^a-zA-Z0-9_-]/g, "") || "tfx-session";
19
5
  }
20
6
 
21
- function sanitizeWorkingDirectory(value) {
22
- const text = String(value || "").replace(/[\r\n\x00-\x1f]/g, "").trim();
23
- return text || process.cwd();
24
- }
25
-
26
7
  export function parseWorkerNumber(value) {
27
8
  const text = String(value || "").trim();
28
9
  const workerMatch = text.match(/^worker-(\d+)$/i);
@@ -32,121 +13,42 @@ export function parseWorkerNumber(value) {
32
13
  return null;
33
14
  }
34
15
 
35
- export function decideDashboardOpenMode({ openAll = false, hasWtSession = !!process.env.WT_SESSION } = {}) {
36
- if (openAll) return hasWtSession ? "tab" : "window";
37
- return hasWtSession ? "split" : "window";
38
- }
39
-
40
- function spawnWindowsTerminal(spec, opts = {}) {
41
- if (!hasWindowsTerminal()) return false;
42
-
43
- const {
44
- mode = "window",
45
- title = "triflux",
46
- cwd = process.cwd(),
47
- split = { orientation: "H", size: 0.50 },
48
- } = opts;
49
-
50
- const safeTitle = sanitizeWindowTitle(title);
51
- const safeCwd = sanitizeWorkingDirectory(cwd);
52
- const orientation = split?.orientation === "V" ? "V" : "H";
53
- const size = Number.isFinite(split?.size) ? Math.min(0.8, Math.max(0.2, split.size)) : 0.50;
54
- const baseArgs = ["--profile", "triflux", "--title", safeTitle, "-d", safeCwd, "--", spec.command, ...spec.args];
55
- const args = mode === "split"
56
- ? ["-w", "0", "sp", `-${orientation}`, "-s", String(size), ...baseArgs]
57
- : mode === "tab"
58
- ? ["-w", "0", "nt", ...baseArgs]
59
- : ["-w", "new", ...baseArgs];
60
-
61
- const child = spawn("wt.exe", args, {
62
- detached: true,
63
- stdio: "ignore",
64
- windowsHide: false,
65
- });
66
- child.unref();
67
- return true;
16
+ function ignoreAsyncFailure(value) {
17
+ if (value && typeof value.then === "function") void value.catch(() => {});
68
18
  }
69
19
 
70
- export function focusManagedPane(target, opts = {}) {
71
- const { teammateMode = "", layout = "1xN" } = opts;
72
- const paneRef = String(target || "");
73
-
74
- if (teammateMode === "wt" || paneRef.startsWith("wt:")) {
75
- const paneIndex = parseWorkerNumber(paneRef);
76
- return paneIndex != null && focusWtPane(paneIndex, { layout });
77
- }
20
+ export async function openHeadlessDashboardTarget(sessionName, opts = {}) {
21
+ const { openAll = false, cwd = process.cwd(), title } = opts;
22
+ const safeSession = sanitizeSessionName(sessionName);
23
+ const workerNumber =
24
+ opts.workerNumber ??
25
+ (opts.worker == null ? null : parseWorkerNumber(opts.worker));
78
26
 
79
- if (!paneRef) return false;
27
+ let opener;
80
28
  try {
81
- if (detectMultiplexer() === "psmux") psmuxExec(["select-pane", "-t", paneRef]);
82
- else tmuxExec(`select-pane -t ${paneRef}`);
83
- return true;
29
+ const deps = opts._deps ?? {};
30
+ const openerFactory = deps.createTerminalOpener ?? createTerminalOpener;
31
+ opener = openerFactory(deps);
84
32
  } catch {
85
- return false;
33
+ return !openAll && workerNumber != null;
86
34
  }
87
- }
88
-
89
- export function openHeadlessDashboardTarget(sessionName, opts = {}) {
90
- const {
91
- worker = null,
92
- openAll = false,
93
- cwd = process.cwd(),
94
- title,
95
- } = opts;
96
-
97
- const safeSession = sanitizeSessionName(sessionName);
98
- const workerNumber = worker == null ? null : parseWorkerNumber(worker);
99
35
 
100
36
  // 선택 워커 → pane focus만 (새 창 열지 않음)
101
37
  if (!openAll && workerNumber != null) {
102
38
  try {
103
- psmuxExec(["select-pane", "-t", `${safeSession}:0.${workerNumber}`]);
39
+ ignoreAsyncFailure(opener.focusPane(safeSession, workerNumber));
104
40
  } catch {}
105
41
  return true;
106
42
  }
107
43
 
108
- // 전체 열기 (Shift+Enter) → 새 WT 창으로 세션 attach
109
- return spawnWindowsTerminal(
110
- { command: "psmux", args: ["attach-session", "-t", safeSession] },
111
- {
112
- mode: decideDashboardOpenMode({ openAll }),
113
- title: title || `▲ ${safeSession}`,
114
- cwd,
115
- },
116
- );
117
- }
118
-
119
- export function openDashboardRuntimeTarget(runtime, opts = {}) {
120
- const {
121
- teammateMode = "",
122
- sessionName = "",
123
- targetPane = "",
124
- layout = "1xN",
125
- openAll = false,
126
- cwd = process.cwd(),
127
- title = "",
128
- } = { ...runtime, ...opts };
129
-
130
- if (teammateMode === "headless") {
131
- return openHeadlessDashboardTarget(sessionName, {
132
- worker: openAll ? null : targetPane,
133
- openAll,
134
- cwd,
135
- title,
136
- });
137
- }
138
-
139
- if ((teammateMode === "wt" || String(targetPane).startsWith("wt:")) && !openAll) {
140
- return focusManagedPane(targetPane, { teammateMode: "wt", layout });
141
- }
142
-
44
+ // 전체 열기 (Shift+Enter) → 새 창으로 세션 attach
143
45
  try {
144
- if (!openAll && targetPane) focusManagedPane(targetPane, { teammateMode, layout });
145
- return spawnWindowsTerminal(resolveAttachCommand(sessionName), {
146
- mode: decideDashboardOpenMode({ openAll }),
147
- title: title || `▲ ${sanitizeSessionName(sessionName)}`,
46
+ const opened = opener.openSession(safeSession, {
47
+ title: title || `▲ ${safeSession}`,
148
48
  cwd,
49
+ profile: opts.profile ?? "triflux",
149
50
  });
51
+ return await opened;
150
52
  } catch {
151
53
  return false;
152
54
  }
@@ -6,7 +6,7 @@
6
6
  // node hub/team/dashboard.mjs --session <세션이름> [--interval 2]
7
7
  // node hub/team/dashboard.mjs --team <팀이름> [--interval 2]
8
8
  import { get } from "node:http";
9
- import { AMBER, GREEN, RED, GRAY, DIM, BOLD, RESET } from "./shared.mjs";
9
+ import { AMBER, BOLD, DIM, GRAY, GREEN, RED, RESET } from "./shared.mjs";
10
10
 
11
11
  /**
12
12
  * HTTP GET JSON
@@ -19,11 +19,18 @@ function fetchJson(url) {
19
19
  let data = "";
20
20
  res.on("data", (chunk) => (data += chunk));
21
21
  res.on("end", () => {
22
- try { resolve(JSON.parse(data)); } catch { resolve(null); }
22
+ try {
23
+ resolve(JSON.parse(data));
24
+ } catch {
25
+ resolve(null);
26
+ }
23
27
  });
24
28
  });
25
29
  req.on("error", () => resolve(null));
26
- req.on("timeout", () => { req.destroy(); resolve(null); });
30
+ req.on("timeout", () => {
31
+ req.destroy();
32
+ resolve(null);
33
+ });
27
34
  });
28
35
  }
29
36
 
@@ -77,10 +84,14 @@ function formatUptime(ms) {
77
84
  */
78
85
  function statusIcon(status) {
79
86
  switch (status) {
80
- case "completed": return `${GREEN}✓${RESET}`;
81
- case "in_progress": return `${AMBER}●${RESET}`;
82
- case "failed": return `${RED}✗${RESET}`;
83
- default: return `${GRAY}○${RESET}`;
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}`;
84
95
  }
85
96
  }
86
97
 
@@ -118,7 +129,12 @@ function buildMemberList(hubTasks, supervisorMembers, teamState) {
118
129
  members.push({
119
130
  name: m.name,
120
131
  cli: m.cli || "",
121
- status: m.status === "running" ? "in_progress" : m.status === "exited" ? "completed" : m.status,
132
+ status:
133
+ m.status === "running"
134
+ ? "in_progress"
135
+ : m.status === "exited"
136
+ ? "completed"
137
+ : m.status,
122
138
  subject: "",
123
139
  preview: m.lastPreview || "",
124
140
  });
@@ -128,7 +144,9 @@ function buildMemberList(hubTasks, supervisorMembers, teamState) {
128
144
 
129
145
  // teamState 폴백 (하위 호환)
130
146
  const panes = teamState?.panes || {};
131
- for (const [, paneInfo] of Object.entries(panes).filter(([, v]) => v.role !== "dashboard" && v.role !== "lead")) {
147
+ for (const [, paneInfo] of Object.entries(panes).filter(
148
+ ([, v]) => v.role !== "dashboard" && v.role !== "lead",
149
+ )) {
132
150
  members.push({
133
151
  name: paneInfo.agentId || paneInfo.name || "?",
134
152
  cli: paneInfo.cli || "",
@@ -162,27 +180,41 @@ export async function renderDashboard(sessionName, opts = {}) {
162
180
  // 데이터 수집 (병렬)
163
181
  const [hubStatus, taskListRes, supervisorRes] = await Promise.all([
164
182
  fetchJson(`${hubUrl}/status`),
165
- teamName ? fetchPost(`${hubUrl}/bridge/team/task-list`, { team_name: teamName }) : null,
183
+ teamName
184
+ ? fetchPost(`${hubUrl}/bridge/team/task-list`, { team_name: teamName })
185
+ : null,
166
186
  supervisorUrl ? fetchJson(`${supervisorUrl}/status`) : null,
167
187
  ]);
168
188
 
169
189
  const hubOnline = !!hubStatus;
170
- const hubState = hubOnline ? `${GREEN}● online${RESET}` : `${RED}● offline${RESET}`;
171
- const uptime = hubStatus?.hub?.uptime ? formatUptime(hubStatus.hub.uptime) : "-";
190
+ const hubState = hubOnline
191
+ ? `${GREEN}● online${RESET}`
192
+ : `${RED}● offline${RESET}`;
193
+ const uptime = hubStatus?.hub?.uptime
194
+ ? formatUptime(hubStatus.hub.uptime)
195
+ : "-";
172
196
  const queueSize = hubStatus?.hub?.queue_depth ?? 0;
173
197
 
174
198
  // Hub task 데이터
175
- const hubTasks = taskListRes?.ok ? (taskListRes.data?.tasks || []) : [];
176
- const completedCount = hubTasks.filter((t) => t.status === "completed").length;
199
+ const hubTasks = taskListRes?.ok ? taskListRes.data?.tasks || [] : [];
200
+ const completedCount = hubTasks.filter(
201
+ (t) => t.status === "completed",
202
+ ).length;
177
203
  const totalCount = hubTasks.length;
178
204
 
179
205
  // Supervisor 멤버 데이터
180
- const supervisorMembers = supervisorRes?.ok ? (supervisorRes.data?.members || []) : [];
206
+ const supervisorMembers = supervisorRes?.ok
207
+ ? supervisorRes.data?.members || []
208
+ : [];
181
209
 
182
210
  // 헤더
183
211
  const progress = totalCount > 0 ? ` ${completedCount}/${totalCount}` : "";
184
- console.log(`${AMBER}┌─ ${sessionName}${progress} ${GRAY}${"─".repeat(Math.max(0, W - sessionName.length - progress.length - 3))}${AMBER}┐${RESET}`);
185
- console.log(`${AMBER}│${RESET} Hub: ${hubState} Uptime: ${DIM}${uptime}${RESET} Queue: ${DIM}${queueSize}${RESET}`);
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
+ );
186
218
  console.log(`${AMBER}│${RESET}`);
187
219
 
188
220
  // 멤버/워커 렌더링
@@ -194,20 +226,29 @@ export async function renderDashboard(sessionName, opts = {}) {
194
226
  for (const m of members) {
195
227
  const icon = statusIcon(m.status);
196
228
  const label = `[${m.name}]`;
197
- const cliTag = m.cli ? m.cli.charAt(0).toUpperCase() + m.cli.slice(1) : "";
229
+ const cliTag = m.cli
230
+ ? m.cli.charAt(0).toUpperCase() + m.cli.slice(1)
231
+ : "";
198
232
 
199
233
  // 진행률 추정
200
- const pct = m.status === "completed" ? 100
201
- : m.status === "in_progress" ? 50
202
- : m.status === "failed" ? 100
203
- : 0;
234
+ const pct =
235
+ m.status === "completed"
236
+ ? 100
237
+ : m.status === "in_progress"
238
+ ? 50
239
+ : m.status === "failed"
240
+ ? 100
241
+ : 0;
204
242
 
205
- console.log(`${AMBER}│${RESET} ${BOLD}${label}${RESET} ${cliTag} ${icon} ${m.status || "pending"} ${progressBar(pct)}`);
243
+ console.log(
244
+ `${AMBER}│${RESET} ${BOLD}${label}${RESET} ${cliTag} ${icon} ${m.status || "pending"} ${progressBar(pct)}`,
245
+ );
206
246
 
207
247
  // 미리보기: supervisor lastPreview > task subject
208
248
  const preview = m.preview || m.subject || "";
209
249
  if (preview) {
210
- const truncated = preview.length > W - 8 ? preview.slice(0, W - 11) + "..." : preview;
250
+ const truncated =
251
+ preview.length > W - 8 ? preview.slice(0, W - 11) + "..." : preview;
211
252
  console.log(`${AMBER}│${RESET} ${DIM}> ${truncated}${RESET}`);
212
253
  }
213
254
  console.log(`${AMBER}│${RESET}`);
@@ -228,10 +269,12 @@ async function loadTeamState() {
228
269
  const sessionId = process.env.CLAUDE_SESSION_ID;
229
270
  if (sessionId) {
230
271
  const sessionPath = join(hubDir, `team-state-${sessionId}.json`);
231
- if (existsSync(sessionPath)) return JSON.parse(readFileSync(sessionPath, "utf8"));
272
+ if (existsSync(sessionPath))
273
+ return JSON.parse(readFileSync(sessionPath, "utf8"));
232
274
  }
233
275
  const legacyPath = join(hubDir, "team-state.json");
234
- if (existsSync(legacyPath)) return JSON.parse(readFileSync(legacyPath, "utf8"));
276
+ if (existsSync(legacyPath))
277
+ return JSON.parse(readFileSync(legacyPath, "utf8"));
235
278
  return {};
236
279
  } catch {
237
280
  return {};
@@ -244,11 +287,16 @@ if (process.argv[1]?.includes("dashboard.mjs")) {
244
287
  const teamIdx = process.argv.indexOf("--team");
245
288
  const sessionName = sessionIdx !== -1 ? process.argv[sessionIdx + 1] : null;
246
289
  const teamName = teamIdx !== -1 ? process.argv[teamIdx + 1] : null;
247
- const intervalSec = parseInt(process.argv[process.argv.indexOf("--interval") + 1] || "2", 10);
290
+ const intervalSec = parseInt(
291
+ process.argv[process.argv.indexOf("--interval") + 1] || "2",
292
+ 10,
293
+ );
248
294
 
249
295
  const displayName = sessionName || teamName;
250
296
  if (!displayName) {
251
- console.error("사용법: node dashboard.mjs --session <세션이름> [--team <팀이름>] [--interval 2]");
297
+ console.error(
298
+ "사용법: node dashboard.mjs --session <세션이름> [--team <팀이름>] [--interval 2]",
299
+ );
252
300
  process.exit(1);
253
301
  }
254
302
 
@@ -2,8 +2,8 @@
2
2
  // Conductor 세션 lifecycle 이벤트를 JSONL 파일에 기록한다.
3
3
  // 기존 hub/server.mjs의 batch-events.jsonl(MCP 이벤트)과 독립. 공존.
4
4
 
5
- import { createWriteStream, mkdirSync } from 'node:fs';
6
- import { dirname } from 'node:path';
5
+ import { createWriteStream, mkdirSync } from "node:fs";
6
+ import { dirname } from "node:path";
7
7
 
8
8
  /**
9
9
  * JSONL event log 팩토리.
@@ -16,7 +16,7 @@ export function createEventLog(filePath, opts = {}) {
16
16
  const { sessionId } = opts;
17
17
 
18
18
  mkdirSync(dirname(filePath), { recursive: true });
19
- const stream = createWriteStream(filePath, { flags: 'a' });
19
+ const stream = createWriteStream(filePath, { flags: "a" });
20
20
 
21
21
  let closed = false;
22
22
  let pending = 0;
@@ -35,7 +35,9 @@ export function createEventLog(filePath, opts = {}) {
35
35
  ...data,
36
36
  };
37
37
  pending += 1;
38
- stream.write(JSON.stringify(entry) + '\n', () => { pending -= 1; });
38
+ stream.write(JSON.stringify(entry) + "\n", () => {
39
+ pending -= 1;
40
+ });
39
41
  }
40
42
 
41
43
  /**
@@ -45,9 +47,9 @@ export function createEventLog(filePath, opts = {}) {
45
47
  function flush() {
46
48
  if (closed) return Promise.resolve();
47
49
  return new Promise((resolve, reject) => {
48
- stream.once('error', reject);
49
- stream.write('', () => {
50
- stream.removeListener('error', reject);
50
+ stream.once("error", reject);
51
+ stream.write("", () => {
52
+ stream.removeListener("error", reject);
51
53
  resolve();
52
54
  });
53
55
  });
@@ -69,8 +71,14 @@ export function createEventLog(filePath, opts = {}) {
69
71
  append,
70
72
  flush,
71
73
  close,
72
- get filePath() { return filePath; },
73
- get pending() { return pending; },
74
- get closed() { return closed; },
74
+ get filePath() {
75
+ return filePath;
76
+ },
77
+ get pending() {
78
+ return pending;
79
+ },
80
+ get closed() {
81
+ return closed;
82
+ },
75
83
  });
76
84
  }