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:
@@ -299,6 +299,18 @@ describe('loadConfig — session/rate/timer constants', () => {
|
||||
expect(() => loadConfig({ MAX_SESSIONS: 'lots' })).toThrow()
|
||||
})
|
||||
|
||||
it('defaults maxFanoutLanes to 6 (W5)', () => {
|
||||
expect(loadConfig({}).maxFanoutLanes).toBe(6)
|
||||
})
|
||||
|
||||
it('reads MAX_FANOUT_LANES from env', () => {
|
||||
expect(loadConfig({ MAX_FANOUT_LANES: '3' }).maxFanoutLanes).toBe(3)
|
||||
})
|
||||
|
||||
it('throws for a negative MAX_FANOUT_LANES (fail-fast, like MAX_SESSIONS)', () => {
|
||||
expect(() => loadConfig({ MAX_FANOUT_LANES: '-1' })).toThrow()
|
||||
})
|
||||
|
||||
it('defaults maxMsgsPerSec to 2000 and reads MAX_MSGS_PER_SEC', () => {
|
||||
expect(loadConfig({}).maxMsgsPerSec).toBe(2000)
|
||||
expect(loadConfig({ MAX_MSGS_PER_SEC: '500' }).maxMsgsPerSec).toBe(500)
|
||||
|
||||
Reference in New Issue
Block a user