feat(fanout): worktree fan-out board — race N agent lanes of one repo (W5)

Fan ONE prompt across N branch/agent lanes: N worktrees, N Claude sessions on the
same prompt, watched side-by-side in the split-grid board, approve/kill per lane,
🏆 keep the winner (losers' worktrees removed). ~90% composition of shipped parts.

- src/http/session-groups.ts (new, pure, no git exec): deriveRepoRoot /
  groupSessionsByRepo (cluster sessions by their <repo>-worktrees parent).
- GET /live-sessions/grouped (read-only; registered BEFORE /live-sessions/:id so
  "grouped" isn't captured as an id). MAX_FANOUT_LANES env (default 6, = grid-6 cap).
- public/fanout.ts (new, pure): buildFanoutCmd shell-quotes the prompt (single-quote
  wrap with '\''-escaping so $(...)/backticks/;/&& can't execute) + collapses newlines
  + caps 4000 chars; laneBranch/slugify. Effective N = min(lanes, maxFanoutLanes, 6),
  ≥2; the maxSessions cap is enforced server-side ("Started K of N" banner).
- public/tabs.ts launchFanout (N× createWorktree → openProject w/ prompt pre-injected
  via the existing initialInput) + keepFanoutWinner; public/projects.ts renderFanoutForm
  + extracted shared createWorktreeReq (DRY). Byte-shuttle preserved (lane = own PTY).
  One-click merge deferred (winner session stays open for a manual merge).

Reused unchanged: createWorktree/removeWorktree, addEntry/initialInput, split-grid +
per-quadrant approve/maximize/monitor + gauges. Verified: typecheck + build:web clean,
2063 pass at --test-timeout=30000 (only the known tmux/PTY flake red). Fixed 3
/config/ui exact-shape tests to include the new maxFanoutLanes field.
This commit is contained in:
Yaojia Wang
2026-07-13 05:09:19 +02:00
parent c81821b890
commit 9683a16f4f
20 changed files with 1225 additions and 8 deletions

View File

@@ -72,6 +72,8 @@ export interface Config {
readonly worktreeEnabled: boolean; // WORKTREE_ENABLED, default true
readonly worktreeRoot: string | undefined; // WORKTREE_ROOT (undefined → computed)
readonly worktreeTimeoutMs: number; // WORKTREE_TIMEOUT_MS, default 10000
// W5 fan-out board — cap on lanes one task may be fanned across (grid-6 capacity)
readonly maxFanoutLanes: number; // MAX_FANOUT_LANES, default 6
// W4 git write (stage / commit / push) — the highest-risk write channel
readonly gitOpsEnabled: boolean; // GIT_OPS_ENABLED, default true (kill-switch → all 3 routes 403)
readonly gitOpsTimeoutMs: number; // GIT_OPS_TIMEOUT_MS, default 10000 (stage/commit exec bound)
@@ -311,6 +313,27 @@ export interface LiveSessionInfo {
readonly queueLength?: number;
}
/* ───────────────── W5 fan-out board (parallel agent lanes) ───────────────── */
/** A group of running sessions sharing a repo/worktree-root (fan-out discovery).
* Derived STRING-ONLY from each session's cwd (no git exec): fan-out worktrees
* live under `<repo>-worktrees/`, so sessions whose cwd shares that parent
* cluster into one group. impl: src/http/session-groups.ts groupSessionsByRepo. */
export interface SessionGroup {
repoRoot: string; // derived repo dir (parent of the *-worktrees folder, or the cwd)
label: string; // basename(repoRoot)
sessions: LiveSessionInfo[]; // members, newest-first (already the manager.list order)
}
/** Launch options the FE passes to TabApp.launchFanout (FE-internal, but typed
* shared so the form and the launcher agree on ONE shape). */
export interface FanoutLaunchOpts {
prompt: string;
lanes: number; // 2..maxFanoutLanes (clamped to grid-6 capacity + the DoS cap)
branchBase: string; // slug; lanes get `${branchBase}-lane-${i}`
mode?: PermissionMode; // reuse PermissionMode
}
/* ───────────────── project manager (v0.6, §4.3 FEATURE doc) ──────────────── */
/** One running session belonging to a project (live-sessions归并 by cwd).
@@ -658,6 +681,9 @@ export interface UiConfig {
/** W3 quick-wins (b): the cost-budget threshold (USD). Present when > 0 so the
* FE can derive cost-overage warn styling client-side; omitted when disabled. */
costBudgetUsd?: number;
/** W5 fan-out board: server-controlled cap on fan-out lanes so the FE stepper
* max mirrors MAX_FANOUT_LANES. Additive OPTIONAL (older clients default to 6). */
maxFanoutLanes?: number;
}
/* ── W3 quick-wins (c) reconnect digest (GET /digest) ── */