sneakoscope 0.7.74 → 0.7.78

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.
@@ -7,7 +7,7 @@ import { getCodexInfo } from './codex-adapter.mjs';
7
7
  import { codexAppIntegrationStatus, formatCodexAppStatus } from './codex-app.mjs';
8
8
  import { REQUIRED_CODEX_MODEL, forceGpt55CodexArgs } from './codex-model-guard.mjs';
9
9
  import { MIN_TEAM_REVIEWER_LANES } from './team-review-policy.mjs';
10
- import { appendTeamEvent, readTeamControl, readTeamDashboard, teamCleanupRequested } from './team-live.mjs';
10
+ import { appendTeamEvent, isTerminalTeamAgentStatus, readTeamControl, readTeamDashboard, teamCleanupRequested } from './team-live.mjs';
11
11
 
12
12
  const SKS_FIGLET_FONT = 'Standard';
13
13
 
@@ -101,27 +101,11 @@ export function tmuxCockpitStatePath(root = process.cwd()) {
101
101
  return path.join(path.resolve(root || process.cwd()), '.sneakoscope', 'state', 'tmux-cockpit.json');
102
102
  }
103
103
 
104
- const TERMINAL_TEAM_AGENT_STATUSES = new Set([
105
- 'agent_closed',
106
- 'agent_done',
107
- 'cancelled',
108
- 'canceled',
109
- 'cleanup',
110
- 'cleanup_requested',
111
- 'closed',
112
- 'complete',
113
- 'completed',
114
- 'done',
115
- 'ended',
116
- 'failed',
117
- 'stopped',
118
- 'terminal',
119
- 'tmux_lane_closed'
120
- ]);
121
-
122
104
  const LEGACY_TEAM_PANE_TITLE_RE = /^(?:overview: mission_overview|scout: analysis_scout|plan: (?:debate|consensus|planner|user)|exec: (?:executor|implementation|worker)|review: (?:reviewer|qa|validation)|safety:)/;
123
105
  const GENERIC_TEAM_AGENT_IDS = new Set(['parent_orchestrator', 'analysis_scout', 'team_consensus', 'implementation_worker', 'db_safety_reviewer', 'qa_reviewer']);
124
106
  const DYNAMIC_TEAM_TMUX_LAYOUT = 'main-vertical';
107
+ const TEAM_TMUX_MAIN_PANE_MIN_WIDTH = 48;
108
+ const TEAM_TMUX_MAIN_PANE_WIDTH_RATIO = 0.5;
125
109
 
126
110
  export function isTmuxShellSession(env = process.env) {
127
111
  return Boolean(String(env.TMUX || '').trim());
@@ -400,11 +384,6 @@ async function isRecordedSksTmuxSession(root, session) {
400
384
  return { ok: true, record };
401
385
  }
402
386
 
403
- function isTerminalTeamAgentStatus(status = '') {
404
- const normalized = String(status || '').trim().toLowerCase();
405
- return TERMINAL_TEAM_AGENT_STATUSES.has(normalized) || /(?:^|_)(?:done|complete|completed|closed|cleanup|cancelled|canceled|failed|ended|stopped)(?:_|$)/.test(normalized);
406
- }
407
-
408
387
  function teamCockpitAgentIds(plan = {}, dashboard = null, control = null, opts = {}) {
409
388
  if (teamCleanupRequested(control)) return [];
410
389
  const visible = teamViewAgentIds(plan).filter((id) => id && id !== 'mission_overview');
@@ -472,6 +451,35 @@ function parseTmuxPaneLines(stdout = '') {
472
451
  }).filter((pane) => /^%\d+$/.test(pane.pane_id || ''));
473
452
  }
474
453
 
454
+ function parseTmuxAllPaneLines(stdout = '') {
455
+ return String(stdout || '').split(/\r?\n/).filter(Boolean).map((line) => {
456
+ const [session, window_id, pane_id, title, command, managed, mission_id, agent, role] = line.split('\t');
457
+ return {
458
+ session: session || '',
459
+ window_id: window_id || '',
460
+ pane_id,
461
+ title: title || '',
462
+ command: command || '',
463
+ managed: managed === '1' || managed === 'true',
464
+ mission_id: mission_id || '',
465
+ agent: agent || '',
466
+ role: role || ''
467
+ };
468
+ }).filter((pane) => pane.session && /^@\d+$/.test(pane.window_id || '') && /^%\d+$/.test(pane.pane_id || ''));
469
+ }
470
+
471
+ function parseTmuxSessionLines(stdout = '') {
472
+ return String(stdout || '').split(/\r?\n/).filter(Boolean).map((line) => {
473
+ const [session, attached, created_at, last_attached_at] = line.split('\t');
474
+ return {
475
+ session: sanitizeTmuxSessionName(session || ''),
476
+ attached: Number(attached || 0) > 0,
477
+ created_at: Number(created_at || 0) || 0,
478
+ last_attached_at: Number(last_attached_at || 0) || 0
479
+ };
480
+ }).filter((entry) => entry.session);
481
+ }
482
+
475
483
  function isLegacyTeamPane(pane = {}) {
476
484
  return LEGACY_TEAM_PANE_TITLE_RE.test(String(pane.title || '').trim());
477
485
  }
@@ -483,6 +491,30 @@ async function listTmuxWindowPanes(bin, windowId) {
483
491
  return { ok: true, panes: parseTmuxPaneLines(run.stdout) };
484
492
  }
485
493
 
