@triflux/remote 10.0.0 → 10.1.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.
- package/hub/index.mjs +5 -5
- package/hub/public/dashboard.html +7 -7
- package/hub/server.mjs +94 -52
- package/hub/store-adapter.mjs +51 -20
- package/hub/store.mjs +8 -8
- package/hub/team/cli/commands/start/start-headless.mjs +5 -3
- package/hub/team/cli/services/hub-client.mjs +1 -1
- package/hub/team/conductor-mesh-bridge.mjs +121 -0
- package/hub/team/conductor.mjs +671 -671
- package/hub/team/dashboard-open.mjs +1 -1
- package/hub/team/headless.mjs +1156 -1151
- package/hub/team/notify.mjs +1 -1
- package/hub/team/orchestrator.mjs +2 -2
- package/hub/team/psmux.mjs +67 -5
- package/hub/team/remote-probe.mjs +1 -1
- package/hub/team/remote-session.mjs +5 -2
- package/hub/team/remote-watcher.mjs +1 -1
- package/hub/team/session.mjs +1 -1
- package/hub/team/swarm-hypervisor.mjs +612 -0
- package/hub/team/swarm-locks.mjs +204 -0
- package/hub/team/swarm-planner.mjs +260 -0
- package/hub/team/swarm-reconciler.mjs +137 -0
- package/hub/team/tui-remote-adapter.mjs +3 -3
- package/hub/team/tui-viewer.mjs +0 -1
- package/hub/team/tui.mjs +12 -15
- package/hub/team/worktree-lifecycle.mjs +229 -0
- package/hub/team/wt-manager.mjs +407 -0
- package/hub/team/wt-templates.json +43 -0
- package/hub/workers/codex-mcp.mjs +507 -510
- package/hub/workers/delegator-mcp.mjs +1 -2
- package/package.json +11 -3
package/hub/team/tui.mjs
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
import {
|
|
6
6
|
RESET,
|
|
7
7
|
FG,
|
|
8
|
-
BG,
|
|
9
8
|
MOCHA,
|
|
10
9
|
color,
|
|
11
10
|
dim,
|
|
@@ -18,7 +17,6 @@ import {
|
|
|
18
17
|
wcswidth,
|
|
19
18
|
progressBar,
|
|
20
19
|
statusBadge,
|
|
21
|
-
STATUS_ICON,
|
|
22
20
|
altScreenOn,
|
|
23
21
|
altScreenOff,
|
|
24
22
|
clearScreen,
|
|
@@ -27,7 +25,6 @@ import {
|
|
|
27
25
|
cursorShow,
|
|
28
26
|
moveTo,
|
|
29
27
|
clearLine,
|
|
30
|
-
clearToEnd,
|
|
31
28
|
} from "./ansi.mjs";
|
|
32
29
|
|
|
33
30
|
import { execFile as _execFile } from "node:child_process";
|
|
@@ -52,7 +49,7 @@ const SPINNER_FRAMES = [...SPINNER_FRAMES_RAW, ...[...SPINNER_FRAMES_RAW].revers
|
|
|
52
49
|
const SPINNER_CYCLE_MS = 2000;
|
|
53
50
|
const SPINNER_BASE_COLOR = { r: 203, g: 166, b: 247 }; // Catppuccin Mocha mauve
|
|
54
51
|
const SPINNER_SHIMMER = { r: 171, g: 43, b: 63 }; // Claude shimmer #ab2b3f
|
|
55
|
-
|
|
52
|
+
const spinnerStart = Date.now();
|
|
56
53
|
let spinnerTick = 0;
|
|
57
54
|
|
|
58
55
|
function lerpRgb(a, b, t) {
|
|
@@ -119,7 +116,7 @@ function activityWave(tick, count = 4) {
|
|
|
119
116
|
const GRID_GAP = 2;
|
|
120
117
|
const DEFAULT_DETAIL_LINES = 10;
|
|
121
118
|
// Tier1 상단 고정 행 수
|
|
122
|
-
const
|
|
119
|
+
const _TIER1_ROWS = 2;
|
|
123
120
|
|
|
124
121
|
const SUMMARY_KEYS = [
|
|
125
122
|
"status", "lead_action", "verdict", "files_changed",
|
|
@@ -128,14 +125,14 @@ const SUMMARY_KEYS = [
|
|
|
128
125
|
|
|
129
126
|
// ── 레이아웃 브레이크포인트 ──────────────────────────────────────────────
|
|
130
127
|
// 80-119: 28col rail, 120-159: 36col rail, 160+: 균등
|
|
131
|
-
function
|
|
128
|
+
function _resolveRailWidth(totalCols, columnCount) {
|
|
132
129
|
if (columnCount <= 1) return totalCols;
|
|
133
130
|
if (totalCols >= 160) return Math.floor((totalCols - GRID_GAP * (columnCount - 1)) / columnCount);
|
|
134
131
|
if (totalCols >= 120) return Math.min(36, Math.floor((totalCols - GRID_GAP * (columnCount - 1)) / columnCount));
|
|
135
132
|
return Math.min(28, Math.floor((totalCols - GRID_GAP * (columnCount - 1)) / columnCount));
|
|
136
133
|
}
|
|
137
134
|
|
|
138
|
-
function
|
|
135
|
+
function _autoColumnCount(totalCols, workerCount) {
|
|
139
136
|
if (workerCount <= 1) return 1;
|
|
140
137
|
if (totalCols >= 160) return Math.min(workerCount, 3);
|
|
141
138
|
if (totalCols >= 120) return Math.min(workerCount, 2);
|
|
@@ -154,7 +151,7 @@ function stripCodeBlocks(text) {
|
|
|
154
151
|
.replace(/```[\s\S]*?(?:```|$)/g, "\n")
|
|
155
152
|
.replace(/^\s*```.*$/gm, "")
|
|
156
153
|
// indented code blocks (4+ spaces or tab at line start)
|
|
157
|
-
.replace(/^(?:
|
|
154
|
+
.replace(/^(?: {4}|\t).+$/gm, "")
|
|
158
155
|
// shell prompts: PS C:\...>, >, $
|
|
159
156
|
.replace(/^(?:PS\s+\S[^\n]*?>|>\s+|\$\s+)[^\n]*/gm, "")
|
|
160
157
|
.trim();
|
|
@@ -275,7 +272,7 @@ function gradientBorderFn(topRgb, bottomRgb) {
|
|
|
275
272
|
}
|
|
276
273
|
|
|
277
274
|
// Effect 3: Flash-fade border — 상태 변경 시 백색 플래시 → 페이드아웃
|
|
278
|
-
function
|
|
275
|
+
function _flashFadeBorderColor(currentStatus, prevStatus, changedAt) {
|
|
279
276
|
const elapsed = Date.now() - (changedAt || 0);
|
|
280
277
|
if (elapsed >= FADE_DURATION_MS || !prevStatus) return null;
|
|
281
278
|
const statusRgb = statusToRgb(currentStatus);
|
|
@@ -319,11 +316,11 @@ function dedupeRole(role, name, cli) {
|
|
|
319
316
|
r = r.replace(new RegExp(esc(cli), "gi"), "").trim();
|
|
320
317
|
r = r.replace(new RegExp(esc(name), "gi"), "").trim();
|
|
321
318
|
// CLI indicator emojis 제거
|
|
322
|
-
r = r.replace(/[⚪⚫🔴🟠🟡🟢🔵🟣🟤⭕🔘]/
|
|
319
|
+
r = r.replace(/[⚪⚫🔴🟠🟡🟢🔵🟣🟤⭕🔘]/gu, "").trim();
|
|
323
320
|
// 빈 괄호 제거 + 중첩 괄호 정리
|
|
324
321
|
r = r.replace(/\(\s*\)/g, "").trim();
|
|
325
322
|
r = r.replace(/^\(([^()]+)\)$/, "$1").trim();
|
|
326
|
-
r = r.replace(/^\s*[
|
|
323
|
+
r = r.replace(/^\s*[•·-]\s*/, "").trim();
|
|
327
324
|
return r;
|
|
328
325
|
}
|
|
329
326
|
|
|
@@ -350,7 +347,7 @@ function wrapLine(text, width) {
|
|
|
350
347
|
return lines.length > 0 ? lines : [source.slice(0, limit)];
|
|
351
348
|
}
|
|
352
349
|
|
|
353
|
-
function
|
|
350
|
+
function _wrapText(text, width, maxLines = DEFAULT_DETAIL_LINES, rawMode = false) {
|
|
354
351
|
if (maxLines <= 0) return [];
|
|
355
352
|
const input = sanitizeTextBlock(text, rawMode);
|
|
356
353
|
if (!input) return [];
|
|
@@ -465,7 +462,7 @@ function buildWorkerRail(name, st, opts = {}) {
|
|
|
465
462
|
selected = false,
|
|
466
463
|
focused = false, // rail 포커스 여부
|
|
467
464
|
previousSelected = false,
|
|
468
|
-
|
|
465
|
+
_rawMode = false,
|
|
469
466
|
compact = false,
|
|
470
467
|
time = Date.now(),
|
|
471
468
|
} = opts;
|
|
@@ -713,7 +710,7 @@ function buildHelpOverlay(width, height) {
|
|
|
713
710
|
}
|
|
714
711
|
|
|
715
712
|
// ── joinColumns ───────────────────────────────────────────────────────────
|
|
716
|
-
function
|
|
713
|
+
function _joinColumns(blocks, gap = GRID_GAP) {
|
|
717
714
|
const maxHeight = Math.max(...blocks.map((b) => b.length));
|
|
718
715
|
return Array.from({ length: maxHeight }, (_, rowIdx) =>
|
|
719
716
|
blocks
|
|
@@ -1418,7 +1415,7 @@ export function renderConductorTier(snapshot, cols = 100) {
|
|
|
1418
1415
|
const dataLines = [];
|
|
1419
1416
|
if (!snapshot || snapshot.length === 0) {
|
|
1420
1417
|
const emptyMsg = color('(no sessions)', FG.muted);
|
|
1421
|
-
const
|
|
1418
|
+
const _emptyPad = clip(stripAnsi(emptyMsg) === '(no sessions)' ? emptyMsg : emptyMsg, inner);
|
|
1422
1419
|
dataLines.push(`${borderSeq}│${RESET} ${padRight(emptyMsg, inner - 2)} ${borderSeq}│${RESET}`);
|
|
1423
1420
|
} else {
|
|
1424
1421
|
for (const s of snapshot) {
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// hub/team/worktree-lifecycle.mjs — Git worktree lifecycle management
|
|
2
|
+
// Replaces shell commands in tfx-codex-swarm Step 5 + merge-worktree Phase 6.
|
|
3
|
+
// Convention: .codex-swarm/wt-{slug} paths, swarm/{runId}/{slug} branches.
|
|
4
|
+
// Remote support: host option → SSH-based git operations via remote-session.mjs.
|
|
5
|
+
|
|
6
|
+
import { execFile } from 'node:child_process';
|
|
7
|
+
import { resolve, normalize } from 'node:path';
|
|
8
|
+
import { mkdir, rm, access } from 'node:fs/promises';
|
|
9
|
+
import { remoteGit, validateHost } from './remote-session.mjs';
|
|
10
|
+
|
|
11
|
+
const SWARM_ROOT = '.codex-swarm';
|
|
12
|
+
const SLEEP_MS = 2000; // WT race-guard (MEMORY.md: wt-attach-spacing)
|
|
13
|
+
|
|
14
|
+
function git(args, cwd) {
|
|
15
|
+
return new Promise((res, rej) => {
|
|
16
|
+
execFile('git', args, { cwd, windowsHide: true, timeout: 30_000 }, (err, stdout, stderr) => {
|
|
17
|
+
if (err) {
|
|
18
|
+
const msg = `git ${args[0]} failed: ${stderr?.trim() || err.message}`;
|
|
19
|
+
rej(new Error(msg));
|
|
20
|
+
} else {
|
|
21
|
+
res(stdout.trim());
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function sleep(ms) {
|
|
28
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Normalize path for Windows compatibility. */
|
|
32
|
+
function normPath(p) {
|
|
33
|
+
return normalize(p).replace(/\\/g, '/');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create a worktree for a shard.
|
|
38
|
+
*
|
|
39
|
+
* @param {object} opts
|
|
40
|
+
* @param {string} opts.slug — shard identifier (e.g. "issue-42" or "auth-refactor")
|
|
41
|
+
* @param {string} opts.runId — swarm run ID
|
|
42
|
+
* @param {string} [opts.rootDir=process.cwd()] — repo root
|
|
43
|
+
* @param {string} [opts.baseBranch='main'] — base branch to branch from
|
|
44
|
+
* @param {string} [opts.host] — SSH host for remote worktree creation
|
|
45
|
+
* @param {object} [opts.remoteEnv] — remote environment from probeRemoteEnv()
|
|
46
|
+
* @returns {Promise<{ worktreePath: string, branchName: string, remote: boolean }>}
|
|
47
|
+
*/
|
|
48
|
+
export async function ensureWorktree({ slug, runId, rootDir = process.cwd(), baseBranch = 'main', host, remoteEnv }) {
|
|
49
|
+
const branchName = `swarm/${runId}/${slug}`;
|
|
50
|
+
|
|
51
|
+
// ── Remote path: SSH-based worktree creation ──
|
|
52
|
+
if (host && remoteEnv) {
|
|
53
|
+
const remoteRoot = rootDir.replace(/\\/g, '/');
|
|
54
|
+
const remoteWtDir = `${remoteRoot}/${SWARM_ROOT}/wt-${slug}`;
|
|
55
|
+
|
|
56
|
+
try { remoteGit(host, remoteEnv, ['worktree', 'prune'], remoteRoot); } catch { /* best-effort */ }
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
remoteGit(host, remoteEnv, ['worktree', 'add', remoteWtDir, '-b', branchName, baseBranch], remoteRoot);
|
|
60
|
+
} catch {
|
|
61
|
+
try {
|
|
62
|
+
remoteGit(host, remoteEnv, ['worktree', 'add', remoteWtDir, branchName], remoteRoot);
|
|
63
|
+
} catch { /* already exists — acceptable */ }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { worktreePath: remoteWtDir, branchName, remote: true };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Local path (existing logic) ──
|
|
70
|
+
const wtDir = resolve(rootDir, SWARM_ROOT, `wt-${slug}`);
|
|
71
|
+
|
|
72
|
+
await mkdir(resolve(rootDir, SWARM_ROOT), { recursive: true });
|
|
73
|
+
|
|
74
|
+
// Check if worktree already exists
|
|
75
|
+
try {
|
|
76
|
+
await access(wtDir);
|
|
77
|
+
await git(['rev-parse', '--is-inside-work-tree'], wtDir);
|
|
78
|
+
return { worktreePath: normPath(wtDir), branchName, remote: false };
|
|
79
|
+
} catch {
|
|
80
|
+
// Doesn't exist or invalid — create fresh
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
await git(['worktree', 'prune'], rootDir);
|
|
85
|
+
} catch { /* best-effort */ }
|
|
86
|
+
|
|
87
|
+
await sleep(SLEEP_MS);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
await git(['worktree', 'add', wtDir, '-b', branchName, baseBranch], rootDir);
|
|
91
|
+
} catch {
|
|
92
|
+
await git(['worktree', 'add', wtDir, branchName], rootDir);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { worktreePath: normPath(wtDir), branchName, remote: false };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Create a temporary integration branch for merge operations.
|
|
100
|
+
*
|
|
101
|
+
* @param {object} opts
|
|
102
|
+
* @param {string} opts.runId
|
|
103
|
+
* @param {string} opts.baseBranch
|
|
104
|
+
* @param {string} [opts.rootDir=process.cwd()]
|
|
105
|
+
* @returns {Promise<{ integrationBranch: string, baseCommit: string }>}
|
|
106
|
+
*/
|
|
107
|
+
export async function prepareIntegrationBranch({ runId, baseBranch, rootDir = process.cwd() }) {
|
|
108
|
+
const integrationBranch = `swarm/${runId}/merge`;
|
|
109
|
+
|
|
110
|
+
// Record base commit for rollback
|
|
111
|
+
const baseCommit = await git(['rev-parse', baseBranch], rootDir);
|
|
112
|
+
|
|
113
|
+
// Create integration branch from base
|
|
114
|
+
try {
|
|
115
|
+
await git(['branch', integrationBranch, baseBranch], rootDir);
|
|
116
|
+
} catch {
|
|
117
|
+
// Branch may already exist — reset to base
|
|
118
|
+
await git(['branch', '-f', integrationBranch, baseBranch], rootDir);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { integrationBranch, baseCommit };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Rebase a shard branch onto the integration branch.
|
|
126
|
+
* Uses rebase + fast-forward only (no merge commits).
|
|
127
|
+
*
|
|
128
|
+
* @param {object} opts
|
|
129
|
+
* @param {string} opts.shardBranch — the shard's branch name
|
|
130
|
+
* @param {string} opts.integrationBranch — target integration branch
|
|
131
|
+
* @param {string} [opts.rootDir=process.cwd()]
|
|
132
|
+
* @returns {Promise<{ ok: boolean, headCommit?: string, error?: string }>}
|
|
133
|
+
*/
|
|
134
|
+
export async function rebaseShardOntoIntegration({ shardBranch, integrationBranch, rootDir = process.cwd() }) {
|
|
135
|
+
// Backup integration HEAD for rollback
|
|
136
|
+
const backupCommit = await git(['rev-parse', integrationBranch], rootDir);
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
// Rebase shard onto integration
|
|
140
|
+
await git(['rebase', integrationBranch, shardBranch], rootDir);
|
|
141
|
+
|
|
142
|
+
// Fast-forward integration to include shard changes
|
|
143
|
+
await git(['checkout', integrationBranch], rootDir);
|
|
144
|
+
await git(['merge', '--ff-only', shardBranch], rootDir);
|
|
145
|
+
|
|
146
|
+
const headCommit = await git(['rev-parse', 'HEAD'], rootDir);
|
|
147
|
+
return { ok: true, headCommit };
|
|
148
|
+
} catch (err) {
|
|
149
|
+
// Abort rebase and restore integration branch
|
|
150
|
+
try { await git(['rebase', '--abort'], rootDir); } catch { /* already clean */ }
|
|
151
|
+
try { await git(['checkout', integrationBranch], rootDir); } catch { /* best-effort */ }
|
|
152
|
+
try { await git(['reset', '--hard', backupCommit], rootDir); } catch { /* best-effort */ }
|
|
153
|
+
|
|
154
|
+
return { ok: false, error: err.message };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Remove a worktree and its branch.
|
|
160
|
+
* Follows WT race-guard: sleep between operations.
|
|
161
|
+
*
|
|
162
|
+
* @param {object} opts
|
|
163
|
+
* @param {string} opts.worktreePath
|
|
164
|
+
* @param {string} [opts.branchName] — optional branch to delete
|
|
165
|
+
* @param {string} [opts.rootDir=process.cwd()]
|
|
166
|
+
* @param {boolean} [opts.force=false]
|
|
167
|
+
*/
|
|
168
|
+
export async function pruneWorktree({ worktreePath, branchName, rootDir = process.cwd(), force = false }) {
|
|
169
|
+
const forceFlag = force ? '--force' : '';
|
|
170
|
+
|
|
171
|
+
// Remove worktree (with retry for Windows file handle issues — E5)
|
|
172
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
173
|
+
try {
|
|
174
|
+
await git(['worktree', 'remove', worktreePath, ...(forceFlag ? [forceFlag] : [])], rootDir);
|
|
175
|
+
break;
|
|
176
|
+
} catch (_err) {
|
|
177
|
+
if (attempt === 2) {
|
|
178
|
+
// Last resort: rm the directory and prune
|
|
179
|
+
try { await rm(worktreePath, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
180
|
+
}
|
|
181
|
+
await sleep(SLEEP_MS);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Prune stale worktree references
|
|
186
|
+
await sleep(SLEEP_MS);
|
|
187
|
+
try { await git(['worktree', 'prune'], rootDir); } catch { /* best-effort */ }
|
|
188
|
+
|
|
189
|
+
// Delete branch if specified
|
|
190
|
+
if (branchName) {
|
|
191
|
+
try { await git(['branch', '-D', branchName], rootDir); } catch { /* may not exist */ }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Fetch a remote shard's branch to the local repo via SSH.
|
|
197
|
+
* Workaround for hosts that cannot push to GitHub (e.g. Ultra4).
|
|
198
|
+
*
|
|
199
|
+
* Flow: add temp remote → fetch branch → remove remote.
|
|
200
|
+
*
|
|
201
|
+
* @param {object} opts
|
|
202
|
+
* @param {string} opts.host — SSH host (e.g. "ultra4")
|
|
203
|
+
* @param {string} opts.sshUser — SSH user (e.g. "SSAFY")
|
|
204
|
+
* @param {string} opts.remoteRepoPath — absolute path on remote (e.g. "/c/Users/SSAFY/Desktop/Projects/cli/triflux")
|
|
205
|
+
* @param {string} opts.branchName — branch to fetch (e.g. "swarm/run123/auth")
|
|
206
|
+
* @param {string} [opts.rootDir=process.cwd()] — local repo root
|
|
207
|
+
* @returns {Promise<{ ok: boolean, localRef?: string, error?: string }>}
|
|
208
|
+
*/
|
|
209
|
+
export async function fetchRemoteShard({ host, sshUser, remoteRepoPath, branchName, rootDir = process.cwd() }) {
|
|
210
|
+
validateHost(host);
|
|
211
|
+
|
|
212
|
+
const remoteName = `_swarm-${host}-${Date.now()}`;
|
|
213
|
+
const sshUrl = `ssh://${sshUser}@${host}${remoteRepoPath}`;
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
await git(['remote', 'add', remoteName, sshUrl], rootDir);
|
|
217
|
+
|
|
218
|
+
await git(['fetch', remoteName, branchName, '--no-tags'], rootDir);
|
|
219
|
+
|
|
220
|
+
const localRef = `${remoteName}/${branchName}`;
|
|
221
|
+
const headCommit = await git(['rev-parse', `FETCH_HEAD`], rootDir);
|
|
222
|
+
|
|
223
|
+
return { ok: true, localRef, headCommit };
|
|
224
|
+
} catch (err) {
|
|
225
|
+
return { ok: false, error: err.message };
|
|
226
|
+
} finally {
|
|
227
|
+
try { await git(['remote', 'remove', remoteName], rootDir); } catch { /* cleanup */ }
|
|
228
|
+
}
|
|
229
|
+
}
|