triflux 10.50.2 → 10.52.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 (263) hide show
  1. package/CLAUDE.md +57 -41
  2. package/README.ko.md +41 -45
  3. package/README.md +41 -45
  4. package/adapters/codex/skills/tfx-live/SKILL.md +25 -11
  5. package/bin/tfx-live.mjs +341 -104
  6. package/bin/triflux.mjs +147 -2624
  7. package/config/mcp-registry.json +3 -12
  8. package/cto/brief.mjs +0 -20
  9. package/cto/collect.mjs +1 -200
  10. package/cto/current.schema.json +2 -23
  11. package/cto/hygiene.mjs +2 -61
  12. package/cto/lake-root.mjs +0 -56
  13. package/cto/status.mjs +5 -187
  14. package/hooks/agy-session-hook.mjs +11 -56
  15. package/hooks/codex-session-hook.mjs +11 -101
  16. package/hub/bridge.mjs +65 -1250
  17. package/hub/cli-adapter-base.mjs +12 -90
  18. package/hub/codex-adapter.mjs +0 -1
  19. package/hub/codex-preflight.mjs +5 -28
  20. package/hub/lib/process-utils.mjs +1 -156
  21. package/hub/lib/spawn-trace.mjs +3 -2
  22. package/hub/lib/timeout-defaults.mjs +0 -1
  23. package/hub/team/agent-map.json +0 -1
  24. package/hub/team/agents-row-attach.py +120 -0
  25. package/hub/team/agents-row.mjs +200 -0
  26. package/hub/team/ansi.mjs +1 -35
  27. package/hub/team/backend.mjs +9 -145
  28. package/hub/team/claude-daemon-control.mjs +1 -2
  29. package/hub/team/claude-session-projection.mjs +5 -73
  30. package/hub/team/cli/commands/attach.mjs +1 -17
  31. package/hub/team/cli/commands/control.mjs +12 -38
  32. package/hub/team/cli/commands/debug.mjs +2 -39
  33. package/hub/team/cli/commands/focus.mjs +5 -31
  34. package/hub/team/cli/commands/interrupt.mjs +6 -25
  35. package/hub/team/cli/commands/kill.mjs +1 -30
  36. package/hub/team/cli/commands/list.mjs +0 -16
  37. package/hub/team/cli/commands/send.mjs +6 -26
  38. package/hub/team/cli/commands/start/index.mjs +42 -133
  39. package/hub/team/cli/commands/start/parse-args.mjs +15 -40
  40. package/hub/team/cli/commands/start/start-headless.mjs +138 -105
  41. package/hub/team/cli/commands/start/start-mux.mjs +2 -7
  42. package/hub/team/cli/commands/status.mjs +2 -57
  43. package/hub/team/cli/commands/stop.mjs +14 -19
  44. package/hub/team/cli/help.mjs +4 -5
  45. package/hub/team/cli/manifest.mjs +5 -1
  46. package/hub/team/cli/render.mjs +0 -12
  47. package/hub/team/cli/services/runtime-mode.mjs +27 -98
  48. package/hub/team/cli/services/state-store.mjs +5 -20
  49. package/hub/team/codex-queue.mjs +385 -0
  50. package/hub/team/handoff.mjs +3 -34
  51. package/hub/team/headless.mjs +108 -1296
  52. package/hub/team/orchestrator.mjs +4 -30
  53. package/hub/team/pane.mjs +6 -28
  54. package/hub/team/psmux.mjs +3 -34
  55. package/hub/team/remote-session.mjs +1 -2
  56. package/hub/team/session-context.mjs +1 -1
  57. package/hub/team/session.mjs +7 -91
  58. package/hub/team/tui-core.mjs +1 -1
  59. package/hub/team/tui-viewer.mjs +11 -58
  60. package/hub/team/tui.mjs +8 -306
  61. package/hub/team/uds-orchestrator.mjs +2 -580
  62. package/hub/team/wt-manager.mjs +2 -2
  63. package/hub/workers/factory.mjs +6 -84
  64. package/hub/workers/interface.mjs +1 -7
  65. package/hub/workers/lib/jsonrpc-core.mjs +11 -0
  66. package/hub/workers/lib/jsonrpc-ws-uds.mjs +5 -8
  67. package/hud/cli-policy.mjs +0 -22
  68. package/hud/constants.mjs +13 -26
  69. package/hud/hud-qos-status.mjs +42 -54
  70. package/hud/providers/gemini.mjs +125 -20
  71. package/hud/renderers.mjs +101 -122
  72. package/hud/utils.mjs +10 -6
  73. package/package.json +7 -12
  74. package/scripts/__tests__/mcp-guard-engine-http-headers.test.mjs +6 -6
  75. package/scripts/__tests__/mcp-guard-engine-policy-sync.test.mjs +1 -4
  76. package/scripts/__tests__/mcp-guard-engine-proactive-sync.test.mjs +16 -21
  77. package/scripts/__tests__/mcp-guard-engine-stdio-sync.test.mjs +1 -1
  78. package/scripts/__tests__/mcp-guard-engine-sync-http-headers.test.mjs +2 -2
  79. package/scripts/__tests__/mcp-guard-engine.test.mjs +56 -63
  80. package/scripts/__tests__/spawn-trace.test.mjs +16 -0
  81. package/scripts/check-codex-config-stable.mjs +4 -37
  82. package/scripts/claude-login-detect.mjs +0 -1
  83. package/scripts/claudemd-sync.mjs +1 -28
  84. package/scripts/lib/agent-route-policy.mjs +143 -62
  85. package/scripts/lib/cli-claude.mjs +0 -14
  86. package/scripts/lib/env-probe.mjs +2 -131
  87. package/scripts/lib/gemini-profiles.mjs +2 -69
  88. package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
  89. package/scripts/lib/machine-profile.mjs +3 -17
  90. package/scripts/lib/mcp-filter.mjs +109 -53
  91. package/scripts/lib/mcp-guard-engine.mjs +40 -98
  92. package/scripts/lib/mcp-health.mjs +2 -2
  93. package/scripts/lib/mcp-server-catalog.mjs +2 -2
  94. package/scripts/lib/psmux-info.mjs +11 -59
  95. package/scripts/lib/session-presence.mjs +2 -100
  96. package/scripts/lib/skill-template.mjs +0 -245
  97. package/scripts/pack.mjs +8 -83
  98. package/scripts/preflight-cache.mjs +5 -18
  99. package/scripts/release/check-packages-mirror.mjs +2 -8
  100. package/scripts/release/version-manifest.json +9 -1
  101. package/scripts/setup.mjs +100 -417
  102. package/scripts/test-lock.mjs +0 -1
  103. package/scripts/tfx-route-post.mjs +5 -69
  104. package/scripts/tfx-route-worker.mjs +4 -79
  105. package/scripts/tfx-route.sh +169 -1495
  106. package/scripts/wt-cli.mjs +1 -2
  107. package/skills/tfx-auto/SKILL.md +123 -741
  108. package/skills/tfx-auto/references/consensus-schema.md +161 -0
  109. package/skills/tfx-auto/references/tmux-observation.md +97 -0
  110. package/skills/tfx-doctor/SKILL.md +5 -6
  111. package/skills/tfx-lead/SKILL.md +195 -0
  112. package/skills/tfx-live/SKILL.md +25 -11
  113. package/skills/tfx-remote/SKILL.md +24 -87
  114. package/skills/tfx-research/SKILL.md +13 -20
  115. package/skills/tfx-review/SKILL.md +33 -35
  116. package/skills/tfx-setup/SKILL.md +1 -1
  117. package/skills/tfx-ship/SKILL.md +6 -27
  118. package/skills/tfx-wt/SKILL.md +34 -68
  119. package/bin/tfx-doctor-tui.mjs +0 -7
  120. package/bin/tfx-setup-tui.mjs +0 -7
  121. package/config/routing-policy.json +0 -124
  122. package/docs/assets/consensus-flow.svg +0 -125
  123. package/docs/assets/deep-vs-light.svg +0 -193
  124. package/docs/assets/demo-dark.gif +0 -0
  125. package/docs/assets/demo-light.gif +0 -0
  126. package/docs/assets/demo-psmux.gif +0 -0
  127. package/hub/adaptive-diagnostic.mjs +0 -350
  128. package/hub/adaptive-inject.mjs +0 -224
  129. package/hub/adaptive-memory.mjs +0 -277
  130. package/hub/adaptive.mjs +0 -172
  131. package/hub/assign-callbacks.mjs +0 -170
  132. package/hub/delegator/contracts.mjs +0 -38
  133. package/hub/delegator/index.mjs +0 -14
  134. package/hub/delegator/schema/delegator-tools.schema.json +0 -250
  135. package/hub/delegator/service.mjs +0 -336
  136. package/hub/delegator/tool-definitions.mjs +0 -35
  137. package/hub/dynamic-routing-engine.mjs +0 -511
  138. package/hub/hitl.mjs +0 -202
  139. package/hub/hub-lifecycle.mjs +0 -248
  140. package/hub/lib/known-errors.json +0 -72
  141. package/hub/lib/memory-store.mjs +0 -1001
  142. package/hub/lib/phase-manager.mjs +0 -493
  143. package/hub/lib/risk-tier.mjs +0 -53
  144. package/hub/lib/ssh-command.mjs +0 -231
  145. package/hub/lib/staged-file-detect.mjs +0 -43
  146. package/hub/lib/state-snapshot.mjs +0 -293
  147. package/hub/lib/tfx-route-args.mjs +0 -353
  148. package/hub/lib/trace-recorder.mjs +0 -153
  149. package/hub/lib/uuidv7.mjs +0 -45
  150. package/hub/memory-doctor.mjs +0 -640
  151. package/hub/middleware/quota-middleware.mjs +0 -70
  152. package/hub/middleware/request-logger.mjs +0 -191
  153. package/hub/paths.mjs +0 -24
  154. package/hub/pipe.mjs +0 -825
  155. package/hub/pipeline/index.mjs +0 -163
  156. package/hub/pipeline/state.mjs +0 -191
  157. package/hub/pipeline/transitions.mjs +0 -131
  158. package/hub/promote-penalties.mjs +0 -138
  159. package/hub/public/dashboard.html +0 -416
  160. package/hub/reflexion.mjs +0 -290
  161. package/hub/role-contract.mjs +0 -314
  162. package/hub/router.mjs +0 -2026
  163. package/hub/routing-snapshot.mjs +0 -261
  164. package/hub/schema.sql +0 -209
  165. package/hub/server.mjs +0 -3048
  166. package/hub/session-fingerprint.mjs +0 -418
  167. package/hub/state.mjs +0 -300
  168. package/hub/store-adapter.mjs +0 -182
  169. package/hub/store.mjs +0 -1736
  170. package/hub/team/build-worker-prompt.mjs +0 -92
  171. package/hub/team/check-mcp-hub.mjs +0 -49
  172. package/hub/team/claude-native-bridge.mjs +0 -987
  173. package/hub/team/cli/commands/start/start-in-process.mjs +0 -50
  174. package/hub/team/cli/commands/start/start-wt.mjs +0 -86
  175. package/hub/team/cli/services/hub-client.mjs +0 -264
  176. package/hub/team/cli/services/native-control.mjs +0 -145
  177. package/hub/team/codex-review.mjs +0 -520
  178. package/hub/team/conductor-mesh-bridge.mjs +0 -122
  179. package/hub/team/conductor-registry.mjs +0 -121
  180. package/hub/team/conductor.mjs +0 -1529
  181. package/hub/team/consensus-meta.mjs +0 -176
  182. package/hub/team/cto-auto-collect.mjs +0 -115
  183. package/hub/team/cto-pull.mjs +0 -31
  184. package/hub/team/daemon-pty-tmux-bridge.mjs +0 -146
  185. package/hub/team/dashboard.mjs +0 -322
  186. package/hub/team/event-log.mjs +0 -84
  187. package/hub/team/execution-mode.mjs +0 -324
  188. package/hub/team/extract-completion-payload.mjs +0 -110
  189. package/hub/team/headless-bridge-session.mjs +0 -17
  190. package/hub/team/health-probe.mjs +0 -435
  191. package/hub/team/interactive-native-launcher.mjs +0 -119
  192. package/hub/team/interactive-native-worker.mjs +0 -80
  193. package/hub/team/interactive-tui-transport.mjs +0 -479
  194. package/hub/team/launcher-template.mjs +0 -144
  195. package/hub/team/lead-control.mjs +0 -191
  196. package/hub/team/mcp-selector.mjs +0 -145
  197. package/hub/team/native-supervisor.mjs +0 -460
  198. package/hub/team/native.mjs +0 -727
  199. package/hub/team/recovery-store.mjs +0 -149
  200. package/hub/team/remote-probe.mjs +0 -292
  201. package/hub/team/remote-watcher.mjs +0 -506
  202. package/hub/team/runtime-strategy.mjs +0 -200
  203. package/hub/team/schemas/worker-completion.json +0 -39
  204. package/hub/team/sentinel-capture.mjs +0 -127
  205. package/hub/team/session-sync.mjs +0 -316
  206. package/hub/team/swarm-cli.mjs +0 -390
  207. package/hub/team/swarm-hypervisor.mjs +0 -2206
  208. package/hub/team/swarm-intent.mjs +0 -216
  209. package/hub/team/swarm-locks.mjs +0 -335
  210. package/hub/team/swarm-planner.mjs +0 -371
  211. package/hub/team/swarm-preflight.mjs +0 -366
  212. package/hub/team/swarm-reconciler.mjs +0 -242
  213. package/hub/team/synapse-cli.mjs +0 -458
  214. package/hub/team/synapse-http.mjs +0 -311
  215. package/hub/team/synapse-registry.mjs +0 -618
  216. package/hub/team/tui-remote-adapter.mjs +0 -377
  217. package/hub/team/tui-synapse.mjs +0 -223
  218. package/hub/team/worker-completion-validator.mjs +0 -79
  219. package/hub/team/worker-sandbox.mjs +0 -125
  220. package/hub/team/worktree-lifecycle.mjs +0 -639
  221. package/hub/team/wt-templates.json +0 -43
  222. package/hub/team-bridge.mjs +0 -27
  223. package/hub/tools.mjs +0 -900
  224. package/hub/workers/antigravity-route-worker.mjs +0 -179
  225. package/hub/workers/codex-app-server-worker.mjs +0 -1067
  226. package/hub/workers/delegator-mcp.mjs +0 -1098
  227. package/hub/workers/lib/jsonrpc-stdio.mjs +0 -204
  228. package/mesh/index.mjs +0 -69
  229. package/mesh/mesh-budget.mjs +0 -137
  230. package/mesh/mesh-heartbeat.mjs +0 -100
  231. package/mesh/mesh-protocol.mjs +0 -96
  232. package/mesh/mesh-queue.mjs +0 -163
  233. package/mesh/mesh-registry.mjs +0 -78
  234. package/mesh/mesh-router.mjs +0 -77
  235. package/scripts/__tests__/skill-surface.test.mjs +0 -93
  236. package/scripts/cache-doctor.mjs +0 -156
  237. package/scripts/cache-warmup.mjs +0 -630
  238. package/scripts/completions/tfx.bash +0 -74
  239. package/scripts/completions/tfx.fish +0 -57
  240. package/scripts/completions/tfx.zsh +0 -98
  241. package/scripts/demo.mjs +0 -197
  242. package/scripts/doctor-dynamic-routing.mjs +0 -87
  243. package/scripts/hub-ensure.mjs +0 -842
  244. package/scripts/hub-watchdog.mjs +0 -90
  245. package/scripts/lib/claudemd-scanner.mjs +0 -223
  246. package/scripts/lib/context.mjs +0 -67
  247. package/scripts/lib/dynamic-route-cli.mjs +0 -107
  248. package/scripts/lib/handoff.mjs +0 -187
  249. package/scripts/lib/logger.mjs +0 -122
  250. package/scripts/lib/stealth-fetch.mjs +0 -176
  251. package/scripts/notion-read.mjs +0 -686
  252. package/scripts/snapshot-codex-state.mjs +0 -37
  253. package/scripts/snapshot-gemini-state.mjs +0 -37
  254. package/scripts/sync-codex-auth.mjs +0 -71
  255. package/scripts/sync-hub-mcp-settings.mjs +0 -697
  256. package/skills/_templates/base.md +0 -4
  257. package/skills/_templates/deep.md +0 -6
  258. package/skills/shared/telemetry-segment.md +0 -6
  259. package/tui/core.mjs +0 -266
  260. package/tui/doctor.mjs +0 -375
  261. package/tui/monitor-data.mjs +0 -152
  262. package/tui/monitor.mjs +0 -318
  263. package/tui/setup.mjs +0 -506
