@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,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
- }