@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,39 +0,0 @@
1
- {
2
- "$schema": "http://json-schema.org/draft-07/schema#",
3
- "title": "WorkerCompletion",
4
- "description": "Self-reported worker completion payload emitted by a swarm shard worker.",
5
- "type": "object",
6
- "required": ["status"],
7
- "properties": {
8
- "status": {
9
- "type": "string",
10
- "enum": ["ok", "skipped", "failed"]
11
- },
12
- "shard": { "type": "string" },
13
- "reason": { "type": "string" },
14
- "commits_made": {
15
- "type": "array",
16
- "items": {
17
- "type": "object",
18
- "required": ["sha"],
19
- "properties": {
20
- "sha": { "type": "string", "minLength": 7 },
21
- "message": { "type": "string" }
22
- },
23
- "additionalProperties": true
24
- }
25
- }
26
- },
27
- "additionalProperties": true,
28
- "allOf": [
29
- {
30
- "if": { "properties": { "status": { "const": "ok" } } },
31
- "then": {
32
- "required": ["commits_made"],
33
- "properties": {
34
- "commits_made": { "minItems": 1 }
35
- }
36
- }
37
- }
38
- ]
39
- }
@@ -1,127 +0,0 @@
1
- // hub/team/sentinel-capture.mjs — stdout sentinel-framed payload capture (#125)
2
- //
3
- // Workers emit completion payloads delimited by `<<<TFX_COMPLETION_BEGIN>>>`
4
- // and `<<<TFX_COMPLETION_END>>>` (see swarm-hypervisor.mjs buildWorkerPrompt
5
- // appendix). conductor.mjs feeds every stdout chunk through `push()`. After the
6
- // child exits, `snapshot()` returns the captured slice (BEGIN..END inclusive)
7
- // for `extractCompletionPayload`.
8
- //
9
- // Why this lives outside the 16 KiB stdoutTail buffer:
10
- // payloads with large `commits_made` arrays can exceed 16 KiB and a sliding
11
- // tail would head-truncate the BEGIN marker, leading to silent fallback to
12
- // brace-scan and an incorrect inner-object extraction (#115 Codex Finding 2).
13
- //
14
- // Pure module — no I/O, no timers — so it can be unit-tested directly.
15
-
16
- export const SENTINEL_BEGIN = "<<<TFX_COMPLETION_BEGIN>>>";
17
- export const SENTINEL_END = "<<<TFX_COMPLETION_END>>>";
18
-
19
- const DEFAULT_MAX_BYTES = 1 << 20; // 1 MiB safety cap on captured region
20
-
21
- // Per the worker prompt appendix, both markers must appear alone on their own
22
- // line. Substring matches inside log lines / debug echoes must NOT trigger
23
- // capture or terminate it. We treat `\n`, `\r`, and string boundaries (start
24
- // or end of buffer) as line edges so Windows `\r\n` works without special-casing.
25
- function isLineEdgeChar(ch) {
26
- return ch === "\n" || ch === "\r";
27
- }
28
-
29
- function findStandaloneMarker(text, marker, fromIdx = 0) {
30
- let from = fromIdx;
31
- while (from <= text.length - marker.length) {
32
- const idx = text.indexOf(marker, from);
33
- if (idx === -1) return -1;
34
- const before = idx === 0 || isLineEdgeChar(text[idx - 1]);
35
- const afterIdx = idx + marker.length;
36
- const after = afterIdx === text.length || isLineEdgeChar(text[afterIdx]);
37
- if (before && after) return idx;
38
- from = idx + 1;
39
- }
40
- return -1;
41
- }
42
-
43
- /**
44
- * Streaming sentinel capture state machine.
45
- *
46
- * @param {object} [opts]
47
- * @param {number} [opts.maxBytes=1048576] safety cap on the captured region.
48
- * If the capture grows past this and head-truncation removes the BEGIN
49
- * marker, `isOverflow()` becomes true so the conductor can deterministically
50
- * reject instead of falling back to brace-scan on a runaway payload
51
- * (Codex R1 MEDIUM finding).
52
- * @returns {{push: (text: string) => void, snapshot: () => string,
53
- * isActive: () => boolean, isComplete: () => boolean,
54
- * isOverflow: () => boolean}}
55
- */
56
- export function createSentinelCapture({ maxBytes = DEFAULT_MAX_BYTES } = {}) {
57
- let capturing = false;
58
- let capture = "";
59
- let overlap = "";
60
- let done = false;
61
- let overflowed = false;
62
-
63
- // Boundary overlap covers the marker length plus one extra byte so we can
64
- // verify the line-edge character that precedes BEGIN across chunk seams.
65
- const overlapWindow = SENTINEL_BEGIN.length;
66
-
67
- function maybeFlagOverflow() {
68
- if (findStandaloneMarker(capture, SENTINEL_BEGIN) === -1) {
69
- overflowed = true;
70
- done = true;
71
- return true;
72
- }
73
- return false;
74
- }
75
-
76
- function push(text) {
77
- if (done || typeof text !== "string" || text.length === 0) return;
78
-
79
- if (!capturing) {
80
- const search = overlap + text;
81
- const idx = findStandaloneMarker(search, SENTINEL_BEGIN);
82
- if (idx !== -1) {
83
- capturing = true;
84
- capture = search.slice(idx);
85
- overlap = "";
86
- if (capture.length > maxBytes) {
87
- capture = capture.slice(-maxBytes);
88
- if (maybeFlagOverflow()) return;
89
- }
90
- } else {
91
- overlap = search.slice(-overlapWindow);
92
- return;
93
- }
94
- } else {
95
- capture += text;
96
- if (capture.length > maxBytes) {
97
- capture = capture.slice(-maxBytes);
98
- if (maybeFlagOverflow()) return;
99
- }
100
- }
101
-
102
- // Stop capturing once the standalone-line END marker is seen. Trim to the
103
- // first BEGIN..END pair so a worker that re-emitted (retry, debug log)
104
- // doesn't bloat the snapshot.
105
- const endIdx = findStandaloneMarker(capture, SENTINEL_END);
106
- if (endIdx !== -1) {
107
- done = true;
108
- capture = capture.slice(0, endIdx + SENTINEL_END.length);
109
- }
110
- }
111
-
112
- return {
113
- push,
114
- snapshot() {
115
- return capture;
116
- },
117
- isActive() {
118
- return capturing && !done;
119
- },
120
- isComplete() {
121
- return done && !overflowed;
122
- },
123
- isOverflow() {
124
- return overflowed;
125
- },
126
- };
127
- }
@@ -1,390 +0,0 @@
1
- // hub/team/swarm-cli.mjs — `tfx swarm` CLI handlers (#93)
2
- // PRD를 planSwarm으로 분석하고 필요 시 createSwarmHypervisor로 실행한다.
3
-
4
- import { existsSync } from "node:fs";
5
- import { join, resolve } from "node:path";
6
- import { createSwarmHypervisor } from "./swarm-hypervisor.mjs";
7
- import { planSwarm } from "./swarm-planner.mjs";
8
- import {
9
- formatPreflightReport,
10
- runSwarmPreflight,
11
- } from "./swarm-preflight.mjs";
12
-
13
- const RESET = "\u001b[0m";
14
- const BOLD = "\u001b[1m";
15
- const GREEN = "\u001b[92m";
16
- const RED = "\u001b[91m";
17
- const YELLOW = "\u001b[93m";
18
- const GRAY = "\u001b[90m";
19
-
20
- /**
21
- * #116-C: non-TTY background 환경에서 `tfx swarm` 실행 시 codex worker spawn 이
22
- * 무한 hang 가능성이 있다 (stdin TTY 대기 또는 hub MCP lease race).
23
- *
24
- * Policy (v10.15+):
25
- * - stdout 또는 stdin 중 하나라도 TTY → silent OK (기존 동작).
26
- * - 양측 non-TTY → warning + 진행 (기본, 사용자 친화 + CI/background 호환).
27
- * - `TFX_BLOCK_NON_TTY_SWARM=1` opt-out → fail-fast + 복구 경로 안내 (안전 망).
28
- * - `TFX_ALLOW_NON_TTY_SWARM=1` 은 silent OK (호환 유지, warning suppress).
29
- *
30
- * 기존 fail-fast 정책은 첫 사용자에게 묻기 효과 (실제 user terminal 은 TTY 인데
31
- * Claude Code run_in_background 같은 spawn 환경에서 child stdio 는 non-TTY).
32
- * 다른 사용자도 동일 마찰 → 기본 동작을 "proceed with warning" 으로 변경.
33
- *
34
- * @param {{
35
- * stdoutIsTTY?: boolean,
36
- * stdinIsTTY?: boolean,
37
- * env?: Record<string,string|undefined>,
38
- * }} [deps]
39
- * @returns {{ ok: boolean, optIn: boolean, warnings: string[], reason?: string }}
40
- */
41
- export function assertTtyForSwarm(deps = {}) {
42
- const stdoutIsTTY =
43
- typeof deps.stdoutIsTTY === "boolean"
44
- ? deps.stdoutIsTTY
45
- : Boolean(process.stdout.isTTY);
46
- const stdinIsTTY =
47
- typeof deps.stdinIsTTY === "boolean"
48
- ? deps.stdinIsTTY
49
- : Boolean(process.stdin.isTTY);
50
- const env = deps.env || process.env;
51
- const warnings = [];
52
-
53
- if (stdoutIsTTY || stdinIsTTY) {
54
- return { ok: true, optIn: false, warnings };
55
- }
56
-
57
- // 양측 non-TTY 부터 적용되는 정책.
58
- if (env.TFX_BLOCK_NON_TTY_SWARM === "1") {
59
- const reason =
60
- "tfx swarm 이 차단됨 — non-TTY 환경 + TFX_BLOCK_NON_TTY_SWARM=1 (#116-C).\n" +
61
- " 복구 경로:\n" +
62
- " 1) 터미널에서 직접 실행: tfx swarm <prd>\n" +
63
- " 2) TFX_BLOCK_NON_TTY_SWARM=0 (또는 unset) 으로 차단 해제 후 재시도";
64
- return { ok: false, optIn: false, warnings, reason };
65
- }
66
-
67
- if (env.TFX_ALLOW_NON_TTY_SWARM === "1") {
68
- // 명시 opt-in — silent OK (기존 호환, warning 미출력).
69
- return { ok: true, optIn: true, warnings };
70
- }
71
-
72
- warnings.push(
73
- "non-TTY 환경 감지 — codex worker spawn hang 가능성 존재 (#116-C). 차단하려면 TFX_BLOCK_NON_TTY_SWARM=1.",
74
- );
75
- return { ok: true, optIn: true, warnings };
76
- }
77
-
78
- export function parseFlags(args) {
79
- const flags = {
80
- dryRun: false,
81
- planOnly: false,
82
- json: false,
83
- filter: null,
84
- maxRestarts: 2,
85
- logsDir: null,
86
- baseBranch: "main",
87
- nativeBridge: true,
88
- };
89
- const positional = [];
90
- let nativeBridgeUiEnabled = false;
91
- let nativeBridgeUiDisabled = false;
92
- for (let i = 0; i < args.length; i++) {
93
- const a = args[i];
94
- if (a === "--dry-run" || a === "--plan-only") flags.dryRun = true;
95
- else if (a === "--json") flags.json = true;
96
- else if (a === "--filter") flags.filter = args[++i];
97
- else if (a === "--max-restarts") flags.maxRestarts = Number(args[++i]) || 2;
98
- else if (a === "--logs-dir") flags.logsDir = args[++i];
99
- else if (a === "--base") {
100
- const v = args[++i];
101
- if (!v || /\s/.test(v)) {
102
- throw new Error(
103
- "--base requires a non-empty branch name without whitespace",
104
- );
105
- }
106
- flags.baseBranch = v;
107
- } else if (a === "--native-bridge-ui") {
108
- nativeBridgeUiEnabled = true;
109
- flags.nativeBridge = true;
110
- } else if (a === "--no-native-bridge-ui") {
111
- nativeBridgeUiDisabled = true;
112
- flags.nativeBridge = false;
113
- } else if (a.startsWith("--")) {
114
- // ignore unknown flags silently
115
- } else {
116
- positional.push(a);
117
- }
118
- }
119
- if (nativeBridgeUiEnabled && nativeBridgeUiDisabled) {
120
- throw new Error(
121
- "conflicting native bridge flags: --native-bridge-ui and --no-native-bridge-ui",
122
- );
123
- }
124
- return { flags, positional };
125
- }
126
-
127
- function planToJson(plan) {
128
- return {
129
- totalShards: plan.shards.length,
130
- shards: plan.shards.map((s) => ({
131
- name: s.name,
132
- agent: s.agent,
133
- host: s.host || null,
134
- files: s.files,
135
- depends: s.depends,
136
- critical: s.critical,
137
- })),
138
- leaseMap: Object.fromEntries(plan.leaseMap),
139
- mcpManifest: Object.fromEntries(plan.mcpManifest),
140
- mergeOrder: plan.mergeOrder,
141
- criticalShards: plan.criticalShards,
142
- conflicts: plan.conflicts,
143
- remoteSuggestion: plan.remoteSuggestion,
144
- };
145
- }
146
-
147
- function printPlan(plan) {
148
- console.log(`\n ${BOLD}Swarm plan${RESET}: ${plan.shards.length} shards`);
149
- for (const s of plan.shards) {
150
- const hostStr = s.host ? `@${s.host}` : "";
151
- const critStr = s.critical ? ` ${YELLOW}[critical]${RESET}` : "";
152
- const depsStr = s.depends.length > 0 ? ` ← ${s.depends.join(", ")}` : "";
153
- console.log(
154
- ` - ${BOLD}${s.name}${RESET} [${s.agent}${hostStr}] files=${s.files.length}${critStr}${depsStr}`,
155
- );
156
- }
157
- console.log(`\n ${GRAY}Merge order:${RESET} ${plan.mergeOrder.join(" → ")}`);
158
- if (plan.criticalShards.length > 0) {
159
- console.log(
160
- ` ${GRAY}Critical (redundant):${RESET} ${plan.criticalShards.join(", ")}`,
161
- );
162
- }
163
- if (plan.conflicts.length > 0) {
164
- console.log(`\n ${YELLOW}⚠ File conflicts:${RESET}`);
165
- for (const c of plan.conflicts) {
166
- console.log(` - ${c.file} → [${c.shards.join(", ")}]`);
167
- }
168
- }
169
- }
170
-
171
- function formatCell(value, width) {
172
- const text = value == null || value === "" ? "-" : String(value);
173
- return text.padEnd(width).slice(0, width);
174
- }
175
-
176
- export function formatIntegrationReport(report = []) {
177
- const rows = Array.isArray(report) ? report : [];
178
- const lines = [
179
- "=== INTEGRATION REPORT ===",
180
- "SHARD STRATEGY COMMITS FINAL_SHA NOTES",
181
- ];
182
-
183
- let integrated = 0;
184
- let recoveryPatches = 0;
185
- for (const row of rows) {
186
- if (row.strategy !== "failed") integrated += 1;
187
- if (row.recoveryPatch) recoveryPatches += 1;
188
- const finalSha = row.finalSha ? String(row.finalSha).slice(0, 7) : "-";
189
- const notes = row.recoveryPatch
190
- ? `${row.notes || "failed"}; recovery: ${row.recoveryPatch}`
191
- : row.notes || "-";
192
- lines.push(
193
- `${formatCell(row.shard, 9)} ${formatCell(row.strategy, 14)} ${formatCell(row.commits, 8)} ${formatCell(finalSha, 10)} ${notes}`,
194
- );
195
- }
196
-
197
- const total = rows.length;
198
- lines.push("");
199
- lines.push(
200
- `TOTAL: ${integrated}/${total} integrated, ${recoveryPatches} recovery patch${recoveryPatches === 1 ? "" : "es"} saved.`,
201
- );
202
- return lines.join("\n");
203
- }
204
-
205
- /**
206
- * tfx swarm <prd-path> — PRD 실행
207
- */
208
- export async function cmdSwarmRun(args, { json = false, deps = {} } = {}) {
209
- const { flags, positional } = parseFlags(args);
210
- flags.json = flags.json || json;
211
-
212
- const prdPath = positional[0];
213
- if (!prdPath) {
214
- throw new Error(
215
- "PRD path required. Usage: tfx swarm <prd-path> [--dry-run] [--filter <shard>] [--json] [--base <branch>]",
216
- );
217
- }
218
- const absPrd = resolve(prdPath);
219
- if (!existsSync(absPrd)) {
220
- throw new Error(`PRD file not found: ${absPrd}`);
221
- }
222
-
223
- if (flags.dryRun) {
224
- const plan = (deps.planSwarm || planSwarm)(absPrd, {
225
- repoRoot: process.cwd(),
226
- });
227
- if (flags.json) {
228
- process.stdout.write(JSON.stringify(planToJson(plan), null, 2) + "\n");
229
- } else {
230
- printPlan(plan);
231
- }
232
- return;
233
- }
234
-
235
- const preflight = (deps.runSwarmPreflight || runSwarmPreflight)(absPrd, {
236
- repoRoot: process.cwd(),
237
- deps,
238
- });
239
- if (!preflight.ok) {
240
- if (flags.json) {
241
- process.stdout.write(JSON.stringify(preflight, null, 2) + "\n");
242
- } else {
243
- process.stderr.write(formatPreflightReport(preflight));
244
- }
245
- process.exitCode = 1;
246
- return;
247
- }
248
-
249
- const plan = (deps.planSwarm || planSwarm)(absPrd, {
250
- repoRoot: process.cwd(),
251
- });
252
-
253
- const ttyGate = assertTtyForSwarm();
254
- for (const w of ttyGate.warnings) {
255
- console.error(` ${YELLOW}⚠${RESET} ${w}`);
256
- }
257
- if (!ttyGate.ok) {
258
- throw new Error(ttyGate.reason);
259
- }
260
-
261
- const logsDir =
262
- flags.logsDir ||
263
- join(process.cwd(), ".triflux", "swarm-logs", `run-${Date.now()}`);
264
- const hyper = (deps.createSwarmHypervisor || createSwarmHypervisor)({
265
- workdir: process.cwd(),
266
- logsDir,
267
- maxRestarts: flags.maxRestarts,
268
- baseBranch: flags.baseBranch,
269
- nativeBridge: flags.nativeBridge,
270
- });
271
-
272
- hyper.on("shardLaunched", ({ shardName, sessionId, remote }) => {
273
- const tag = remote ? ` ${GRAY}(remote)${RESET}` : "";
274
- console.log(
275
- ` ${GREEN}▸${RESET} launched: ${shardName}${tag} [${sessionId}]`,
276
- );
277
- });
278
- hyper.on("shardCompleted", ({ shardName, sessionId, isRedundant }) => {
279
- const tag = isRedundant ? ` ${GRAY}(redundant)${RESET}` : "";
280
- console.log(` ${GREEN}✓${RESET} ${shardName}${tag} [${sessionId}]`);
281
- });
282
- hyper.on("shardFailed", ({ shardName, failureMode, reason }) => {
283
- const reasonStr = reason ? ` ${GRAY}(${reason})${RESET}` : "";
284
- const modeStr = failureMode ? ` ${GRAY}[${failureMode}]${RESET}` : "";
285
- console.log(` ${RED}✗${RESET} ${shardName}${modeStr}${reasonStr}`);
286
- });
287
- hyper.on("warning", ({ type, ...rest }) => {
288
- console.error(
289
- ` ${YELLOW}⚠${RESET} ${type}: ${JSON.stringify(rest).slice(0, 200)}`,
290
- );
291
- });
292
-
293
- console.log(
294
- `\n ${BOLD}Launching swarm:${RESET} ${plan.shards.length} shards (logs: ${logsDir})`,
295
- );
296
- const run = await hyper.launch(plan);
297
-
298
- try {
299
- await run.done;
300
- } catch (err) {
301
- console.error(` ${RED}integration failed:${RESET} ${err.message}`);
302
- }
303
-
304
- const status = hyper.getStatus();
305
-
306
- // #126: surface integration outcome in summary + exit code so silent
307
- // success failures (worker exit 0 + integration_failed) can't mask state.
308
- const ip = status.integrationPromise || {};
309
- const integratedCount = Array.isArray(ip.integrated)
310
- ? ip.integrated.length
311
- : 0;
312
- const integrationFailureCount = Array.isArray(ip.integrationFailures)
313
- ? ip.integrationFailures.length
314
- : 0;
315
- const finalState = status.state || "unknown";
316
- const isFailure =
317
- status.failedShards > 0 ||
318
- integrationFailureCount > 0 ||
319
- finalState === "failed";
320
- const stateColor = isFailure ? RED : GREEN;
321
-
322
- console.log(
323
- `\n ${BOLD}Summary:${RESET} state=${stateColor}${finalState}${RESET} completed=${status.completedShards}/${status.totalShards} failed=${status.failedShards} integrated=${integratedCount} integration_failures=${integrationFailureCount}`,
324
- );
325
-
326
- if (integrationFailureCount > 0) {
327
- console.log(
328
- ` ${RED}integration failures:${RESET} ${ip.integrationFailures.join(", ")}`,
329
- );
330
- }
331
-
332
- if (Array.isArray(ip.report) && ip.report.length > 0) {
333
- console.log(`\n${formatIntegrationReport(ip.report)}`);
334
- }
335
-
336
- if (flags.json) {
337
- process.stdout.write(JSON.stringify(status, null, 2) + "\n");
338
- }
339
-
340
- await hyper.shutdown("completed");
341
- if (isFailure) process.exitCode = 1;
342
- }
343
-
344
- /**
345
- * tfx swarm plan <prd-path> — dry-run (실행 없이 계획만 출력)
346
- */
347
- export async function cmdSwarmPlan(args, { json = false } = {}) {
348
- return cmdSwarmRun(["--dry-run", ...args], { json });
349
- }
350
-
351
- /**
352
- * tfx swarm preflight <prd-path> — PRD 실행 전 no-go 리포트 출력
353
- */
354
- export async function cmdSwarmPreflight(
355
- args,
356
- { json = false, deps = {} } = {},
357
- ) {
358
- const { flags, positional } = parseFlags(args);
359
- flags.json = flags.json || json;
360
-
361
- const prdPath = positional[0];
362
- if (!prdPath) {
363
- throw new Error(
364
- "PRD path required. Usage: tfx swarm preflight <prd-path> [--json]",
365
- );
366
- }
367
- const absPrd = resolve(prdPath);
368
- if (!existsSync(absPrd)) {
369
- throw new Error(`PRD file not found: ${absPrd}`);
370
- }
371
-
372
- const report = (deps.runSwarmPreflight || runSwarmPreflight)(absPrd, {
373
- repoRoot: process.cwd(),
374
- deps,
375
- });
376
- if (flags.json) {
377
- process.stdout.write(JSON.stringify(report, null, 2) + "\n");
378
- } else {
379
- process.stdout.write(formatPreflightReport(report));
380
- }
381
- if (!report.ok) process.exitCode = 1;
382
- }
383
-
384
- /**
385
- * tfx swarm list — synapse status로 위임 (swarm 세션은 synapse registry에 등록)
386
- */
387
- export async function cmdSwarmList(args, { json = false } = {}) {
388
- const { cmdSynapseStatus } = await import("./synapse-cli.mjs");
389
- return cmdSynapseStatus(args, { json });
390
- }