@@ -1,458 +0,0 @@
1
- // hub/team/synapse-cli.mjs — CLI surface for Synapse v1.
2
- // Reads persisted registry state + git log for the "status" and "why" commands.
3
- // Hub integration comes later; for v1 we go straight to the JSON persist files
4
- // so the CLI works even when Hub is offline.
5
-
6
- import { execFile } from "node:child_process";
7
- import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
8
- import { homedir } from "node:os";
9
- import { join, resolve } from "node:path";
10
-
11
- import { parseIntentTrailer } from "./swarm-intent.mjs";
12
-
13
- const DEFAULT_REGISTRY_CANDIDATES = [
14
- ".triflux/synapse-registry.json",
15
- ".triflux/synapse/registry.json",
16
- join(homedir(), ".claude", "cache", "tfx-hub", "synapse-registry.json"),
17
- ];
18
- const DEFAULT_SWARM_LOGS_DIR = ".triflux/swarm-logs";
19
- const STALE_AFTER_MS = 30 * 60 * 1000;
20
- const STATUS_PRIORITY = {
21
- active: 3,
22
- running: 3,
23
- completed: 2,
24
- failed: 2,
25
- stale: 1,
26
- };
27
-
28
- function gitExec(args, cwd) {
29
- return new Promise((res, rej) => {
30
- execFile(
31
- "git",
32
- args,
33
- { cwd, windowsHide: true, timeout: 15_000 },
34
- (err, stdout) => {
35
- if (err) rej(err);
36
- else res(stdout);
37
- },
38
- );
39
- });
40
- }
41
-
42
- function locateRegistryPath(explicit) {
43
- if (explicit && existsSync(explicit)) return explicit;
44
- for (const candidate of DEFAULT_REGISTRY_CANDIDATES) {
45
- if (existsSync(candidate)) return candidate;
46
- }
47
- return null;
48
- }
49
-
50
- function loadRegistrySnapshot(path) {
51
- if (!path) return [];
52
- try {
53
- const raw = readFileSync(path, "utf8");
54
- const parsed = JSON.parse(raw);
55
- if (Array.isArray(parsed)) return parsed;
56
- if (parsed && typeof parsed === "object") {
57
- // Accept both array-form and object-form persist layouts.
58
- if (Array.isArray(parsed.sessions)) return parsed.sessions;
59
- return Object.values(parsed);
60
- }
61
- return [];
62
- } catch {
63
- return [];
64
- }
65
- }
66
-
67
- function parseJsonLines(path) {
68
- try {
69
- return readFileSync(path, "utf8")
70
- .split(/\r?\n/)
71
- .map((line) => line.trim())
72
- .filter(Boolean)
73
- .flatMap((line) => {
74
- try {
75
- return [JSON.parse(line)];
76
- } catch {
77
- return [];
78
- }
79
- });
80
- } catch {
81
- return [];
82
- }
83
- }
84
-
85
- function normalizeRunId(name) {
86
- return String(name || "").replace(/^run-/, "");
87
- }
88
-
89
- function eventTimeMs(event) {
90
- const ms = Date.parse(String(event?.ts || ""));
91
- return Number.isFinite(ms) ? ms : 0;
92
- }
93
-
94
- function deriveLogStatus(events, lastHeartbeatMs, nowMs) {
95
- const stateEvents = events.filter((e) => e.event === "swarm_state");
96
- const lastState = stateEvents[stateEvents.length - 1]?.to;
97
- if (lastState === "completed" || lastState === "failed") return lastState;
98
- if (events.some((e) => e.event === "integration_complete")) {
99
- const lastIntegration = [...events]
100
- .reverse()
101
- .find((e) => e.event === "integration_complete");
102
- if (
103
- Array.isArray(lastIntegration?.failed) &&
104
- lastIntegration.failed.length
105
- ) {
106
- return "failed";
107
- }
108
- return "completed";
109
- }
110
- if (nowMs - lastHeartbeatMs > STALE_AFTER_MS) return "stale";
111
- return "active";
112
- }
113
-
114
- function buildLogSession(runDir, nowMs = Date.now()) {
115
- const eventPath = join(runDir, "swarm-events.jsonl");
116
- if (!existsSync(eventPath)) return null;
117
- const events = parseJsonLines(eventPath);
118
- if (events.length === 0) return null;
119
-
120
- let statMs = 0;
121
- try {
122
- statMs = statSync(eventPath).mtimeMs;
123
- } catch {}
124
-
125
- const eventMs = events.map(eventTimeMs).filter((ms) => ms > 0);
126
- const lastEventMs = eventMs.length ? Math.max(...eventMs) : statMs;
127
- const launched = new Set();
128
- const completed = new Set();
129
- const failed = new Set();
130
- for (const event of events) {
131
- if (event.event === "shard_launched" && event.shard) {
132
- launched.add(event.shard);
133
- } else if (event.event === "shard_completed" && event.shard) {
134
- completed.add(event.shard);
135
- } else if (
136
- (event.event === "shard_failed" ||
137
- event.event === "shard_launch_failed") &&
138
- event.shard
139
- ) {
140
- failed.add(event.shard);
141
- }
142
- }
143
-
144
- const runId = normalizeRunId(runDir.split(/[\\/]/).pop());
145
- const totalShards = launched.size || completed.size + failed.size;
146
- const aliveWorkers = [...launched].filter(
147
- (name) => !completed.has(name) && !failed.has(name),
148
- ).length;
149
-
150
- return {
151
- sessionId: runId,
152
- runId,
153
- host: "-",
154
- branch: "-",
155
- dirtyFiles: [],
156
- status: deriveLogStatus(events, lastEventMs || nowMs, nowMs),
157
- taskSummary: "swarm log run",
158
- source: "logs",
159
- shards: totalShards ? `${completed.size}/${totalShards}` : "-",
160
- workers: totalShards ? `${aliveWorkers} alive` : "-",
161
- lastHeartbeat: lastEventMs || null,
162
- logDir: runDir,
163
- };
164
- }
165
-
166
- export function loadSwarmLogSessions(logsDir, opts = {}) {
167
- const nowMs = opts.nowMs ?? Date.now();
168
- const root = logsDir || DEFAULT_SWARM_LOGS_DIR;
169
- if (!existsSync(root)) return [];
170
- try {
171
- return readdirSync(root, { withFileTypes: true })
172
- .filter((entry) => entry.isDirectory() && entry.name.startsWith("run-"))
173
- .map((entry) => buildLogSession(join(root, entry.name), nowMs))
174
- .filter(Boolean);
175
- } catch {
176
- return [];
177
- }
178
- }
179
-
180
- function statusPriority(status) {
181
- return STATUS_PRIORITY[status] || 0;
182
- }
183
-
184
- function mergeSession(existing, next) {
185
- const status =
186
- statusPriority(next.status) > statusPriority(existing.status)
187
- ? next.status
188
- : existing.status;
189
- const source =
190
- existing.source && next.source && existing.source !== next.source
191
- ? "synapse + logs"
192
- : existing.source || next.source || "synapse";
193
- return {
194
- ...next,
195
- ...existing,
196
- status,
197
- source,
198
- shards: next.shards || existing.shards,
199
- workers: next.workers || existing.workers,
200
- lastHeartbeat: Math.max(
201
- Number(existing.lastHeartbeat || 0),
202
- Number(next.lastHeartbeat || 0),
203
- ),
204
- logDir: existing.logDir || next.logDir,
205
- };
206
- }
207
-
208
- export function mergeRegistryAndLogSessions(registrySessions, logSessions) {
209
- const merged = new Map();
210
- for (const session of registrySessions) {
211
- const key = String(session.runId || session.sessionId || "");
212
- if (!key) continue;
213
- merged.set(key, {
214
- ...session,
215
- source: session.source || "synapse",
216
- lastHeartbeat: session.lastHeartbeat || null,
217
- });
218
- }
219
- for (const session of logSessions) {
220
- const key = String(session.runId || session.sessionId || "");
221
- if (!key) continue;
222
- const existing = merged.get(key);
223
- merged.set(key, existing ? mergeSession(existing, session) : session);
224
- }
225
- return [...merged.values()].sort((left, right) => {
226
- const rightTs = Number(right.lastHeartbeat || 0);
227
- const leftTs = Number(left.lastHeartbeat || 0);
228
- return rightTs - leftTs;
229
- });
230
- }
231
-
232
- function formatAge(ts, nowMs = Date.now()) {
233
- if (!Number.isFinite(Number(ts)) || Number(ts) <= 0) return "-";
234
- const seconds = Math.max(0, Math.round((nowMs - Number(ts)) / 1000));
235
- if (seconds < 60) return `${seconds}s ago`;
236
- const minutes = Math.round(seconds / 60);
237
- if (minutes < 60) return `${minutes}m ago`;
238
- const hours = Math.round(minutes / 60);
239
- return `${hours}h ago`;
240
- }
241
-
242
- function hasSwarmLogFields(sessions) {
243
- return sessions.some(
244
- (s) =>
245
- s.source === "logs" ||
246
- s.source === "synapse + logs" ||
247
- s.runId ||
248
- s.shards ||
249
- s.workers,
250
- );
251
- }
252
-
253
- function formatSwarmStatus(sessions) {
254
- const nowMs = Date.now();
255
- const rows = [];
256
- rows.push(
257
- "RUN_ID STATUS SHARDS WORKERS LAST_HEARTBEAT SOURCE",
258
- );
259
- rows.push(
260
- "───────────────────── ───────── ─────── ─────── ────────────── ──────────────",
261
- );
262
- for (const s of sessions) {
263
- const id = String(s.runId || s.sessionId || "?")
264
- .padEnd(21)
265
- .slice(0, 21);
266
- const status = String(s.status || "active")
267
- .padEnd(9)
268
- .slice(0, 9);
269
- const shards = String(s.shards || "-")
270
- .padEnd(7)
271
- .slice(0, 7);
272
- const workers = String(s.workers || "-")
273
- .padEnd(7)
274
- .slice(0, 7);
275
- const heartbeat = formatAge(s.lastHeartbeat, nowMs).padEnd(14).slice(0, 14);
276
- const source = String(s.source || "synapse").slice(0, 20);
277
- rows.push(
278
- `${id} ${status} ${shards} ${workers} ${heartbeat} ${source}`,
279
- );
280
- }
281
- return rows.join("\n");
282
- }
283
-
284
- function formatStatus(sessions) {
285
- if (!sessions.length) {
286
- return "no active sessions (synapse-registry empty)";
287
- }
288
- if (hasSwarmLogFields(sessions)) return formatSwarmStatus(sessions);
289
- const rows = [];
290
- rows.push("SESSION HOST BRANCH DIRTY STATE TASK");
291
- rows.push(
292
- "───────────────────── ────── ──────── ────── ─────── ─────────────",
293
- );
294
- for (const s of sessions) {
295
- const id = (s.sessionId || "?").padEnd(21).slice(0, 21);
296
- const host = (s.host || "?").padEnd(6).slice(0, 6);
297
- const branch = (s.branch || "?").padEnd(8).slice(0, 8);
298
- const dirty = String((s.dirtyFiles || []).length)
299
- .padEnd(6)
300
- .slice(0, 6);
301
- const state = (s.status || "active").padEnd(7).slice(0, 7);
302
- const task = (s.taskSummary || "").slice(0, 40);
303
- rows.push(`${id} ${host} ${branch} ${dirty} ${state} ${task}`);
304
- }
305
- return rows.join("\n");
306
- }
307
-
308
- /**
309
- * `tfx synapse status` — list active swarm sessions from persisted registry.
310
- */
311
- export async function cmdSynapseStatus(args = [], opts = {}) {
312
- const jsonOut = args.includes("--json") || opts.json;
313
- const explicit = extractFlag(args, "--registry");
314
- const explicitLogsDir = extractFlag(args, "--logs-dir");
315
- const path = locateRegistryPath(explicit);
316
- const registrySessions = loadRegistrySnapshot(path);
317
- const logsDir = explicitLogsDir || DEFAULT_SWARM_LOGS_DIR;
318
- const logSessions = loadSwarmLogSessions(logsDir);
319
- const sessions = mergeRegistryAndLogSessions(registrySessions, logSessions);
320
-
321
- if (jsonOut) {
322
- process.stdout.write(
323
- JSON.stringify(
324
- {
325
- registry: path,
326
- logsDir: existsSync(logsDir) ? logsDir : null,
327
- count: sessions.length,
328
- sessions,
329
- },
330
- null,
331
- 2,
332
- ) + "\n",
333
- );
334
- return;
335
- }
336
-
337
- if (!path && logSessions.length === 0) {
338
- process.stdout.write(
339
- "no registry or swarm logs found (looked for .triflux/synapse-registry.json and .triflux/swarm-logs)\n",
340
- );
341
- return;
342
- }
343
- if (path) process.stdout.write(`registry: ${path}\n`);
344
- if (logSessions.length > 0) process.stdout.write(`swarm logs: ${logsDir}\n`);
345
- process.stdout.write(`${formatStatus(sessions)}\n`);
346
- }
347
-
348
- function extractFlag(args, name) {
349
- const idx = args.indexOf(name);
350
- if (idx < 0) return null;
351
- return args[idx + 1] || null;
352
- }
353
-
354
- async function resolveCommitForPath(path, cwd) {
355
- try {
356
- const out = await gitExec(["log", "-1", "--format=%H", "--", path], cwd);
357
- return out.trim();
358
- } catch {
359
- return "";
360
- }
361
- }
362
-
363
- async function readCommitMessage(sha, cwd) {
364
- try {
365
- return await gitExec(["log", "-1", "--format=%B", sha], cwd);
366
- } catch {
367
- return "";
368
- }
369
- }
370
-
371
- async function readCommitMeta(sha, cwd) {
372
- try {
373
- const out = await gitExec(
374
- ["log", "-1", "--format=%h%x1f%an%x1f%ad%x1f%s", "--date=iso", sha],
375
- cwd,
376
- );
377
- const [short, author, date, subject] = out.trim().split("\x1f");
378
- return { short, author, date, subject };
379
- } catch {
380
- return null;
381
- }
382
- }
383
-
384
- /**
385
- * `tfx why <path>` — show the X-Intent of the last commit touching <path>.
386
- */
387
- export async function cmdSynapseWhy(args = [], opts = {}) {
388
- const jsonOut = args.includes("--json") || opts.json;
389
- const cwd = process.cwd();
390
- const positional = args.filter((a) => !a.startsWith("--"));
391
- const target = positional[0];
392
-
393
- if (!target) {
394
- const err = { error: "path required", usage: "tfx why <path>" };
395
- if (jsonOut) process.stdout.write(JSON.stringify(err) + "\n");
396
- else process.stderr.write("tfx why <path> — path 인자 필요\n");
397
- process.exitCode = 1;
398
- return;
399
- }
400
-
401
- const resolved = resolve(cwd, target);
402
- if (!existsSync(resolved)) {
403
- const err = { error: "path not found", path: target };
404
- if (jsonOut) process.stdout.write(JSON.stringify(err) + "\n");
405
- else process.stderr.write(`tfx why: 파일 없음: ${target}\n`);
406
- process.exitCode = 1;
407
- return;
408
- }
409
-
410
- const sha = await resolveCommitForPath(target, cwd);
411
- if (!sha) {
412
- const result = { path: target, commit: null, intent: null };
413
- if (jsonOut) process.stdout.write(JSON.stringify(result) + "\n");
414
- else process.stdout.write(`no commit touches ${target}\n`);
415
- return;
416
- }
417
-
418
- const [message, meta] = await Promise.all([
419
- readCommitMessage(sha, cwd),
420
- readCommitMeta(sha, cwd),
421
- ]);
422
- const intent = parseIntentTrailer(message);
423
-
424
- if (jsonOut) {
425
- process.stdout.write(
426
- JSON.stringify(
427
- { path: target, commit: { sha, ...meta }, intent },
428
- null,
429
- 2,
430
- ) + "\n",
431
- );
432
- return;
433
- }
434
-
435
- const header = meta
436
- ? `${meta.short} ${meta.author} ${meta.date} ${meta.subject}`
437
- : sha;
438
- process.stdout.write(`${target}\n`);
439
- process.stdout.write(` last commit: ${header}\n`);
440
- if (intent) {
441
- process.stdout.write(` intent.scope: ${intent.scope || "-"}\n`);
442
- process.stdout.write(` intent.action: ${intent.action || "-"}\n`);
443
- process.stdout.write(` intent.reason: ${intent.reason || "-"}\n`);
444
- if (Array.isArray(intent.touches) && intent.touches.length) {
445
- process.stdout.write(
446
- ` intent.touches: ${intent.touches.join(", ")}\n`,
447
- );
448
- }
449
- if (intent.invariant) {
450
- process.stdout.write(` intent.invariant: ${intent.invariant}\n`);
451
- }
452
- if (intent.conflictsWith) {
453
- process.stdout.write(` intent.conflicts: ${intent.conflictsWith}\n`);
454
- }
455
- } else {
456
- process.stdout.write(" intent: (no X-Intent trailer)\n");
457
- }
458
- }