494
+ async function listAllTmuxPanes(bin) {
495
+ const format = ['#{session_name}', '#{window_id}', '#{pane_id}', '#{pane_title}', '#{pane_current_command}', '#{@sks_team_managed}', '#{@sks_mission_id}', '#{@sks_agent_id}', '#{@sks_lane_role}'].join('\t');
496
+ const run = await tmuxRun(bin, ['list-panes', '-a', '-F', format], { timeoutMs: 5000, maxOutputBytes: 64 * 1024 });
497
+ if (run.code !== 0) return { ok: false, panes: [], stderr: run.stderr || run.stdout || 'tmux list-panes -a failed' };
498
+ return { ok: true, panes: parseTmuxAllPaneLines(run.stdout) };
499
+ }
500
+
501
+ async function listAllTmuxSessions(bin) {
502
+ const format = ['#{session_name}', '#{session_attached}', '#{session_created}', '#{session_last_attached}'].join('\t');
503
+ const run = await tmuxRun(bin, ['list-sessions', '-F', format], { timeoutMs: 5000, maxOutputBytes: 64 * 1024 });
504
+ if (run.code !== 0) return { ok: false, sessions: [], stderr: run.stderr || run.stdout || 'tmux list-sessions failed' };
505
+ return { ok: true, sessions: parseTmuxSessionLines(run.stdout) };
506
+ }
507
+
508
+ async function tmuxPaneExists(bin, paneId) {
509
+ if (!paneId || !String(paneId).startsWith('%')) return false;
510
+ const run = await tmuxRun(bin, ['list-panes', '-t', paneId, '-F', '#{pane_dead}\t#{pane_id}'], { timeoutMs: 5000, maxOutputBytes: 4096 });
511
+ if (run.code !== 0) return false;
512
+ return String(run.stdout || '').split(/\r?\n/).some((line) => {
513
+ const [dead = '', id = ''] = line.trim().split('\t');
514
+ return id === paneId && dead !== '1';
515
+ });
516
+ }
517
+
486
518
  async function setTmuxPaneUserOptions(bin, paneId, options = {}) {
487
519
  const applied = [];
488
520
  const failed = [];
@@ -510,6 +542,56 @@ async function writeTmuxCockpitRecord(root, record = {}) {
510
542
  return nextRecord;
511
543
  }
512
544
 
545
+ async function readRecordedTeamSurfaceHints(root) {
546
+ const resolvedRoot = path.resolve(root || process.cwd());
547
+ const teamState = await readJson(tmuxTeamStatePath(resolvedRoot), {}).catch(() => ({}));
548
+ const cockpitState = await readJson(tmuxCockpitStatePath(resolvedRoot), {}).catch(() => ({}));
549
+ const missionRoot = path.join(resolvedRoot, '.sneakoscope', 'missions');
550
+ const records = [
551
+ ...Object.values(teamState?.missions || {}),
552
+ ...Object.values(cockpitState?.missions || {})
553
+ ].filter((entry) => entry && typeof entry === 'object')
554
+ .filter((entry) => !entry.root || path.resolve(entry.root) === resolvedRoot);
555
+ const missionIds = new Set();
556
+ const sessions = new Set();
557
+ const paneKeys = new Set();
558
+ for (const record of records) {
559
+ const missionId = String(record.mission_id || '').trim();
560
+ const session = sanitizeTmuxSessionName(record.session || '');
561
+ if (missionId) missionIds.add(missionId);
562
+ if (session) sessions.add(session);
563
+ for (const pane of Array.isArray(record.panes) ? record.panes : []) {
564
+ if (session && pane?.pane_id) paneKeys.add(`${session}\t${pane.pane_id}`);
565
+ }
566
+ }
567
+ const missionDirs = await fsp.readdir(missionRoot).catch(() => []);
568
+ for (const name of missionDirs) {
569
+ if (!/^M-\d{8}-\d{6}-[A-Za-z0-9]+$/.test(name)) continue;
570
+ missionIds.add(name);
571
+ sessions.add(sanitizeTmuxSessionName(`sks-team-${name}`));
572
+ }
573
+ return { missionIds, sessions, paneKeys, records };
574
+ }
575
+
576
+ function paneBelongsToKeptMission(pane = {}, keepMissionIds = new Set()) {
577
+ if (pane.mission_id && keepMissionIds.has(pane.mission_id)) return true;
578
+ const match = String(pane.session || '').match(/^sks-team-(M-\d{8}-\d{6}-[A-Za-z0-9]+)$/);
579
+ return Boolean(match && keepMissionIds.has(match[1]));
580
+ }
581
+
582
+ function shouldSweepTeamPane(pane = {}, hints = {}, opts = {}) {
583
+ if (!pane?.pane_id || pane.pane_id === opts.currentPaneId) return false;
584
+ if (paneBelongsToKeptMission(pane, opts.keepMissionIds || new Set())) return false;
585
+ const missionIds = hints.missionIds || new Set();
586
+ const sessions = hints.sessions || new Set();
587
+ const paneKeys = hints.paneKeys || new Set();
588
+ if (pane.managed && pane.mission_id && missionIds.has(pane.mission_id)) return true;
589
+ if (paneKeys.has(`${pane.session}\t${pane.pane_id}`)) return true;
590
+ if (sessions.has(pane.session) && isLegacyTeamPane(pane)) return true;
591
+ const legacySession = String(pane.session || '').match(/^sks-team-(M-\d{8}-\d{6}-[A-Za-z0-9]+)$/);
592
+ return Boolean(legacySession && missionIds.has(legacySession[1]) && isLegacyTeamPane(pane));
593
+ }
594
+
513
595
  function tmuxLayoutName(value = 'tiled') {
514
596
  const layout = String(value || 'tiled').trim();
515
597
  return /^(tiled|even-horizontal|even-vertical|main-horizontal|main-horizontal-mirrored|main-vertical|main-vertical-mirrored)$/.test(layout)
@@ -517,11 +599,52 @@ function tmuxLayoutName(value = 'tiled') {
517
599
  : 'tiled';
518
600
  }
519
601
 
602
+ function teamMainPaneWidthFromWindow(width) {
603
+ const n = Number(width);
604
+ if (!Number.isFinite(n) || n <= 0) return TEAM_TMUX_MAIN_PANE_MIN_WIDTH;
605
+ return Math.max(TEAM_TMUX_MAIN_PANE_MIN_WIDTH, Math.floor(n * TEAM_TMUX_MAIN_PANE_WIDTH_RATIO));
606
+ }
607
+
608
+ async function applyStableTeamLayout(tmuxBin, target, mainPaneId = null, opts = {}) {
609
+ const layout = tmuxLayoutName(opts.layout || DYNAMIC_TEAM_TMUX_LAYOUT);
610
+ const windowTarget = target || mainPaneId;
611
+ const applied = [];
612
+ const failed = [];
613
+ const runAndRecord = async (args) => {
614
+ const run = await tmuxRun(tmuxBin, args, { timeoutMs: 5000 });
615
+ const command = [path.basename(tmuxBin), ...args].join(' ');
616
+ if (run.code === 0) applied.push(command);
617
+ else failed.push({ command, stderr: run.stderr || run.stdout || 'tmux command failed' });
618
+ return run;
619
+ };
620
+ if (mainPaneId) await runAndRecord(['select-pane', '-t', mainPaneId]);
621
+ const width = await tmuxRun(tmuxBin, ['display-message', '-p', '-t', windowTarget, '#{window_width}'], { timeoutMs: 5000, maxOutputBytes: 1024 });
622
+ if (width.code === 0) {
623
+ const mainWidth = teamMainPaneWidthFromWindow(String(width.stdout || '').trim());
624
+ await runAndRecord(['set-window-option', '-t', windowTarget, 'main-pane-width', String(mainWidth)]);
625
+ }
626
+ await runAndRecord(['select-layout', '-t', windowTarget, layout]);
627
+ return { ok: failed.length === 0, layout_name: layout, applied, failed };
628
+ }
629
+
520
630
  async function enableTmuxDynamicResize(tmuxBin, session, opts = {}) {
521
631
  const layout = tmuxLayoutName(opts.layout || 'tiled');
522
632
  const safeSession = sanitizeTmuxSessionName(session);
523
633
  const target = await tmuxWindowTarget(tmuxBin, safeSession);
524
- const relayout = `resize-window -t ${target} -A; set-window-option -t ${target} window-size latest; select-layout -t ${target} ${layout}; select-layout -t ${target} -E; set-window-option -t ${target} window-size latest`;
634
+ const stableMainVertical = layout === DYNAMIC_TEAM_TMUX_LAYOUT && opts.stableTeamLayout;
635
+ const tmuxShell = shellEscape(tmuxBin || 'tmux');
636
+ const targetShell = shellEscape(target);
637
+ const stableRelayoutShell = [
638
+ `${tmuxShell} resize-window -t ${targetShell} -A >/dev/null 2>&1 || true`,
639
+ `${tmuxShell} set-window-option -t ${targetShell} window-size latest >/dev/null 2>&1 || true`,
640
+ `w=$(${tmuxShell} display-message -p -t ${targetShell} '#{window_width}' 2>/dev/null || printf 120)`,
641
+ `if [ "$w" -gt 0 ] 2>/dev/null; then ${tmuxShell} set-window-option -t ${targetShell} main-pane-width $((w / 2)) >/dev/null 2>&1 || true; fi`,
642
+ `${tmuxShell} select-layout -t ${targetShell} ${layout} >/dev/null 2>&1 || true`,
643
+ `${tmuxShell} set-window-option -t ${targetShell} window-size latest >/dev/null 2>&1 || true`
644
+ ].join('; ');
645
+ const relayout = stableMainVertical
646
+ ? `run-shell -b ${shellEscape(stableRelayoutShell)}`
647
+ : `resize-window -t ${target} -A; set-window-option -t ${target} window-size latest; select-layout -t ${target} ${layout}; select-layout -t ${target} -E; set-window-option -t ${target} window-size latest`;
525
648
  const commands = [
526
649
  ['set-window-option', '-t', target, 'window-size', 'latest'],
527
650
  ['set-window-option', '-t', target, 'aggressive-resize', 'on'],
@@ -529,8 +652,8 @@ async function enableTmuxDynamicResize(tmuxBin, session, opts = {}) {
529
652
  ['set-hook', '-t', safeSession, 'client-resized', relayout],
530
653
  ['resize-window', '-t', target, '-A'],
531
654
  ['set-window-option', '-t', target, 'window-size', 'latest'],
532
- ['select-layout', '-t', target, layout],
533
- ['select-layout', '-t', target, '-E'],
655
+ ...(stableMainVertical ? [] : [['select-layout', '-t', target, layout], ['select-layout', '-t', target, '-E']]),
656
+ ...(stableMainVertical ? [['display-message', '-p', '-t', target, '#{window_width}'], ['select-layout', '-t', target, layout]] : []),
534
657
  ['set-window-option', '-t', target, 'window-size', 'latest']
535
658
  ];
536
659
  const applied = [];
@@ -599,19 +722,21 @@ export async function createTmuxSession(plan = {}, panes = [], opts = {}) {
599
722
  const create = await tmuxRun(tmuxBin, ['new-session', '-d', '-x', dimensions.width, '-y', dimensions.height, '-s', session, '-c', path.resolve(first.cwd || root), '-n', 'sks', '-P', '-F', '#{pane_id}', first.command || 'pwd']);
600
723
  if (create.code !== 0) return { ok: false, session, panes: [], stderr: create.stderr || create.stdout || 'tmux new-session failed' };
601
724
  const created = [{ pane_id: paneId(create.stdout), role: first.role || 'overview', title: first.title || 'overview' }];
602
- let rightStackTarget = created[0].pane_id || session;
725
+ let rightStackRootPaneId = null;
603
726
  for (const pane of normalizedPanes.slice(1)) {
604
727
  const direction = rightSidePanes ? (created.length === 1 ? '-h' : '-v') : (pane.vertical ? '-v' : '-h');
605
- const splitTarget = rightSidePanes ? rightStackTarget : session;
728
+ const splitTarget = rightSidePanes ? (rightStackRootPaneId || created[0].pane_id || session) : session;
606
729
  const split = await tmuxRun(tmuxBin, ['split-window', '-t', splitTarget, direction, '-d', '-P', '-F', '#{pane_id}', '-c', path.resolve(pane.cwd || root), pane.command || 'pwd']);
607
730
  if (split.code !== 0) return { ok: false, session, panes: created, stderr: split.stderr || split.stdout || 'tmux split-window failed' };
608
731
  const newPaneId = paneId(split.stdout);
732
+ if (newPaneId && !(await tmuxPaneExists(tmuxBin, newPaneId))) return { ok: false, session, panes: created, stderr: `tmux split-window returned pane ${newPaneId}, but the pane was not present after creation` };
609
733
  created.push({ pane_id: newPaneId, role: pane.role || 'lane', title: pane.title || null });
610
- if (rightSidePanes && newPaneId) rightStackTarget = newPaneId;
611
- await tmuxRun(tmuxBin, ['select-layout', '-t', session, layout]).catch(() => null);
734
+ if (rightSidePanes && !rightStackRootPaneId && newPaneId) rightStackRootPaneId = newPaneId;
735
+ if (!rightSidePanes) await tmuxRun(tmuxBin, ['select-layout', '-t', session, layout]).catch(() => null);
612
736
  }
613
- const dynamic_resize = await enableTmuxDynamicResize(tmuxBin, session, { layout });
614
- return { ok: true, reused: false, session, panes: created, attach_command: `tmux attach-session -t ${session}`, layout, initial_size: dimensions, dynamic_resize };
737
+ const stable_layout = rightSidePanes ? await applyStableTeamLayout(tmuxBin, session, created[0].pane_id, { layout }) : null;
738
+ const dynamic_resize = await enableTmuxDynamicResize(tmuxBin, session, { layout, stableTeamLayout: rightSidePanes });
739
+ return { ok: true, reused: false, session, panes: created, attach_command: `tmux attach-session -t ${session}`, layout, initial_size: dimensions, stable_layout, dynamic_resize };
615
740
  }
616
741
 
617
742
  export async function launchTmuxUi(args = [], opts = {}) {
@@ -625,11 +750,16 @@ export async function launchTmuxUi(args = [], opts = {}) {
625
750
  return { plan };
626
751
  }
627
752
  if (args.includes('--status-only')) return { plan };
753
+ const codexLbSweep = (opts.codexLbFreshSession || opts.codexLbBypassed)
754
+ ? await sweepCodexLbTmuxSessions({ root: plan.root, keepSession: plan.session }).catch((err) => ({ ok: false, skipped: true, reason: err.message || 'codex-lb tmux cleanup failed', closed_session_count: 0 }))
755
+ : null;
628
756
  const command = codexLaunchCommand(plan.root, plan.codex.bin, plan.codexArgs);
629
757
  const created = await createTmuxSession({ ...plan, command }, [{ cwd: plan.root, command, focused: true, role: 'codex', title: 'Codex CLI' }]);
630
- if (created.ok) await writeTmuxSessionRecord(plan.root, { session: created.session, attach_command: created.attach_command, panes: created.panes }).catch(() => null);
758
+ if (created.ok) await writeTmuxSessionRecord(plan.root, { session: created.session, attach_command: created.attach_command, panes: created.panes, mode: opts.codexLbBypassed ? 'codex_lb_bypassed_openai_session' : opts.codexLbFreshSession ? 'codex_lb_fresh_session' : 'codex_session' }).catch(() => null);
631
759
  if (!args.includes('--quiet')) {
632
760
  console.log(`SKS tmux session: ${created.session || plan.session}`);
761
+ if (codexLbSweep?.closed_session_count) console.log(`codex-lb tmux cleanup: closed ${codexLbSweep.closed_session_count} stale detached session(s)`);
762
+ else if (codexLbSweep && !codexLbSweep.ok && !codexLbSweep.skipped) console.log(`codex-lb tmux cleanup: skipped (${codexLbSweep.reason || 'tmux cleanup failed'})`);
633
763
  if (created.ok && created.reused) console.log('tmux: reused existing session');
634
764
  else if (created.ok) console.log(`tmux: created ${created.panes.length} pane(s)`);
635
765
  else console.log(`tmux: not created (${created.stderr || 'tmux failed'})`);
@@ -644,7 +774,7 @@ export async function launchTmuxUi(args = [], opts = {}) {
644
774
  process.exitCode = attached.status || 1;
645
775
  }
646
776
  }
647
- return { plan, created: Boolean(created.ok), session: created.session || plan.session, opened: created, attached };
777
+ return { plan, created: Boolean(created.ok), session: created.session || plan.session, opened: created, attached, codex_lb_cleanup: codexLbSweep };
648
778
  }
649
779
 
650
780
  export async function launchMadTmuxUi(args = [], opts = {}) {
@@ -658,15 +788,20 @@ export async function launchMadTmuxUi(args = [], opts = {}) {
658
788
  return { plan };
659
789
  }
660
790
  if (args.includes('--status-only')) return { plan };
791
+ const codexLbSweep = (opts.codexLbFreshSession || opts.codexLbBypassed)
792
+ ? await sweepCodexLbTmuxSessions({ root: plan.root, keepSession: plan.session }).catch((err) => ({ ok: false, skipped: true, reason: err.message || 'codex-lb tmux cleanup failed', closed_session_count: 0 }))
793
+ : null;
661
794
  const missionId = opts.missionId || opts.madMissionId || 'latest';
662
795
  const mainCommand = codexLaunchCommand(plan.root, plan.codex.bin, plan.codexArgs);
663
796
  const panes = [
664
797
  { cwd: plan.root, command: mainCommand, focused: true, role: 'codex', title: 'Codex CLI' }
665
798
  ];
666
799
  const created = await createTmuxSession({ ...plan, command: mainCommand }, panes, { recreate: true });
667
- if (created.ok) await writeTmuxSessionRecord(plan.root, { session: created.session, attach_command: created.attach_command, panes: created.panes, mode: 'mad_session', mission_id: missionId }).catch(() => null);
800
+ if (created.ok) await writeTmuxSessionRecord(plan.root, { session: created.session, attach_command: created.attach_command, panes: created.panes, mode: opts.codexLbBypassed ? 'mad_codex_lb_bypassed_openai_session' : opts.codexLbFreshSession ? 'mad_codex_lb_fresh_session' : 'mad_session', mission_id: missionId }).catch(() => null);
668
801
  if (!args.includes('--quiet')) {
669
802
  console.log(`SKS MAD tmux session: ${created.session || plan.session}`);
803
+ if (codexLbSweep?.closed_session_count) console.log(`codex-lb tmux cleanup: closed ${codexLbSweep.closed_session_count} stale detached session(s)`);
804
+ else if (codexLbSweep && !codexLbSweep.ok && !codexLbSweep.skipped) console.log(`codex-lb tmux cleanup: skipped (${codexLbSweep.reason || 'tmux cleanup failed'})`);
670
805
  if (created.ok) console.log(`tmux: opened ${created.panes.length} pane(s)`);
671
806
  else console.log(`tmux: not created (${created.stderr || 'tmux failed'})`);
672
807
  if (created.ok) console.log(`Attach: ${created.attach_command}`);
@@ -680,7 +815,7 @@ export async function launchMadTmuxUi(args = [], opts = {}) {
680
815
  process.exitCode = attached.status || 1;
681
816
  }
682
817
  }
683
- return { plan, created: Boolean(created.ok), session: created.session || plan.session, opened: created, attached, mode: 'mad_session', mission_id: missionId };
818
+ return { plan, created: Boolean(created.ok), session: created.session || plan.session, opened: created, attached, mode: 'mad_session', mission_id: missionId, codex_lb_cleanup: codexLbSweep };
684
819
  }
685
820
 
686
821
  function printTmuxLaunchBlocked(plan, opts = {}) {
@@ -754,34 +889,53 @@ export async function reconcileTmuxTeamCockpit({ root, missionId, plan = {}, pro
754
889
  const previousCockpit = cockpitState?.missions?.[id] || {};
755
890
  const currentPane = paneList.panes.find((pane) => pane.pane_id === target.pane_id);
756
891
  const mainPaneId = previousCockpit.main_pane_id || (currentPane?.managed && currentPane?.mission_id === id ? null : target.pane_id);
757
- const managed = paneList.panes.filter((pane) => pane.managed && pane.mission_id === id);
758
- const byAgent = new Map();
759
- for (const pane of managed) {
760
- if (pane.agent && !byAgent.has(pane.agent)) byAgent.set(pane.agent, pane);
761
- }
892
+ const allManaged = paneList.panes.filter((pane) => pane.managed);
893
+ const managed = allManaged.filter((pane) => pane.mission_id === id);
894
+ const staleManaged = allManaged.filter((pane) => pane.mission_id && pane.mission_id !== id);
762
895
  const opened = [];
763
896
  const closed = [];
764
897
  const failed = [];
898
+ const closePane = async (pane, reason) => {
899
+ const kill = await tmuxRun(tmuxBin, ['kill-pane', '-t', pane.pane_id], { timeoutMs: 5000 });
900
+ if (kill.code === 0) closed.push({ pane_id: pane.pane_id, agent: pane.agent, role: pane.role, mission_id: pane.mission_id, reason });
901
+ else failed.push({ action: 'kill-pane', pane_id: pane.pane_id, agent: pane.agent, mission_id: pane.mission_id, reason, stderr: kill.stderr || kill.stdout || 'tmux kill-pane failed' });
902
+ };
903
+ for (const pane of staleManaged) {
904
+ await closePane(pane, 'stale_mission');
905
+ }
906
+ const keptAgents = new Set();
765
907
  for (const pane of managed) {
766
- if (!desiredAgents.has(pane.agent)) {
767
- const kill = await tmuxRun(tmuxBin, ['kill-pane', '-t', pane.pane_id], { timeoutMs: 5000 });
768
- if (kill.code === 0) closed.push({ pane_id: pane.pane_id, agent: pane.agent, role: pane.role });
769
- else failed.push({ action: 'kill-pane', pane_id: pane.pane_id, agent: pane.agent, stderr: kill.stderr || kill.stdout || 'tmux kill-pane failed' });
908
+ const desired = desiredAgents.has(pane.agent);
909
+ const duplicate = Boolean(pane.agent && keptAgents.has(pane.agent));
910
+ if (desired && !duplicate) {
911
+ keptAgents.add(pane.agent);
912
+ } else {
913
+ await closePane(pane, duplicate ? 'duplicate_agent_lane' : 'not_desired');
770
914
  }
771
915
  }
772
- const remainingManaged = managed.filter((pane) => desiredAgents.has(pane.agent) && !closed.some((entry) => entry.pane_id === pane.pane_id));
773
- let rightStackTarget = remainingManaged.at(-1)?.pane_id || mainPaneId || target.window_id;
916
+ const closedIds = new Set(closed.map((entry) => entry.pane_id));
917
+ const remainingManaged = managed.filter((pane) => desiredAgents.has(pane.agent) && !closedIds.has(pane.pane_id));
918
+ const byAgent = new Map();
919
+ for (const pane of remainingManaged) {
920
+ if (pane.agent && !byAgent.has(pane.agent)) byAgent.set(pane.agent, pane);
921
+ }
922
+ let rightStackRootPaneId = remainingManaged[0]?.pane_id || null;
774
923
  for (const lane of lanes) {
775
924
  if (byAgent.has(lane.agent)) continue;
776
925
  const firstRightPane = remainingManaged.length === 0 && opened.length === 0;
777
926
  const direction = firstRightPane ? '-h' : '-v';
778
- const split = await tmuxRun(tmuxBin, ['split-window', direction, '-t', rightStackTarget, '-d', '-P', '-F', '#{pane_id}', '-c', resolvedRoot, lane.command || 'pwd'], { timeoutMs: 5000, maxOutputBytes: 4096 });
927
+ const splitTarget = firstRightPane ? (mainPaneId || target.window_id) : (rightStackRootPaneId || mainPaneId || target.window_id);
928
+ const split = await tmuxRun(tmuxBin, ['split-window', direction, '-t', splitTarget, '-d', '-P', '-F', '#{pane_id}', '-c', resolvedRoot, lane.command || 'pwd'], { timeoutMs: 5000, maxOutputBytes: 4096 });
779
929
  const pane_id = paneId(split.stdout);
780
930
  if (split.code !== 0 || !pane_id) {
781
931
  failed.push({ action: 'split-window', agent: lane.agent, role: lane.role, stderr: split.stderr || split.stdout || 'tmux split-window failed' });
782
932
  continue;
783
933
  }
784
- rightStackTarget = pane_id;
934
+ if (!(await tmuxPaneExists(tmuxBin, pane_id))) {
935
+ failed.push({ action: 'verify-pane', pane_id, agent: lane.agent, role: lane.role, stderr: 'tmux split-window returned a pane id, but the pane was not present after creation' });
936
+ continue;
937
+ }
938
+ if (!rightStackRootPaneId) rightStackRootPaneId = pane_id;
785
939
  const optionResult = await setTmuxPaneUserOptions(tmuxBin, pane_id, {
786
940
  '@sks_team_managed': '1',
787
941
  '@sks_mission_id': id,
@@ -793,13 +947,10 @@ export async function reconcileTmuxTeamCockpit({ root, missionId, plan = {}, pro
793
947
  }
794
948
  let relayout = null;
795
949
  if (opened.length || closed.length) {
796
- const selectedMain = mainPaneId ? await tmuxRun(tmuxBin, ['select-pane', '-t', mainPaneId], { timeoutMs: 5000 }) : { code: 0 };
797
- const tiled = await tmuxRun(tmuxBin, ['select-layout', '-t', target.window_id, DYNAMIC_TEAM_TMUX_LAYOUT], { timeoutMs: 5000 });
798
- const even = await tmuxRun(tmuxBin, ['select-layout', '-t', target.window_id, '-E'], { timeoutMs: 5000 });
799
- relayout = { ok: selectedMain.code === 0 && tiled.code === 0 && even.code === 0, selected_main: selectedMain.code, layout: tiled.code, even: even.code, layout_name: DYNAMIC_TEAM_TMUX_LAYOUT };
950
+ relayout = await applyStableTeamLayout(tmuxBin, target.window_id, mainPaneId, { layout: DYNAMIC_TEAM_TMUX_LAYOUT });
800
951
  }
801
952
  const nextPanes = [
802
- ...managed.filter((pane) => desiredAgents.has(pane.agent) && !closed.some((entry) => entry.pane_id === pane.pane_id)),
953
+ ...remainingManaged,
803
954
  ...opened
804
955
  ];
805
956
  await writeTmuxCockpitRecord(resolvedRoot, {
@@ -835,6 +986,92 @@ export async function reconcileTmuxTeamCockpit({ root, missionId, plan = {}, pro
835
986
  };
836
987
  }
837
988
 
989
+ export async function sweepTmuxTeamSurfaces({ root, keepMissionId = null, env = process.env, tmux = null, dryRun = false } = {}) {
990
+ const resolvedRoot = path.resolve(root || await sksRoot());
991
+ const tmuxBin = tmux?.bin || await findTmuxBin() || 'tmux';
992
+ const hints = await readRecordedTeamSurfaceHints(resolvedRoot);
993
+ if (!hints.missionIds.size && !hints.sessions.size && !hints.paneKeys.size) {
994
+ return { ok: true, skipped: true, reason: 'no recorded SKS Team tmux surfaces for this repo', closed_lane_count: 0, targets: [] };
995
+ }
996
+ const paneList = await listAllTmuxPanes(tmuxBin);
997
+ if (!paneList.ok) return { ok: false, skipped: true, reason: paneList.stderr, closed_lane_count: 0, targets: [] };
998
+ const current = await currentTmuxTarget(tmuxBin, env).catch(() => ({ ok: false }));
999
+ const keepMissionIds = new Set(Array.isArray(keepMissionId) ? keepMissionId.filter(Boolean).map(String) : (keepMissionId ? [String(keepMissionId)] : []));
1000
+ const targets = paneList.panes
1001
+ .filter((pane) => shouldSweepTeamPane(pane, hints, { currentPaneId: current?.pane_id || '', keepMissionIds }))
1002
+ .filter((pane, index, panes) => panes.findIndex((candidate) => candidate.pane_id === pane.pane_id) === index);
1003
+ const closed = [];
1004
+ const failed = [];
1005
+ for (const pane of targets) {
1006
+ if (dryRun) {
1007
+ closed.push({ pane_id: pane.pane_id, session: pane.session, window_id: pane.window_id, agent: pane.agent, role: pane.role, mission_id: pane.mission_id, title: pane.title, dry_run: true });
1008
+ continue;
1009
+ }
1010
+ const kill = await tmuxRun(tmuxBin, ['kill-pane', '-t', pane.pane_id], { timeoutMs: 5000 });
1011
+ if (kill.code === 0) closed.push({ pane_id: pane.pane_id, session: pane.session, window_id: pane.window_id, agent: pane.agent, role: pane.role, mission_id: pane.mission_id, title: pane.title });
1012
+ else failed.push({ pane_id: pane.pane_id, session: pane.session, window_id: pane.window_id, agent: pane.agent, mission_id: pane.mission_id, title: pane.title, stderr: kill.stderr || kill.stdout || 'tmux kill-pane failed' });
1013
+ }
1014
+ if (!dryRun && closed.length) {
1015
+ for (const windowId of Array.from(new Set(closed.map((pane) => pane.window_id).filter(Boolean)))) {
1016
+ await tmuxRun(tmuxBin, ['select-layout', '-t', windowId, DYNAMIC_TEAM_TMUX_LAYOUT], { timeoutMs: 5000 }).catch(() => null);
1017
+ await tmuxRun(tmuxBin, ['select-layout', '-t', windowId, '-E'], { timeoutMs: 5000 }).catch(() => null);
1018
+ }
1019
+ }
1020
+ return {
1021
+ ok: failed.length === 0,
1022
+ skipped: false,
1023
+ root: resolvedRoot,
1024
+ keep_mission_id: keepMissionIds.size ? Array.from(keepMissionIds) : null,
1025
+ scanned_pane_count: paneList.panes.length,
1026
+ target_count: targets.length,
1027
+ closed_lane_count: closed.length,
1028
+ closed,
1029
+ failed,
1030
+ reason: closed.length
1031
+ ? 'swept stale recorded SKS Team tmux panes for this repo.'
1032
+ : 'no stale recorded SKS Team tmux panes found for this repo.'
1033
+ };
1034
+ }
1035
+
1036
+ export async function sweepCodexLbTmuxSessions({ root, keepSession = null, env = process.env, tmux = null, dryRun = false } = {}) {
1037
+ const resolvedRoot = path.resolve(root || await sksRoot());
1038
+ const tmuxBin = tmux?.bin || await findTmuxBin() || 'tmux';
1039
+ const sessionList = await listAllTmuxSessions(tmuxBin);
1040
+ if (!sessionList.ok) return { ok: false, skipped: true, reason: sessionList.stderr, closed_session_count: 0, targets: [] };
1041
+ const current = await currentTmuxTarget(tmuxBin, env).catch(() => ({ ok: false }));
1042
+ const suffix = `-${defaultTmuxSessionName(resolvedRoot)}`;
1043
+ const keep = new Set([keepSession, current?.session].filter(Boolean).map((entry) => sanitizeTmuxSessionName(entry)));
1044
+ const targets = sessionList.sessions
1045
+ .filter((entry) => entry.session.startsWith('sks-codex-lb-') && entry.session.endsWith(suffix))
1046
+ .filter((entry) => !entry.attached && !keep.has(entry.session))
1047
+ .sort((a, b) => (a.created_at || 0) - (b.created_at || 0));
1048
+ const closed = [];
1049
+ const failed = [];
1050
+ for (const target of targets) {
1051
+ if (dryRun) {
1052
+ closed.push({ session: target.session, dry_run: true, created_at: target.created_at, last_attached_at: target.last_attached_at });
1053
+ continue;
1054
+ }
1055
+ const kill = await tmuxRun(tmuxBin, ['kill-session', '-t', target.session], { timeoutMs: 5000 });
1056
+ if (kill.code === 0) closed.push({ session: target.session, created_at: target.created_at, last_attached_at: target.last_attached_at });
1057
+ else failed.push({ session: target.session, stderr: kill.stderr || kill.stdout || 'tmux kill-session failed' });
1058
+ }
1059
+ return {
1060
+ ok: failed.length === 0,
1061
+ skipped: false,
1062
+ root: resolvedRoot,
1063
+ keep_session: keepSession || null,
1064
+ scanned_session_count: sessionList.sessions.length,
1065
+ target_count: targets.length,
1066
+ closed_session_count: closed.length,
1067
+ closed,
1068
+ failed,
1069
+ reason: closed.length
1070
+ ? 'swept detached codex-lb tmux sessions for this repo.'
1071
+ : 'no stale detached codex-lb tmux sessions found for this repo.'
1072
+ };
1073
+ }
1074
+
838
1075
  export async function launchTmuxTeamView({ root, missionId, plan = {}, promptFile = null, json = false, attach = false, args = [] } = {}) {
839
1076
  const launch = await buildTmuxLaunchPlan({ root, session: `sks-team-${missionId}` });
840
1077
  const missionDir = path.join(launch.root, '.sneakoscope', 'missions', missionId);
@@ -878,6 +1115,7 @@ export async function launchTmuxTeamView({ root, missionId, plan = {}, promptFil
878
1115
  attach_command: launch.attach_command
879
1116
  };
880
1117
  if (json || !launch.ready) return result;
1118
+ result.preflight_cleanup = await sweepTmuxTeamSurfaces({ root: launch.root, keepMissionId: missionId }).catch((err) => ({ ok: false, skipped: true, reason: err.message || 'tmux preflight sweep failed', closed_lane_count: 0 }));
881
1119
  const wantsSeparateSession = args.includes('--separate-session') || args.includes('--new-session') || args.includes('--legacy-team-session') || args.includes('--no-dynamic-team-tmux');
882
1120
  if (!wantsSeparateSession) {
883
1121
  const cockpit = await reconcileTmuxTeamCockpit({ root: launch.root, missionId, plan, promptFile, dashboard, control, plannedFallback: true });
@@ -1026,14 +1264,16 @@ export async function cleanupTmuxTeamView({ root, missionId = 'latest', closeSes
1026
1264
  const record = await readTmuxTeamRecord(resolvedRoot, missionId);
1027
1265
  if (!record?.session) {
1028
1266
  const legacy = await cleanupLegacyTmuxTeamSurfaces(resolvedRoot, missionId, { closeSession }).catch((err) => ({ ok: false, skipped: true, reason: err.message || 'legacy tmux cleanup failed' }));
1267
+ const sweep = await sweepTmuxTeamSurfaces({ root: resolvedRoot }).catch((err) => ({ ok: false, skipped: true, reason: err.message || 'tmux sweep failed', closed_lane_count: 0 }));
1029
1268
  return {
1030
- ok: legacy.ok,
1031
- skipped: legacy.closed_lane_count === 0 && !legacy.killed_session,
1269
+ ok: legacy.ok && sweep.ok !== false,
1270
+ skipped: legacy.closed_lane_count === 0 && !legacy.killed_session && !sweep.closed_lane_count,
1032
1271
  reason: legacy.reason,
1033
1272
  mission_id: missionId,
1034
1273
  legacy_cleanup: legacy,
1035
- requested_close_surfaces: legacy.requested_close_surfaces || 0,
1036
- closed_surfaces: legacy.closed_lane_count || (legacy.killed_session ? 1 : 0)
1274
+ sweep_cleanup: sweep,
1275
+ requested_close_surfaces: (legacy.requested_close_surfaces || 0) + (sweep.target_count || 0),
1276
+ closed_surfaces: (legacy.closed_lane_count || (legacy.killed_session ? 1 : 0)) + (sweep.closed_lane_count || 0)
1037
1277
  };
1038
1278
  }
1039
1279
  const dynamicCleanup = await reconcileTmuxTeamCockpit({ root: resolvedRoot, missionId: record.mission_id || missionId, close: true }).catch((err) => ({ ok: false, skipped: true, reason: err.message || 'dynamic tmux cleanup failed' }));
@@ -1043,6 +1283,7 @@ export async function cleanupTmuxTeamView({ root, missionId = 'latest', closeSes
1043
1283
  const legacyCleanup = (dynamicCleanup?.closed_lane_count || recordedCleanup?.closed_lane_count)
1044
1284
  ? null
1045
1285
  : await cleanupLegacyTmuxTeamSurfaces(resolvedRoot, record.mission_id || missionId, { closeSession: false }).catch((err) => ({ ok: false, skipped: true, reason: err.message || 'legacy tmux cleanup failed' }));
1286
+ const sweepCleanup = await sweepTmuxTeamSurfaces({ root: resolvedRoot }).catch((err) => ({ ok: false, skipped: true, reason: err.message || 'tmux sweep failed', closed_lane_count: 0 }));
1046
1287
  let killed_session = false;
1047
1288
  if ((closeSession || closeSession === true) && record.mode !== 'current_session_dynamic_panes') {
1048
1289
  const tmuxBin = await findTmuxBin() || 'tmux';
@@ -1060,8 +1301,9 @@ export async function cleanupTmuxTeamView({ root, missionId = 'latest', closeSes
1060
1301
  dynamic_cleanup: dynamicCleanup,
1061
1302
  recorded_cleanup: recordedCleanup,
1062
1303
  legacy_cleanup: legacyCleanup,
1063
- requested_close_surfaces: closeSession ? 1 : (dynamicCleanup?.closed_lane_count || recordedCleanup?.closed_lane_count || legacyCleanup?.requested_close_surfaces || 0),
1064
- closed_surfaces: killed_session ? 1 : (dynamicCleanup?.closed_lane_count || recordedCleanup?.closed_lane_count || legacyCleanup?.closed_lane_count || 0),
1304
+ sweep_cleanup: sweepCleanup,
1305
+ requested_close_surfaces: (closeSession ? 1 : (dynamicCleanup?.closed_lane_count || recordedCleanup?.closed_lane_count || legacyCleanup?.requested_close_surfaces || 0)) + (sweepCleanup?.target_count || 0),
1306
+ closed_surfaces: (killed_session ? 1 : (dynamicCleanup?.closed_lane_count || recordedCleanup?.closed_lane_count || legacyCleanup?.closed_lane_count || 0)) + (sweepCleanup?.closed_lane_count || 0),
1065
1307
  reason: dynamicCleanup?.ok
1066
1308
  ? 'cleanup closed managed Team panes in the current SKS tmux session.'
1067
1309
  : recordedCleanup?.ok