@triflux/remote 10.50.2 → 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.
- package/cto/brief.mjs +0 -12
- package/cto/collect.mjs +0 -73
- package/cto/current.schema.json +0 -4
- package/cto/status.mjs +2 -50
- package/hub/index.mjs +0 -8
- package/hub/team/agent-map.json +0 -1
- package/hub/team/ansi.mjs +1 -35
- package/hub/team/backend.mjs +9 -145
- package/hub/team/cli/commands/attach.mjs +1 -17
- package/hub/team/cli/commands/control.mjs +12 -38
- package/hub/team/cli/commands/debug.mjs +1 -37
- package/hub/team/cli/commands/focus.mjs +5 -31
- package/hub/team/cli/commands/interrupt.mjs +6 -25
- package/hub/team/cli/commands/kill.mjs +1 -30
- package/hub/team/cli/commands/list.mjs +0 -16
- package/hub/team/cli/commands/send.mjs +6 -26
- package/hub/team/cli/commands/start/index.mjs +41 -133
- package/hub/team/cli/commands/start/parse-args.mjs +15 -40
- package/hub/team/cli/commands/start/start-headless.mjs +138 -105
- package/hub/team/cli/commands/start/start-mux.mjs +0 -3
- package/hub/team/cli/commands/status.mjs +2 -57
- package/hub/team/cli/commands/stop.mjs +14 -19
- package/hub/team/cli/help.mjs +4 -5
- package/hub/team/cli/manifest.mjs +5 -1
- package/hub/team/cli/render.mjs +0 -12
- package/hub/team/cli/services/hub-client.mjs +2 -88
- package/hub/team/cli/services/runtime-mode.mjs +27 -98
- package/hub/team/handoff.mjs +3 -34
- package/hub/team/headless.mjs +13 -754
- package/hub/team/orchestrator.mjs +4 -30
- package/hub/team/pane.mjs +6 -28
- package/hub/team/psmux.mjs +0 -1
- package/hub/team/remote-session.mjs +1 -2
- package/hub/team/session.mjs +7 -91
- package/hub/team/swarm-intent.mjs +0 -198
- package/hub/team/tui-core.mjs +1 -1
- package/hub/team/tui-viewer.mjs +2 -2
- package/hub/team/tui.mjs +8 -269
- package/hub/team/wt-manager.mjs +2 -2
- package/hub/workers/factory.mjs +4 -37
- package/hub/workers/lib/jsonrpc-ws-uds.mjs +0 -2
- package/package.json +1 -1
- package/scripts/lib/agent-route-policy.mjs +162 -14
- package/scripts/lib/claudemd-scanner.mjs +2 -199
- package/scripts/lib/cli-claude.mjs +0 -2
- package/scripts/lib/context.mjs +3 -19
- package/scripts/lib/env-probe.mjs +5 -64
- package/scripts/lib/gemini-profiles.mjs +2 -69
- package/scripts/lib/machine-profile.mjs +3 -17
- package/scripts/lib/mcp-filter.mjs +109 -53
- package/scripts/lib/mcp-health.mjs +2 -2
- package/scripts/lib/mcp-server-catalog.mjs +2 -2
- package/scripts/lib/psmux-info.mjs +11 -59
- package/scripts/lib/skill-template.mjs +0 -245
- package/hub/team/build-worker-prompt.mjs +0 -92
- package/hub/team/check-mcp-hub.mjs +0 -49
- package/hub/team/claude-native-bridge.mjs +0 -987
- package/hub/team/cli/commands/start/start-in-process.mjs +0 -50
- package/hub/team/cli/commands/start/start-wt.mjs +0 -86
- package/hub/team/cli/services/native-control.mjs +0 -145
- package/hub/team/codex-review.mjs +0 -520
- package/hub/team/conductor-mesh-bridge.mjs +0 -122
- package/hub/team/conductor.mjs +0 -1529
- package/hub/team/consensus-meta.mjs +0 -176
- package/hub/team/cto-pull.mjs +0 -31
- package/hub/team/daemon-pty-tmux-bridge.mjs +0 -146
- package/hub/team/dashboard.mjs +0 -322
- package/hub/team/event-log.mjs +0 -84
- package/hub/team/execution-mode.mjs +0 -324
- package/hub/team/extract-completion-payload.mjs +0 -110
- package/hub/team/headless-bridge-session.mjs +0 -17
- package/hub/team/health-probe.mjs +0 -435
- package/hub/team/interactive-native-launcher.mjs +0 -119
- package/hub/team/interactive-native-worker.mjs +0 -80
- package/hub/team/interactive-tui-transport.mjs +0 -479
- package/hub/team/launcher-template.mjs +0 -144
- package/hub/team/mcp-selector.mjs +0 -145
- package/hub/team/native-supervisor.mjs +0 -460
- package/hub/team/native.mjs +0 -727
- package/hub/team/recovery-store.mjs +0 -149
- package/hub/team/remote-probe.mjs +0 -292
- package/hub/team/remote-watcher.mjs +0 -506
- package/hub/team/runtime-strategy.mjs +0 -200
- package/hub/team/schemas/worker-completion.json +0 -39
- package/hub/team/sentinel-capture.mjs +0 -127
- package/hub/team/swarm-cli.mjs +0 -390
- package/hub/team/swarm-hypervisor.mjs +0 -2206
- package/hub/team/swarm-locks.mjs +0 -335
- package/hub/team/swarm-planner.mjs +0 -371
- package/hub/team/swarm-preflight.mjs +0 -366
- package/hub/team/swarm-reconciler.mjs +0 -242
- package/hub/team/tui-remote-adapter.mjs +0 -377
- package/hub/team/worker-completion-validator.mjs +0 -79
- package/hub/team/worktree-lifecycle.mjs +0 -639
- package/hub/team/wt-templates.json +0 -43
- package/hub/workers/antigravity-route-worker.mjs +0 -179
- package/scripts/lib/dynamic-route-cli.mjs +0 -107
|
@@ -1,639 +0,0 @@
|
|
|
1
|
-
// hub/team/worktree-lifecycle.mjs — Git worktree lifecycle management
|
|
2
|
-
// Convention: .codex-swarm/wt-{slug} paths, swarm/{runId}/{slug} branches.
|
|
3
|
-
// Remote support: host option → SSH-based git operations via remote-session.mjs.
|
|
4
|
-
|
|
5
|
-
import { execFile } from "node:child_process";
|
|
6
|
-
import {
|
|
7
|
-
access,
|
|
8
|
-
mkdir,
|
|
9
|
-
mkdtemp,
|
|
10
|
-
readdir,
|
|
11
|
-
realpath,
|
|
12
|
-
rm,
|
|
13
|
-
} from "node:fs/promises";
|
|
14
|
-
import { tmpdir } from "node:os";
|
|
15
|
-
import { join, normalize, relative, resolve } from "node:path";
|
|
16
|
-
import { remoteGit, validateHost } from "./remote-session.mjs";
|
|
17
|
-
|
|
18
|
-
const SWARM_ROOT = ".codex-swarm";
|
|
19
|
-
const SLEEP_MS = 2000; // WT race-guard (MEMORY.md: wt-attach-spacing)
|
|
20
|
-
|
|
21
|
-
// Tracked files intentionally removed from swarm worktrees. Keep this list
|
|
22
|
-
// narrow: .claude-plugin metadata must be preserved for Codex worker startup.
|
|
23
|
-
export const EXPECTED_WORKTREE_DELETIONS = Object.freeze([]);
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Parse `git status --short` output into a dirty-file list. Filters out paths
|
|
27
|
-
* in `expectedDeletions` only when the XY status code indicates a deletion
|
|
28
|
-
* (X='D' or Y='D'). Modifications / additions / untracked of the same paths
|
|
29
|
-
* remain dirty — otherwise a worker could silently corrupt those files and
|
|
30
|
-
* bypass F6 no_commit_guard (Codex review #134 round 2).
|
|
31
|
-
*
|
|
32
|
-
* git status --short XY codes reference:
|
|
33
|
-
* ' D' unstaged delete, 'D ' staged delete, 'DD' both — filtered if path
|
|
34
|
-
* is in expectedDeletions.
|
|
35
|
-
* ' M'/'M '/'MM' modify, '??' untracked, 'A ' add, 'R ' rename — kept.
|
|
36
|
-
*
|
|
37
|
-
* @param {string} rawStatus — stdout of `git status --short`
|
|
38
|
-
* @param {string[]} [expectedDeletions=EXPECTED_WORKTREE_DELETIONS] — paths eligible for deletion-only skip
|
|
39
|
-
* @returns {string[]} — remaining dirty paths after filtering
|
|
40
|
-
*/
|
|
41
|
-
export function extractDirtyFiles(
|
|
42
|
-
rawStatus,
|
|
43
|
-
expectedDeletions = EXPECTED_WORKTREE_DELETIONS,
|
|
44
|
-
) {
|
|
45
|
-
const skip = new Set(expectedDeletions);
|
|
46
|
-
const out = [];
|
|
47
|
-
for (const raw of String(rawStatus ?? "").split(/\r?\n/)) {
|
|
48
|
-
if (raw.length < 3) continue;
|
|
49
|
-
const xy = raw.slice(0, 2);
|
|
50
|
-
const path = raw.slice(2).trim();
|
|
51
|
-
if (!path) continue;
|
|
52
|
-
const isDeletion = xy.includes("D");
|
|
53
|
-
if (isDeletion && skip.has(path)) continue;
|
|
54
|
-
out.push(path);
|
|
55
|
-
}
|
|
56
|
-
return out;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function git(args, cwd) {
|
|
60
|
-
return new Promise((res, rej) => {
|
|
61
|
-
execFile(
|
|
62
|
-
"git",
|
|
63
|
-
args,
|
|
64
|
-
{
|
|
65
|
-
cwd,
|
|
66
|
-
windowsHide: true,
|
|
67
|
-
timeout: 30_000,
|
|
68
|
-
env: {
|
|
69
|
-
...process.env,
|
|
70
|
-
GIT_AUTHOR_NAME: process.env.GIT_AUTHOR_NAME || "Triflux",
|
|
71
|
-
GIT_AUTHOR_EMAIL:
|
|
72
|
-
process.env.GIT_AUTHOR_EMAIL || "triflux@example.invalid",
|
|
73
|
-
GIT_COMMITTER_NAME: process.env.GIT_COMMITTER_NAME || "Triflux",
|
|
74
|
-
GIT_COMMITTER_EMAIL:
|
|
75
|
-
process.env.GIT_COMMITTER_EMAIL || "triflux@example.invalid",
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
(err, stdout, stderr) => {
|
|
79
|
-
if (err) {
|
|
80
|
-
const msg = `git ${args[0]} failed: ${stderr?.trim() || err.message}`;
|
|
81
|
-
rej(new Error(msg));
|
|
82
|
-
} else {
|
|
83
|
-
res(stdout.trim());
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
);
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Best-effort shutdown for the Git fsmonitor daemon attached to a worktree.
|
|
92
|
-
* Missing or already-stopped daemons are treated as successful no-ops because
|
|
93
|
-
* cleanup should not be blocked by watcher state.
|
|
94
|
-
*
|
|
95
|
-
* @param {string} worktreePath
|
|
96
|
-
* @param {(args: string[], cwd: string) => Promise<string>} [gitImpl=git]
|
|
97
|
-
* @returns {Promise<{ ok: boolean, stopped: boolean, reason?: 'not_running', error?: string }>}
|
|
98
|
-
*/
|
|
99
|
-
async function stopFsmonitorDaemon(worktreePath, gitImpl = git) {
|
|
100
|
-
try {
|
|
101
|
-
await gitImpl(["fsmonitor--daemon", "stop"], worktreePath);
|
|
102
|
-
return { ok: true, stopped: true };
|
|
103
|
-
} catch (err) {
|
|
104
|
-
const msg = String(err?.message || err || "");
|
|
105
|
-
if (
|
|
106
|
-
/not running|not watching|fsmonitor--daemon is not running/iu.test(msg)
|
|
107
|
-
) {
|
|
108
|
-
return { ok: true, stopped: false, reason: "not_running" };
|
|
109
|
-
}
|
|
110
|
-
return { ok: false, stopped: false, error: msg };
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function sleep(ms) {
|
|
115
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** Normalize path for Windows compatibility. */
|
|
119
|
-
function normPath(p) {
|
|
120
|
-
return normalize(p).replace(/\\/g, "/");
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async function normExistingPath(p) {
|
|
124
|
-
try {
|
|
125
|
-
return normPath(await realpath(p));
|
|
126
|
-
} catch {
|
|
127
|
-
return normPath(p);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function resolveCleanupTarget(worktreePath, rootDir) {
|
|
132
|
-
const resolvedRoot = resolve(rootDir);
|
|
133
|
-
const resolvedWorktree = resolve(worktreePath);
|
|
134
|
-
const normalizedRoot = normPath(resolvedRoot).replace(/\/+$/u, "");
|
|
135
|
-
const normalizedWorktree = normPath(resolvedWorktree).replace(/\/+$/u, "");
|
|
136
|
-
|
|
137
|
-
if (normalizedWorktree === normalizedRoot) {
|
|
138
|
-
throw new Error("refusing to cleanup the main working tree");
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const rel = normPath(relative(resolvedRoot, resolvedWorktree));
|
|
142
|
-
if (
|
|
143
|
-
!rel ||
|
|
144
|
-
rel === "." ||
|
|
145
|
-
rel === ".." ||
|
|
146
|
-
rel.startsWith("../") ||
|
|
147
|
-
rel.includes("/../")
|
|
148
|
-
) {
|
|
149
|
-
throw new Error(
|
|
150
|
-
`refusing to cleanup path outside rootDir: ${worktreePath}`,
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const allowed =
|
|
155
|
-
rel.startsWith(".codex-swarm/wt-") || rel.startsWith(".triflux/");
|
|
156
|
-
if (!allowed) {
|
|
157
|
-
throw new Error(`refusing to cleanup unsafe path: ${worktreePath}`);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return {
|
|
161
|
-
resolvedRoot,
|
|
162
|
-
resolvedWorktree,
|
|
163
|
-
normalizedWorktree,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
async function branchExists(branchName, rootDir, gitImpl = git) {
|
|
168
|
-
if (!branchName) return false;
|
|
169
|
-
try {
|
|
170
|
-
const listed = await gitImpl(["branch", "--list", branchName], rootDir);
|
|
171
|
-
return listed.trim().length > 0;
|
|
172
|
-
} catch {
|
|
173
|
-
return false;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Create a worktree for a shard.
|
|
179
|
-
*
|
|
180
|
-
* @param {object} opts
|
|
181
|
-
* @param {string} opts.slug — shard identifier (e.g. "issue-42" or "auth-refactor")
|
|
182
|
-
* @param {string} opts.runId — swarm run ID
|
|
183
|
-
* @param {string} [opts.rootDir=process.cwd()] — repo root
|
|
184
|
-
* @param {string} [opts.baseBranch='main'] — base branch to branch from
|
|
185
|
-
* @param {string} [opts.host] — SSH host for remote worktree creation
|
|
186
|
-
* @param {object} [opts.remoteEnv] — remote environment from probeRemoteEnv()
|
|
187
|
-
* @returns {Promise<{ worktreePath: string, branchName: string, remote: boolean }>}
|
|
188
|
-
*/
|
|
189
|
-
export async function ensureWorktree({
|
|
190
|
-
slug,
|
|
191
|
-
runId,
|
|
192
|
-
rootDir = process.cwd(),
|
|
193
|
-
baseBranch = "main",
|
|
194
|
-
host,
|
|
195
|
-
remoteEnv,
|
|
196
|
-
}) {
|
|
197
|
-
const branchName = `swarm/${runId}/${slug}`;
|
|
198
|
-
|
|
199
|
-
// ── Remote path: SSH-based worktree creation ──
|
|
200
|
-
if (host && remoteEnv) {
|
|
201
|
-
const remoteRoot = rootDir.replace(/\\/g, "/");
|
|
202
|
-
const remoteWtDir = `${remoteRoot}/${SWARM_ROOT}/wt-${slug}`;
|
|
203
|
-
|
|
204
|
-
try {
|
|
205
|
-
remoteGit(host, remoteEnv, ["worktree", "prune"], remoteRoot);
|
|
206
|
-
} catch {
|
|
207
|
-
/* best-effort */
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
try {
|
|
211
|
-
remoteGit(
|
|
212
|
-
host,
|
|
213
|
-
remoteEnv,
|
|
214
|
-
["worktree", "add", remoteWtDir, "-b", branchName, baseBranch],
|
|
215
|
-
remoteRoot,
|
|
216
|
-
);
|
|
217
|
-
} catch {
|
|
218
|
-
try {
|
|
219
|
-
remoteGit(
|
|
220
|
-
host,
|
|
221
|
-
remoteEnv,
|
|
222
|
-
["worktree", "add", remoteWtDir, branchName],
|
|
223
|
-
remoteRoot,
|
|
224
|
-
);
|
|
225
|
-
} catch {
|
|
226
|
-
/* already exists — acceptable */
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
return { worktreePath: remoteWtDir, branchName, remote: true };
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// ── Local path (existing logic) ──
|
|
234
|
-
const wtDir = resolve(rootDir, SWARM_ROOT, `wt-${slug}`);
|
|
235
|
-
|
|
236
|
-
await mkdir(resolve(rootDir, SWARM_ROOT), { recursive: true });
|
|
237
|
-
|
|
238
|
-
// Check if worktree already exists
|
|
239
|
-
try {
|
|
240
|
-
await access(wtDir);
|
|
241
|
-
await git(["rev-parse", "--is-inside-work-tree"], wtDir);
|
|
242
|
-
return { worktreePath: normPath(wtDir), branchName, remote: false };
|
|
243
|
-
} catch {
|
|
244
|
-
// Doesn't exist or invalid — create fresh
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
try {
|
|
248
|
-
await git(["worktree", "prune"], rootDir);
|
|
249
|
-
} catch {
|
|
250
|
-
/* best-effort */
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
await sleep(SLEEP_MS);
|
|
254
|
-
|
|
255
|
-
try {
|
|
256
|
-
await git(
|
|
257
|
-
["worktree", "add", wtDir, "-b", branchName, baseBranch],
|
|
258
|
-
rootDir,
|
|
259
|
-
);
|
|
260
|
-
} catch {
|
|
261
|
-
await git(["worktree", "add", wtDir, branchName], rootDir);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
return { worktreePath: normPath(wtDir), branchName, remote: false };
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Create a temporary integration branch for merge operations.
|
|
269
|
-
*
|
|
270
|
-
* @param {object} opts
|
|
271
|
-
* @param {string} opts.runId
|
|
272
|
-
* @param {string} opts.baseBranch
|
|
273
|
-
* @param {string} [opts.rootDir=process.cwd()]
|
|
274
|
-
* @returns {Promise<{ integrationBranch: string, baseCommit: string }>}
|
|
275
|
-
* @throws {Error} when the integration branch is checked out by another
|
|
276
|
-
* worktree; this is an intentional NO-GO and leaves that worktree untouched.
|
|
277
|
-
*/
|
|
278
|
-
export async function prepareIntegrationBranch({
|
|
279
|
-
runId,
|
|
280
|
-
baseBranch,
|
|
281
|
-
rootDir = process.cwd(),
|
|
282
|
-
}) {
|
|
283
|
-
const integrationBranch = `swarm/${runId}/merge`;
|
|
284
|
-
|
|
285
|
-
// Record base commit for rollback
|
|
286
|
-
const baseCommit = await git(["rev-parse", baseBranch], rootDir);
|
|
287
|
-
|
|
288
|
-
// Create integration branch from base
|
|
289
|
-
try {
|
|
290
|
-
await git(["branch", integrationBranch, baseBranch], rootDir);
|
|
291
|
-
} catch {
|
|
292
|
-
// Branch may already exist — reset to base
|
|
293
|
-
await git(["branch", "-f", integrationBranch, baseBranch], rootDir);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return { integrationBranch, baseCommit };
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Rebase a shard branch onto the integration branch.
|
|
301
|
-
* Uses rebase + fast-forward only (no merge commits).
|
|
302
|
-
*
|
|
303
|
-
* @param {object} opts
|
|
304
|
-
* @param {string} opts.shardBranch — the shard's branch name
|
|
305
|
-
* @param {string} opts.integrationBranch — target integration branch
|
|
306
|
-
* @param {string} [opts.rootDir=process.cwd()]
|
|
307
|
-
* @returns {Promise<{ ok: boolean, headCommit?: string, strategy?: string, commits?: number, notes?: string, fallbackReason?: string, error?: string }>}
|
|
308
|
-
*/
|
|
309
|
-
export async function rebaseShardOntoIntegration({
|
|
310
|
-
shardBranch,
|
|
311
|
-
integrationBranch,
|
|
312
|
-
rootDir = process.cwd(),
|
|
313
|
-
}) {
|
|
314
|
-
// #127 BUG-E accident history: this function used to checkout the
|
|
315
|
-
// integration branch in rootDir, leak rootDir HEAD, and redirect later user
|
|
316
|
-
// edits/commits into swarm/.../merge. Integration now happens exclusively in
|
|
317
|
-
// a disposable worktree, so rootDir HEAD never moves and needs no restore.
|
|
318
|
-
//
|
|
319
|
-
// #129 BUG-J accident history: checkout failure was followed by a blind
|
|
320
|
-
// reset --hard (or branch -f rollback) against rootDir, which rewound the
|
|
321
|
-
// caller branch. Rollback now resets only the disposable worktree; there is
|
|
322
|
-
// no rootDir reset path and therefore no stash-create last-resort path.
|
|
323
|
-
const backupCommit = await git(["rev-parse", integrationBranch], rootDir);
|
|
324
|
-
|
|
325
|
-
let shaList = [];
|
|
326
|
-
let ffError = null;
|
|
327
|
-
let integrationWorktreeAdded = false;
|
|
328
|
-
const integrationWorktree = await mkdtemp(
|
|
329
|
-
join(tmpdir(), "tfx-swarm-integration-"),
|
|
330
|
-
);
|
|
331
|
-
|
|
332
|
-
try {
|
|
333
|
-
const log = await git(
|
|
334
|
-
[
|
|
335
|
-
"log",
|
|
336
|
-
"--reverse",
|
|
337
|
-
"--format=%H",
|
|
338
|
-
`${integrationBranch}..${shardBranch}`,
|
|
339
|
-
],
|
|
340
|
-
rootDir,
|
|
341
|
-
);
|
|
342
|
-
shaList = log
|
|
343
|
-
.split("\n")
|
|
344
|
-
.map((s) => s.trim())
|
|
345
|
-
.filter(Boolean);
|
|
346
|
-
|
|
347
|
-
await git(
|
|
348
|
-
["worktree", "add", "--quiet", integrationWorktree, integrationBranch],
|
|
349
|
-
rootDir,
|
|
350
|
-
);
|
|
351
|
-
integrationWorktreeAdded = true;
|
|
352
|
-
|
|
353
|
-
try {
|
|
354
|
-
await git(["merge", "--ff-only", shardBranch], integrationWorktree);
|
|
355
|
-
const headCommit = await git(["rev-parse", "HEAD"], integrationWorktree);
|
|
356
|
-
return {
|
|
357
|
-
ok: true,
|
|
358
|
-
headCommit,
|
|
359
|
-
strategy: "auto-ff",
|
|
360
|
-
commits: shaList.length,
|
|
361
|
-
notes: "linear (merge --ff-only)",
|
|
362
|
-
};
|
|
363
|
-
} catch (err) {
|
|
364
|
-
ffError = err;
|
|
365
|
-
try {
|
|
366
|
-
await git(["merge", "--abort"], integrationWorktree);
|
|
367
|
-
} catch {
|
|
368
|
-
/* ff-only usually leaves no merge state */
|
|
369
|
-
}
|
|
370
|
-
try {
|
|
371
|
-
// Safe destructive operation: this cwd is the disposable integration
|
|
372
|
-
// worktree, never rootDir. It restores the integration ref before the
|
|
373
|
-
// cherry-pick fallback without touching user files.
|
|
374
|
-
await git(["reset", "--hard", backupCommit], integrationWorktree);
|
|
375
|
-
} catch {
|
|
376
|
-
/* fallback below will report if cherry-pick also fails */
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// #127: cherry-pick fallback instead of branch merge. Rebase/ff paths can
|
|
381
|
-
// fail when histories diverge or a shard branch is checked out by a swarm
|
|
382
|
-
// worktree. Cherry-pick reads commits without moving the shard branch ref.
|
|
383
|
-
for (const sha of shaList) {
|
|
384
|
-
await git(["cherry-pick", sha], integrationWorktree);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const headCommit = await git(["rev-parse", "HEAD"], integrationWorktree);
|
|
388
|
-
return {
|
|
389
|
-
ok: true,
|
|
390
|
-
headCommit,
|
|
391
|
-
strategy: "cherry-pick",
|
|
392
|
-
commits: shaList.length,
|
|
393
|
-
fallbackReason: ffError?.message || null,
|
|
394
|
-
notes: ffError
|
|
395
|
-
? `auto-ff failed: ${ffError.message}`
|
|
396
|
-
: "cherry-pick fallback",
|
|
397
|
-
};
|
|
398
|
-
} catch (err) {
|
|
399
|
-
if (integrationWorktreeAdded) {
|
|
400
|
-
try {
|
|
401
|
-
await git(["cherry-pick", "--abort"], integrationWorktree);
|
|
402
|
-
} catch {
|
|
403
|
-
/* already clean */
|
|
404
|
-
}
|
|
405
|
-
try {
|
|
406
|
-
// #129 rollback is now local to the disposable integration worktree.
|
|
407
|
-
await git(["reset", "--hard", backupCommit], integrationWorktree);
|
|
408
|
-
} catch {
|
|
409
|
-
/* best-effort */
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
return {
|
|
414
|
-
ok: false,
|
|
415
|
-
error: err.message,
|
|
416
|
-
strategy: "failed",
|
|
417
|
-
commits: shaList.length,
|
|
418
|
-
fallbackReason: ffError?.message || null,
|
|
419
|
-
};
|
|
420
|
-
} finally {
|
|
421
|
-
if (integrationWorktreeAdded) {
|
|
422
|
-
try {
|
|
423
|
-
await git(
|
|
424
|
-
["worktree", "remove", "--force", integrationWorktree],
|
|
425
|
-
rootDir,
|
|
426
|
-
);
|
|
427
|
-
} catch {
|
|
428
|
-
await rm(integrationWorktree, { recursive: true, force: true });
|
|
429
|
-
try {
|
|
430
|
-
await git(["worktree", "prune"], rootDir);
|
|
431
|
-
} catch {
|
|
432
|
-
/* best-effort metadata cleanup */
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
} else {
|
|
436
|
-
await rm(integrationWorktree, { recursive: true, force: true });
|
|
437
|
-
try {
|
|
438
|
-
await git(["worktree", "prune"], rootDir);
|
|
439
|
-
} catch {
|
|
440
|
-
/* best-effort metadata cleanup after a partial worktree add */
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
/**
|
|
447
|
-
* Safely remove a worktree directory and delete its branch.
|
|
448
|
-
* Refuses to touch the main working tree or paths outside known swarm state.
|
|
449
|
-
*
|
|
450
|
-
* @param {object} opts
|
|
451
|
-
* @param {string} opts.worktreePath
|
|
452
|
-
* @param {string} [opts.branchName]
|
|
453
|
-
* @param {string} [opts.rootDir=process.cwd()]
|
|
454
|
-
* @param {boolean} [opts.force=false]
|
|
455
|
-
*/
|
|
456
|
-
export async function cleanupWorktree({
|
|
457
|
-
worktreePath,
|
|
458
|
-
branchName,
|
|
459
|
-
rootDir = process.cwd(),
|
|
460
|
-
force = false,
|
|
461
|
-
_git = git,
|
|
462
|
-
}) {
|
|
463
|
-
const { resolvedWorktree } = resolveCleanupTarget(worktreePath, rootDir);
|
|
464
|
-
const fsmonitorStop = await stopFsmonitorDaemon(resolvedWorktree, _git);
|
|
465
|
-
const forceArgs = force ? ["--force"] : [];
|
|
466
|
-
|
|
467
|
-
for (let attempt = 0; attempt < 3; attempt++) {
|
|
468
|
-
try {
|
|
469
|
-
await _git(
|
|
470
|
-
["worktree", "remove", ...forceArgs, resolvedWorktree],
|
|
471
|
-
rootDir,
|
|
472
|
-
);
|
|
473
|
-
break;
|
|
474
|
-
} catch (_err) {
|
|
475
|
-
if (attempt === 2) {
|
|
476
|
-
try {
|
|
477
|
-
await rm(resolvedWorktree, { recursive: true, force: true });
|
|
478
|
-
} catch {
|
|
479
|
-
/* ignore */
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
await sleep(SLEEP_MS);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
await sleep(SLEEP_MS);
|
|
487
|
-
try {
|
|
488
|
-
await _git(["worktree", "prune"], rootDir);
|
|
489
|
-
} catch {
|
|
490
|
-
/* best-effort */
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
if (branchName && (await branchExists(branchName, rootDir, _git))) {
|
|
494
|
-
try {
|
|
495
|
-
await _git(["branch", "-D", branchName], rootDir);
|
|
496
|
-
} catch {
|
|
497
|
-
/* branch may already be gone */
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
return { ok: true, fsmonitorStop };
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
/**
|
|
505
|
-
* Remove a worktree and its branch.
|
|
506
|
-
* Follows WT race-guard: sleep between operations.
|
|
507
|
-
*
|
|
508
|
-
* @param {object} opts
|
|
509
|
-
* @param {string} opts.worktreePath
|
|
510
|
-
* @param {string} [opts.branchName] — optional branch to delete
|
|
511
|
-
* @param {string} [opts.rootDir=process.cwd()]
|
|
512
|
-
* @param {boolean} [opts.force=false]
|
|
513
|
-
*/
|
|
514
|
-
export async function pruneWorktree({
|
|
515
|
-
worktreePath,
|
|
516
|
-
branchName,
|
|
517
|
-
rootDir = process.cwd(),
|
|
518
|
-
force = false,
|
|
519
|
-
}) {
|
|
520
|
-
const cleanupResult = await cleanupWorktree({
|
|
521
|
-
worktreePath,
|
|
522
|
-
branchName,
|
|
523
|
-
rootDir,
|
|
524
|
-
force,
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
return { ok: true, fsmonitorStop: cleanupResult.fsmonitorStop };
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
/**
|
|
531
|
-
* #34 L3: Detect and remove orphan worktree directories.
|
|
532
|
-
* Compares .codex-swarm/wt-* directories against `git worktree list`.
|
|
533
|
-
* Directories not registered as worktrees are removed.
|
|
534
|
-
*
|
|
535
|
-
* @param {object} [opts]
|
|
536
|
-
* @param {string} [opts.rootDir=process.cwd()]
|
|
537
|
-
* @returns {Promise<string[]>} removed directory names
|
|
538
|
-
*/
|
|
539
|
-
export async function pruneOrphanWorktrees({ rootDir = process.cwd() } = {}) {
|
|
540
|
-
const swarmDir = resolve(rootDir, SWARM_ROOT);
|
|
541
|
-
const removed = [];
|
|
542
|
-
|
|
543
|
-
let entries;
|
|
544
|
-
try {
|
|
545
|
-
entries = await readdir(swarmDir);
|
|
546
|
-
} catch {
|
|
547
|
-
return removed; // .codex-swarm/ doesn't exist → nothing to clean
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
const wtDirs = entries.filter((e) => e.startsWith("wt-"));
|
|
551
|
-
if (wtDirs.length === 0) return removed;
|
|
552
|
-
|
|
553
|
-
// Get registered worktree paths from git
|
|
554
|
-
let registeredPaths;
|
|
555
|
-
try {
|
|
556
|
-
const raw = await git(["worktree", "list", "--porcelain"], rootDir);
|
|
557
|
-
registeredPaths = new Set(
|
|
558
|
-
await Promise.all(
|
|
559
|
-
raw
|
|
560
|
-
.split("\n")
|
|
561
|
-
.filter((l) => l.startsWith("worktree "))
|
|
562
|
-
.map((l) => normExistingPath(l.slice("worktree ".length))),
|
|
563
|
-
),
|
|
564
|
-
);
|
|
565
|
-
} catch {
|
|
566
|
-
return removed; // git worktree list failed → don't remove anything
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
for (const dir of wtDirs) {
|
|
570
|
-
const fullPath = resolve(swarmDir, dir);
|
|
571
|
-
const normalized = await normExistingPath(fullPath);
|
|
572
|
-
if (!registeredPaths.has(normalized)) {
|
|
573
|
-
try {
|
|
574
|
-
await stopFsmonitorDaemon(fullPath).catch(() => null);
|
|
575
|
-
await rm(fullPath, { recursive: true, force: true });
|
|
576
|
-
removed.push(dir);
|
|
577
|
-
} catch {
|
|
578
|
-
/* best-effort */
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// Prune stale git references
|
|
584
|
-
if (removed.length > 0) {
|
|
585
|
-
try {
|
|
586
|
-
await git(["worktree", "prune"], rootDir);
|
|
587
|
-
} catch {
|
|
588
|
-
/* best-effort */
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
return removed;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
/**
|
|
596
|
-
* Fetch a remote shard's branch to the local repo via SSH.
|
|
597
|
-
* Workaround for hosts that cannot push to GitHub (e.g. Ultra4).
|
|
598
|
-
*
|
|
599
|
-
* Flow: add temp remote → fetch branch → remove remote.
|
|
600
|
-
*
|
|
601
|
-
* @param {object} opts
|
|
602
|
-
* @param {string} opts.host — SSH host (e.g. "ultra4")
|
|
603
|
-
* @param {string} opts.sshUser — SSH user (e.g. "SSAFY")
|
|
604
|
-
* @param {string} opts.remoteRepoPath — absolute path on remote (e.g. "/c/Users/SSAFY/Desktop/Projects/cli/triflux")
|
|
605
|
-
* @param {string} opts.branchName — branch to fetch (e.g. "swarm/run123/auth")
|
|
606
|
-
* @param {string} [opts.rootDir=process.cwd()] — local repo root
|
|
607
|
-
* @returns {Promise<{ ok: boolean, localRef?: string, error?: string }>}
|
|
608
|
-
*/
|
|
609
|
-
export async function fetchRemoteShard({
|
|
610
|
-
host,
|
|
611
|
-
sshUser,
|
|
612
|
-
remoteRepoPath,
|
|
613
|
-
branchName,
|
|
614
|
-
rootDir = process.cwd(),
|
|
615
|
-
}) {
|
|
616
|
-
validateHost(host);
|
|
617
|
-
|
|
618
|
-
const remoteName = `_swarm-${host}-${Date.now()}`;
|
|
619
|
-
const sshUrl = `ssh://${sshUser}@${host}${remoteRepoPath}`;
|
|
620
|
-
|
|
621
|
-
try {
|
|
622
|
-
await git(["remote", "add", remoteName, sshUrl], rootDir);
|
|
623
|
-
|
|
624
|
-
await git(["fetch", remoteName, branchName, "--no-tags"], rootDir);
|
|
625
|
-
|
|
626
|
-
const localRef = `${remoteName}/${branchName}`;
|
|
627
|
-
const headCommit = await git(["rev-parse", `FETCH_HEAD`], rootDir);
|
|
628
|
-
|
|
629
|
-
return { ok: true, localRef, headCommit };
|
|
630
|
-
} catch (err) {
|
|
631
|
-
return { ok: false, error: err.message };
|
|
632
|
-
} finally {
|
|
633
|
-
try {
|
|
634
|
-
await git(["remote", "remove", remoteName], rootDir);
|
|
635
|
-
} catch {
|
|
636
|
-
/* cleanup */
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 1,
|
|
3
|
-
"templates": {
|
|
4
|
-
"swarm-3": {
|
|
5
|
-
"description": "Lead tab plus two worker tabs in the shared triflux window.",
|
|
6
|
-
"windowName": "triflux",
|
|
7
|
-
"tabs": [
|
|
8
|
-
{
|
|
9
|
-
"title": "swarm lead",
|
|
10
|
-
"profile": "triflux"
|
|
11
|
-
},
|
|
12
|
-
{
|
|
13
|
-
"title": "swarm worker-1",
|
|
14
|
-
"profile": "triflux"
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
"title": "swarm worker-2",
|
|
18
|
-
"profile": "triflux"
|
|
19
|
-
}
|
|
20
|
-
]
|
|
21
|
-
},
|
|
22
|
-
"monitor": {
|
|
23
|
-
"description": "Single monitoring tab for dashboard, log tailing, or observer flows.",
|
|
24
|
-
"windowName": "triflux",
|
|
25
|
-
"tabs": [
|
|
26
|
-
{
|
|
27
|
-
"title": "swarm monitor",
|
|
28
|
-
"profile": "triflux"
|
|
29
|
-
}
|
|
30
|
-
]
|
|
31
|
-
},
|
|
32
|
-
"single": {
|
|
33
|
-
"description": "One focused tab for direct operator work.",
|
|
34
|
-
"windowName": "triflux",
|
|
35
|
-
"tabs": [
|
|
36
|
-
{
|
|
37
|
-
"title": "single worker",
|
|
38
|
-
"profile": "triflux"
|
|
39
|
-
}
|
|
40
|
-
]
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|