@triflux/remote 10.51.0 → 10.53.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 (62) hide show
  1. package/cto/brief.mjs +0 -8
  2. package/cto/collect.mjs +1 -127
  3. package/cto/current.schema.json +2 -19
  4. package/cto/hygiene.mjs +2 -61
  5. package/cto/lake-root.mjs +0 -56
  6. package/cto/status.mjs +5 -139
  7. package/hub/index.mjs +0 -9
  8. package/hub/team/agents-row-attach.py +120 -0
  9. package/hub/team/agents-row.mjs +200 -0
  10. package/hub/team/claude-daemon-control.mjs +1 -2
  11. package/hub/team/claude-session-projection.mjs +5 -73
  12. package/hub/team/claude-transcript.mjs +19 -0
  13. package/hub/team/cli/commands/debug.mjs +1 -2
  14. package/hub/team/cli/commands/start/index.mjs +2 -1
  15. package/hub/team/cli/commands/start/start-headless.mjs +2 -0
  16. package/hub/team/cli/commands/start/start-mux.mjs +2 -4
  17. package/hub/team/cli/services/state-store.mjs +5 -20
  18. package/hub/team/headless.mjs +132 -551
  19. package/hub/team/psmux.mjs +3 -33
  20. package/hub/team/results-index.mjs +92 -0
  21. package/hub/team/session-context.mjs +1 -1
  22. package/hub/team/tui-viewer.mjs +9 -56
  23. package/hub/team/tui.mjs +0 -37
  24. package/hub/team/uds-orchestrator.mjs +20 -599
  25. package/hub/workers/factory.mjs +6 -51
  26. package/hub/workers/interface.mjs +1 -7
  27. package/hub/workers/lib/jsonrpc-core.mjs +11 -0
  28. package/hub/workers/lib/jsonrpc-ws-uds.mjs +5 -6
  29. package/package.json +3 -8
  30. package/scripts/lib/agent-route-policy.mjs +0 -67
  31. package/scripts/lib/cli-claude.mjs +0 -12
  32. package/scripts/lib/env-probe.mjs +0 -70
  33. package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
  34. package/scripts/lib/mcp-guard-engine.mjs +42 -101
  35. package/hub/hub-lifecycle.mjs +0 -248
  36. package/hub/pipe.mjs +0 -825
  37. package/hub/promote-penalties.mjs +0 -138
  38. package/hub/public/dashboard.html +0 -416
  39. package/hub/schema.sql +0 -209
  40. package/hub/server.mjs +0 -3048
  41. package/hub/store-adapter.mjs +0 -182
  42. package/hub/store.mjs +0 -1736
  43. package/hub/team/cli/services/hub-client.mjs +0 -178
  44. package/hub/team/conductor-registry.mjs +0 -121
  45. package/hub/team/cto-auto-collect.mjs +0 -115
  46. package/hub/team/lead-control.mjs +0 -191
  47. package/hub/team/session-sync.mjs +0 -316
  48. package/hub/team/swarm-intent.mjs +0 -18
  49. package/hub/team/synapse-cli.mjs +0 -458
  50. package/hub/team/synapse-http.mjs +0 -311
  51. package/hub/team/synapse-registry.mjs +0 -618
  52. package/hub/team/tui-synapse.mjs +0 -223
  53. package/hub/team/worker-sandbox.mjs +0 -125
  54. package/hub/tools.mjs +0 -900
  55. package/hub/workers/codex-app-server-worker.mjs +0 -1067
  56. package/hub/workers/delegator-mcp.mjs +0 -1098
  57. package/hub/workers/lib/jsonrpc-stdio.mjs +0 -204
  58. package/scripts/lib/claudemd-scanner.mjs +0 -26
  59. package/scripts/lib/context.mjs +0 -51
  60. package/scripts/lib/handoff.mjs +0 -187
  61. package/scripts/lib/logger.mjs +0 -122
  62. package/scripts/lib/stealth-fetch.mjs +0 -176
@@ -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
- }