@@ -71,6 +71,10 @@ if [[ -n "$CODEX_FLEET_TMUX_SOCKET" ]]; then
7171fi
7272WAKE=" ${WAKE:-/ tmp/ codex-fleet-wake-prompt.md} "
7373N_PANES=8
74+ N_PANES_EXPLICIT=0
75+ # Hard ceiling for auto-grown N_PANES so a 100-account user doesn't
76+ # accidentally spawn 100 panes. Override via env if you really do want more.
77+ N_PANES_AUTO_MAX=" ${CODEX_FLEET_N_PANES_AUTO_MAX:- 24} "
7478ATTACH=1
7579PLAN_SLUG=" "
7680FLEET_ID=" ${FLEET_ID:- } "
@@ -80,7 +84,7 @@ NO_CAP_CACHE=0
8084while [ $# -gt 0 ]; do
8185 case " $1 " in
8286 --plan-slug) PLAN_SLUG=" $2 " ; shift 2 ;;
83- --n) N_PANES=" $2 " ; shift 2 ;;
87+ --n) N_PANES=" $2 " ; N_PANES_EXPLICIT=1 ; shift 2 ;;
8488 --no-attach) ATTACH=0; shift ;;
8589 --fleet-id) FLEET_ID=" $2 " ; shift 2 ;;
8690 --auto-fleet-id) AUTO_FLEET_ID=1; shift ;;
@@ -102,6 +106,12 @@ preflight_warn() { warn "preflight: $*"; }
102106# shellcheck source=lib/mcp-preflight.sh
103107. " $SCRIPT_DIR /lib/mcp-preflight.sh"
104108
109+ # Ensure the Colony worker daemon (embeddings + viewer + MCP context store)
110+ # is running so every fleet worker can read/write task context from boot.
111+ # Idempotent + non-fatal: if Colony is unhealthy the fleet still spawns and
112+ # falls back to shell-CLI calls, matching the worker-prompt's fallback path.
113+ fleet_ensure_colony_running
114+
105115FLEET_CONFIG_TMPL=" ${CODEX_FLEET_CONFIG_TMPL:- $SCRIPT_DIR / fleet-config.toml.tmpl} "
106116
107117cd " $REPO "
414424# (a) Score by agent-auth's 5h% * weekly% (fast, but unreliable for codex
415425# CLI's own rolling cap).
416426# (b) Probe each candidate with `codex exec` to detect the *real* cap
417- # state. Take top 3N candidates so we can skip up to 2N capped
418- # accounts and still end up with N healthy ones.
419- TOP=$(( N_PANES * 3 ))
420- log " picking $TOP candidate accounts (will probe + filter down to $N_PANES healthy)"
421- CANDIDATES=$( agent-auth list 2> /dev/null | N=" $TOP " python3 -c '
427+ # state. We probe EVERY account that has any meaningful budget left —
428+ # cap-probe.sh has its own per-email cache so re-probing 30+ accounts
429+ # on a warm cache is cheap. Skipping the *3 cap is what lets the
430+ # bringup actually use all 30+ accounts when most are partially used.
431+ # Override via CODEX_FLEET_TOP_CANDIDATES (default 0 = no cap, probe all
432+ # that pass the minimum-budget filter). Floors are deliberately permissive
433+ # (5/5) — anything with a non-trivial budget gets probed; the live
434+ # `codex exec` ping is the source of truth for "actually usable".
435+ TOP=" ${CODEX_FLEET_TOP_CANDIDATES:- 0} "
436+ MIN_5H=" ${CODEX_FLEET_MIN_5H_PCT:- 5} "
437+ MIN_WEEKLY=" ${CODEX_FLEET_MIN_WEEKLY_PCT:- 5} "
438+ if [ " $TOP " = " 0" ]; then
439+ log " picking ALL candidate accounts with 5h>=${MIN_5H} %, weekly>=${MIN_WEEKLY} % (will probe + spawn min(healthy, $N_PANES ) workers)"
440+ else
441+ log " picking top $TOP candidate accounts with 5h>=${MIN_5H} %, weekly>=${MIN_WEEKLY} % (override via CODEX_FLEET_TOP_CANDIDATES)"
442+ fi
443+ CANDIDATES=$( agent-auth list 2> /dev/null | N=" $TOP " MIN_H=" $MIN_5H " MIN_W=" $MIN_WEEKLY " python3 -c '
422444import os, sys, re
423- n = int(os.environ["N"])
445+ n = int(os.environ["N"]) # 0 means "no cap, return all"
446+ mh = int(os.environ["MIN_H"])
447+ mw = int(os.environ["MIN_W"])
424448rows = []
425449for line in sys.stdin:
426450 em = re.search(r"([\w.+-]+@[\w.-]+\.[a-z]+)", line)
@@ -429,13 +453,14 @@ for line in sys.stdin:
429453 h5m = re.search(r"5h=(\d+)%", line); wkm = re.search(r"weekly=(\d+)%", line)
430454 if not h5m or not wkm: continue
431455 h, w = int(h5m.group(1)), int(wkm.group(1))
432- if h < 40 or w < 25 : continue
456+ if h < mh or w < mw : continue
433457 rows.append((h*w, email))
434458rows.sort(reverse=True)
435- for _, email in rows[:n]:
459+ out = rows if n == 0 else rows[:n]
460+ for _, email in out:
436461 print(email)
437462' )
438- [ -n " $CANDIDATES " ] || die " no candidate accounts found (need 5h>=40 %, wk>=25 % in agent-auth list)"
463+ [ -n " $CANDIDATES " ] || die " no candidate accounts found (need 5h>=${MIN_5H} %, wk>=${MIN_WEEKLY} % in agent-auth list — relax via CODEX_FLEET_MIN_5H_PCT / CODEX_FLEET_MIN_WEEKLY_PCT )"
439464CAND_N=$( printf " %s\n" " $CANDIDATES " | wc -l)
440465log " ranked $CAND_N candidates by agent-auth score; running live probe..."
441466
@@ -473,6 +498,25 @@ if [ "$cap_cache_hit" = "0" ]; then
473498 HEALTHY_EMAILS=$( bash " $SCRIPT_DIR /cap-probe.sh" " $N_PANES " $CANDIDATES 2> /tmp/cap-probe.err) || true
474499fi
475500HEALTHY_N=$( printf " %s\n" " $HEALTHY_EMAILS " | grep -c " @" || true)
501+
502+ # Auto-grow N_PANES to match what the probe actually found, capped by
503+ # CODEX_FLEET_N_PANES_AUTO_MAX (default 24). Triggered only when the
504+ # operator did NOT pass --n explicitly — explicit --n always wins. Without
505+ # this, the bringup historically clamped at the default --n=8 and left
506+ # extra healthy accounts idle in cache instead of spawning them.
507+ if [ " $N_PANES_EXPLICIT " = " 0" ] && [ " $HEALTHY_N " -gt " $N_PANES " ]; then
508+ prev_n=" $N_PANES "
509+ N_PANES=" $HEALTHY_N "
510+ if [ " $N_PANES " -gt " $N_PANES_AUTO_MAX " ]; then
511+ N_PANES=" $N_PANES_AUTO_MAX "
512+ fi
513+ log " auto-grow N_PANES: $prev_n → $N_PANES (HEALTHY_N=$HEALTHY_N , cap=$N_PANES_AUTO_MAX ). Override with --n or CODEX_FLEET_N_PANES_AUTO_MAX."
514+ # Truncate HEALTHY_EMAILS to N_PANES so downstream stage/spawn loops
515+ # don't try to spin more workers than the layout produces.
516+ HEALTHY_EMAILS=$( printf " %s\n" " $HEALTHY_EMAILS " | head -n " $N_PANES " )
517+ HEALTHY_N=$( printf " %s\n" " $HEALTHY_EMAILS " | grep -c " @" || true)
518+ fi
519+
476520if [ " $HEALTHY_N " -lt " $N_PANES " ]; then
477521 warn " cap-probe found only $HEALTHY_N /$N_PANES healthy accounts"
478522 warn " $( cat /tmp/cap-probe.err 2> /dev/null) "
@@ -612,20 +656,27 @@ tmux set-option -w -t "$SESSION:overview" remain-on-exit on
612656# silently hiding the tab strip.
613657
614658# 8.5 + 9. Apply the typed overview layout. fleet-layout owns the pane
615- # topology now; full-bringup only decides operator policy (header rows,
616- # binary lookup, and worker spawn metadata).
659+ # topology now; full-bringup only decides operator policy (header rows
660+ # and worker spawn metadata).
661+ #
662+ # The legacy `fleet-tab-strip` Rust binary that used to draw an in-window
663+ # header pane at the top of overview was retired in PR #107. The tmux
664+ # status bar configured by style-tabs.sh (`status-position top`, iOS-style
665+ # pills, window-status-format with range=window markers) is now the single
666+ # canonical nav surface — visible on EVERY window, not just overview.
667+ #
668+ # Opting back into an in-window header pane via CODEX_FLEET_OVERVIEW_HEADER_ROWS
669+ # is still honoured for forward-compat, but defaults to 0 so the bringup
670+ # stops hunting for a binary that no longer exists (previous default of 1
671+ # fell into a warn branch on every fresh checkout → no header pane created
672+ # AND a stale "cargo build -p fleet-tab-strip" message printed for a crate
673+ # the workspace doesn't ship anymore).
617674HEADER_ROWS=" ${CODEX_FLEET_OVERVIEW_HEADER_ROWS:- 0} "
618675HEADER_PANE_ID=" "
619676HEADER_CMD=" "
620677if (( HEADER_ROWS > 0 )) ; then
621- STRIP_BIN=" $REPO /rust/target/release/fleet-tab-strip"
622- [ -x " $STRIP_BIN " ] || STRIP_BIN=" $REPO /rust/target/debug/fleet-tab-strip"
623- if [ -x " $STRIP_BIN " ]; then
624- HEADER_CMD=" env CODEX_FLEET_SESSION='$SESSION ' '$STRIP_BIN '"
625- else
626- warn " fleet-tab-strip not built — overview header skipped (run: cargo build --release -p fleet-tab-strip)"
627- HEADER_ROWS=0
628- fi
678+ warn " CODEX_FLEET_OVERVIEW_HEADER_ROWS=$HEADER_ROWS requested but fleet-tab-strip crate was retired (PR #107) — falling back to status-bar-only nav. Unset the env to silence."
679+ HEADER_ROWS=0
629680fi
630681
631682FLEET_APPLY_LAYOUT_BIN=" $REPO /rust/target/release/fleet-apply-layout"
@@ -674,6 +725,24 @@ else
674725 die " fleet-apply-layout failed: $layout_output "
675726fi
676727log " overview layout applied via fleet-apply-layout (workers=$N_PANES , header_rows=$HEADER_ROWS )"
728+
729+ # Default geometry: retile the 2-column preset into a uniform grid so every
730+ # pane has the same width/height. The Rust preset still owns *topology* (which
731+ # pane is the header, which are workers), we just override the geometry on top.
732+ # Operators who prefer the 2-column shape can set CODEX_FLEET_OVERVIEW_LAYOUT=preset.
733+ case " ${CODEX_FLEET_OVERVIEW_LAYOUT:- tiled} " in
734+ tiled)
735+ tmux select-layout -t " $SESSION :overview" tiled > /dev/null 2>&1 || true
736+ log " overview re-tiled into uniform grid (CODEX_FLEET_OVERVIEW_LAYOUT=tiled)"
737+ ;;
738+ preset|2col|two-column)
739+ log " overview keeping fleet-layout 2-column preset (CODEX_FLEET_OVERVIEW_LAYOUT=$CODEX_FLEET_OVERVIEW_LAYOUT )"
740+ ;;
741+ * )
742+ warn " unknown CODEX_FLEET_OVERVIEW_LAYOUT='$CODEX_FLEET_OVERVIEW_LAYOUT ' — keeping preset"
743+ ;;
744+ esac
745+
677746if (( HEADER_ROWS > 0 )) ; then
678747 HEADER_PANE_ID=" $( tmux list-panes -t " $SESSION :overview" -F ' #{@panel}|#{pane_id}' \
679748 | awk -F' |' ' $1 == "[codex-fleet-tab-strip]" { print $2; exit }' ) "
@@ -726,6 +795,36 @@ while IFS='|' read -r id email tier specialty; do
726795 i=$(( i + 1 ))
727796done <<< " $ACCOUNTS"
728797
798+ # 10.5 Add a 7-row "mascot strip" at the bottom of the overview window.
799+ #
800+ # Renders a small transparent Claude pixel-art + live fleet stats. The strip
801+ # pane is *added after* worker spawn so we don't disturb the
802+ # fleet-apply-layout topology — split-window -f -v -l 7 carves it from the
803+ # full-window height, then a final tiled retile gives us uniform workers + a
804+ # fixed-height strip. Operator opt-out: CODEX_FLEET_MASCOT_STRIP=0.
805+ #
806+ # Tuning via env: see ~/.local/bin/claude-mascot-strip --help-style comments
807+ # (CLAUDE_STRIP_IMG, CLAUDE_STRIP_INTERVAL, CLAUDE_STRIP_PLACE, …).
808+ if [ " ${CODEX_FLEET_MASCOT_STRIP:- 1} " = " 1" ] && [ -x " $HOME /.local/bin/claude-mascot-strip" ]; then
809+ # `-P -F '#{pane_id}'` makes split-window print the new pane's id directly,
810+ # which is reliable. The list-panes + awk approach we tried first looked at
811+ # `pane_current_command`, but a bash script exec'd from bash keeps the
812+ # command name as `bash`, so the match silently dropped.
813+ mascot_pid=$( tmux split-window -d -P -F ' #{pane_id}' -f -v -l 7 \
814+ -t " $SESSION :overview" \
815+ " exec $HOME /.local/bin/claude-mascot-strip" 2> /dev/null || true)
816+ if [ -n " $mascot_pid " ]; then
817+ tmux set-option -p -t " $mascot_pid " ' @panel' ' [mascot-strip]' > /dev/null 2>&1 || true
818+ tmux set-option -p -t " $mascot_pid " remain-on-exit on > /dev/null 2>&1 || true
819+ # Pin the strip height back to 7 — tmux's tiled re-distribution above us
820+ # would otherwise rescale everything uniformly.
821+ tmux resize-pane -t " $mascot_pid " -y 7 > /dev/null 2>&1 || true
822+ log " overview mascot strip spawned → $mascot_pid (CODEX_FLEET_MASCOT_STRIP=0 to disable)"
823+ else
824+ warn " overview mascot strip split-window did not return a pane id; strip skipped"
825+ fi
826+ fi
827+
729828# 11. Create fleet / plan / waves windows
730829log " creating fleet / plan / waves windows"
731830
@@ -998,12 +1097,28 @@ CODEX_FLEET_SESSION="$TICKER_SESSION" bash "$SCRIPT_DIR/style-tabs.sh" >/dev/nul
9981097# global, so any non-`off` global value means the chrome is in place.
9991098expected_h=" ${STYLE_TABS_HEIGHT:- 1} "
10001099chrome_status=$( tmux show-options -gv status 2> /dev/null || echo " " )
1100+ chrome_position=$( tmux show-options -gv status-position 2> /dev/null || echo " " )
10011101case " $chrome_status " in
10021102 ' ' |off|0)
1003- warn " iOS chrome looks wrong: global status='$chrome_status ' (expected on or ${expected_h} )"
1103+ # Auto-correct: re-assert the bare minimum (status on, top-docked) so
1104+ # the operator gets a visible nav header even if style-tabs.sh partially
1105+ # failed. Previously the chrome was just `warn`-ed and left hidden,
1106+ # which surfaces as "the navigation header is not visible by default".
1107+ warn " iOS chrome verify: global status='$chrome_status ' — forcing status=on, status-position=top"
1108+ tmux set-option -g status on > /dev/null 2>&1 || true
1109+ tmux set-option -g status-position top > /dev/null 2>&1 || true
1110+ tmux set-option -t " $SESSION " -u status > /dev/null 2>&1 || true
1111+ tmux set-option -t " $SESSION " -u status-position > /dev/null 2>&1 || true
1112+ tmux refresh-client -S > /dev/null 2>&1 || true
10041113 ;;
10051114 * )
1006- log " iOS chrome verified: status=$chrome_status (target ${expected_h} )"
1115+ if [ " $chrome_position " != " top" ]; then
1116+ warn " iOS chrome verify: status-position='$chrome_position ' — forcing top"
1117+ tmux set-option -g status-position top > /dev/null 2>&1 || true
1118+ tmux set-option -t " $SESSION " -u status-position > /dev/null 2>&1 || true
1119+ tmux refresh-client -S > /dev/null 2>&1 || true
1120+ fi
1121+ log " iOS chrome verified: status=$chrome_status position=${chrome_position:- top} (target on/${expected_h} , top)"
10071122 ;;
10081123esac
10091124
0 commit comments