From 8cfe3d79d1771981d8681c40ff4055bfbc5466db Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Sat, 16 May 2026 18:08:42 +0200 Subject: [PATCH] feat(codex-fleet): synthetic-data demo of 8-agent fleet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scripts/codex-fleet/demo/ — bring up the production tmux layout populated by fake plan / pane / counter / quality-score files so the real fleet-state / fleet-plan-tree / fleet-waves binaries render against synthetic data. No real codex sessions, no API spend. For design iteration: change a renderer, kill + restart the demo, see the result. Structure: - up.sh seeds /tmp/claude-viz/, spawns tmux session codex-fleet-demo with overview (8 worker panes) + fleet/plan/waves/watcher dashboard windows, starts tick simulator - down.sh tears tmux down, removes synthetic state, deletes runtime plan copy so working tree stays clean - tick.sh mutates the runtime plan every 3s (configurable via CODEX_FLEET_DEMO_TICK_INTERVAL): assign idle agents, advance claimed → in_progress → completed, refresh pane scrollback. Loops when all 12 tasks complete (set CODEX_FLEET_DEMO_LOOP=0 to stop) - agent-auth PATH shim so fleet-data::accounts::load_via_agent_auth() reads 8 synthetic accounts instead of the real binary - scenarios/refactor-wave/plan.json committed template; up.sh copies to openspec/plans// at runtime 8 agents named after herbs; clover is scripted to be "capped" so the PaneState::Capped branch is exercised. Plan has 12 tasks across 3 dependency waves modelled after the recent refactor PRs (#154-159). Usage: bash scripts/codex-fleet/demo/up.sh Prereqs: tmux, jq, release builds of fleet-state, fleet-plan-tree, fleet-waves (+ optionally fleet-watcher) under rust/target/release/. --- scripts/codex-fleet/demo/README.md | 121 +++++++++ scripts/codex-fleet/demo/agent-auth | 41 +++ scripts/codex-fleet/demo/down.sh | 43 +++ .../demo/scenarios/refactor-wave/plan.json | 222 ++++++++++++++++ scripts/codex-fleet/demo/tick.sh | 251 ++++++++++++++++++ scripts/codex-fleet/demo/up.sh | 215 +++++++++++++++ 6 files changed, 893 insertions(+) create mode 100644 scripts/codex-fleet/demo/README.md create mode 100755 scripts/codex-fleet/demo/agent-auth create mode 100755 scripts/codex-fleet/demo/down.sh create mode 100644 scripts/codex-fleet/demo/scenarios/refactor-wave/plan.json create mode 100755 scripts/codex-fleet/demo/tick.sh create mode 100755 scripts/codex-fleet/demo/up.sh diff --git a/scripts/codex-fleet/demo/README.md b/scripts/codex-fleet/demo/README.md new file mode 100644 index 0000000..4c73a30 --- /dev/null +++ b/scripts/codex-fleet/demo/README.md @@ -0,0 +1,121 @@ +# codex-fleet demo + +Bring up the production codex-fleet tmux layout driven by **synthetic data** +— no real codex sessions spawned, no API spend, no GitHub touched. The +existing `fleet-state`, `fleet-tab-strip`, `fleet-plan-tree`, and +`fleet-waves` binaries render against fake plan/pane/heartbeat files, so +what you see is exactly what the live fleet would look like with 8 agents +mid-flight on a refactor wave. + +Intended for design iteration: change a renderer, kill+restart the demo, +see the result without paying for codex API calls. + +## Run + +```bash +bash scripts/codex-fleet/demo/up.sh # bring up + auto-attach +bash scripts/codex-fleet/demo/up.sh --no-attach +bash scripts/codex-fleet/demo/up.sh --no-tick # static state, no animation +bash scripts/codex-fleet/demo/down.sh # tear down +``` + +Prereqs: `tmux`, `jq`, and release builds of `fleet-state`, `fleet-plan-tree`, +`fleet-waves` (plus optionally `fleet-watcher`) in `rust/target/release/` +(or debug fallback). Build with: + +```bash +cd rust && cargo build --release -p fleet-state -p fleet-plan-tree \ + -p fleet-waves -p fleet-watcher +``` + +## Layout + +Tmux session `codex-fleet-demo` on socket `codex-fleet-demo`: + +- **overview** — 8 worker panes in a 4×2 grid. Each pane displays + scripted scrollback the `fleet-data::scrape` parser will extract a + runtime + model + headline from. Pane `@panel` options are set to + `[codex-]` so `fleet-data::panes::list_panes` maps them to + accounts. +- **fleet** — `fleet-state` worker list (image G reference design). + Renders its own iOS tab strip inline (via `fleet_ui::tab_strip` reading + `fleet-tab-counters.json`); the standalone `fleet-tab-strip` binary was + removed by PR #107. +- **plan** — `fleet-plan-tree` topo levels view. +- **waves** — `fleet-waves` spawn-timeline view. +- **watcher** *(if built)* — `fleet-watcher` if the binary is available. + +## Scenario + +`scripts/codex-fleet/demo/scenarios/refactor-wave/plan.json` (committed +template) — 12 tasks in 3 dependency waves modelled after the refactor +PRs (#154–#159): toposort/scrape/tab_strip splits + CliConvention trait + +shell helper lib + dependent follow-ups + a final docs/smoke gate. 8 +agents (named after herbs) claim and complete tasks on a tick loop. + +`up.sh` copies the template into +`openspec/plans/demo-refactor-wave-2026-05-16/plan.json` so the +dashboards can discover it via their normal `openspec/plans//` +lookup. `tick.sh` mutates that runtime copy in place; `down.sh` removes +it, so the working tree stays clean across runs. + +The `tick.sh` simulator mutates `plan.json` in place every 3s +(configurable via `CODEX_FLEET_DEMO_TICK_INTERVAL`): + +1. Assign next ready task to each idle agent. +2. Bump runtime + rewrite pane scrollback (the workers' `tail -F` loops + pick this up). +3. After ~4s the task moves `claimed → in_progress`; after ~18s it + completes. +4. When all 12 tasks complete, loop back to wave 0 (set + `CODEX_FLEET_DEMO_LOOP=0` to stop instead). + +One agent (`clover`) is scripted to be "capped" when idle so the demo +exercises `PaneState::Capped` rendering. + +## Synthetic files written + +| Path | Owner | Purpose | +|------|-------|---------| +| `/tmp/claude-viz/fleet-tab-counters.json` | `up.sh` + `tick.sh` | `fleet-tab-strip` counter badges | +| `/tmp/claude-viz/fleet-quality-scores.json` | `up.sh` | `fleet-state` quality column | +| `/tmp/claude-viz/plan-tree-pin.txt` | `up.sh` | Pins plan-tree to the demo plan | +| `/tmp/claude-viz/demo-current-account` | `up.sh` | Marks the `*` row in agent-auth | +| `/tmp/claude-viz/demo-panes/.txt` | `tick.sh` | Per-pane fake scrollback (workers `tail -F` these) | +| `/tmp/claude-viz/demo-active` | `up.sh` | Sentinel; `tick.sh` exits when removed | +| `/tmp/claude-viz/demo-tick.pid` | `up.sh` | PID of background tick simulator | +| `openspec/plans/demo-refactor-wave-2026-05-16/plan.json` | committed | Plan fixture mutated in place by `tick.sh` | + +`down.sh` removes everything in the table except the committed plan +fixture. + +## Shim + +`scripts/codex-fleet/demo/agent-auth` is prepended to `$PATH` for the +binaries. `fleet-data::accounts::load_via_agent_auth()` calls +`agent-auth list` as a subprocess — the shim emits 8 synthetic rows that +match the real parser's format. + +## Extending + +- **New scenario:** add `openspec/plans//plan.json` and pass + `CODEX_FLEET_DEMO_PLAN_SLUG=` (TODO: wire this through + `up.sh` + `tick.sh` — currently hardcoded to `demo-refactor-wave-…`). +- **Different worker count:** change `WORKERS` + `AIDS` in `up.sh` and + `AIDS` in `tick.sh`. Tmux layout also needs adjustment past 8. +- **Slow down / speed up:** `CODEX_FLEET_DEMO_TICK_INTERVAL=1` (faster) + or `=10` (slower). + +## Caveats + +- The demo plan slug carries a date suffix so the "newest plan" picker in + `fleet-plan-tree` selects it on a clean repo. If you have other plans + with a newer date suffix, the `plan-tree-pin.txt` override kicks in. +- `fleet-watcher` is not wired into the demo yet — its data dependencies + overlap with `fleet-state` but it also looks at colony/auto-reviewer + state that isn't faked here. +- Workers' `current_command` shows `bash` (not `codex`), which + `panes::classify` would flag as `Dead`. The classifier checks for + "codex" in scrollback as an escape; the fixture text starts with + `codex 0.42.0 — …` so this branch evaluates correctly. If you see + Dead-state rendering, that's the cause. diff --git a/scripts/codex-fleet/demo/agent-auth b/scripts/codex-fleet/demo/agent-auth new file mode 100755 index 0000000..a118f91 --- /dev/null +++ b/scripts/codex-fleet/demo/agent-auth @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Demo shim: fake `agent-auth list` output for codex-fleet/demo/up.sh. +# Used via PATH override so fleet-data::accounts::load_via_agent_auth() reads +# this instead of the real binary. Eight synthetic accounts with varied +# 5h/weekly caps so the dashboard shows visual range. +# +# When invoked with a sub-command other than `list`, exit 0 silently — the +# real binary supports more verbs, but the dashboards only call `list`. +set -euo pipefail + +if [[ "${1:-}" != "list" ]]; then + exit 0 +fi + +# Allow the tick simulator to flip the "current" account by writing the +# email into this file. Defaults to admin-magnolia. +current_file="${CODEX_FLEET_DEMO_CURRENT:-/tmp/claude-viz/demo-current-account}" +current_email="" +if [[ -r "$current_file" ]]; then + current_email="$(head -1 "$current_file" | tr -d '[:space:]')" +fi + +print_row() { + local email="$1" five_h="$2" weekly="$3" + local marker=" " + if [[ "$email" == "$current_email" ]]; then + marker="*" + fi + printf "%s %-32s 5h=%s%% weekly=%s%%\n" "$marker" "$email" "$five_h" "$weekly" +} + +# 8 synthetic accounts. Caps tuned so the dashboards show variety: some +# heavily loaded, some fresh, one near the limit. +print_row admin-magnolia@example.dev 88 62 +print_row admin-sumac@example.dev 71 48 +print_row admin-yarrow@example.dev 59 35 +print_row admin-clover@example.dev 94 78 +print_row admin-thistle@example.dev 42 21 +print_row admin-fennel@example.dev 67 44 +print_row admin-mallow@example.dev 53 29 +print_row admin-borage@example.dev 81 56 diff --git a/scripts/codex-fleet/demo/down.sh b/scripts/codex-fleet/demo/down.sh new file mode 100755 index 0000000..72cd30a --- /dev/null +++ b/scripts/codex-fleet/demo/down.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Tear down the codex-fleet demo brought up by up.sh. Kills the tmux +# session, stops the tick simulator, removes synthetic state. Does NOT +# remove the openspec/plans/demo-refactor-wave-2026-05-16 fixture +# (that's committed to the repo). Safe to run repeatedly. +set -euo pipefail + +SOCKET="${CODEX_FLEET_DEMO_SOCKET:-codex-fleet-demo}" +SESSION="codex-fleet-demo" +STATE_DIR="/tmp/claude-viz" +DEMO_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd -- "$DEMO_DIR/../../.." && pwd)" +PLAN_SLUG="demo-refactor-wave-2026-05-16" +PLAN_RUNTIME="$REPO_ROOT/openspec/plans/$PLAN_SLUG/plan.json" + +# Tick simulator +if [[ -f "$STATE_DIR/demo-tick.pid" ]]; then + pid="$(cat "$STATE_DIR/demo-tick.pid")" + kill "$pid" 2>/dev/null || true + rm -f "$STATE_DIR/demo-tick.pid" +fi +pkill -f 'codex-fleet/demo/tick.sh' 2>/dev/null || true + +# Tmux session +tmux -L "$SOCKET" kill-session -t "$SESSION" 2>/dev/null || true +tmux -L "$SOCKET" kill-server 2>/dev/null || true + +# Synthetic state files +rm -f "$STATE_DIR/demo-active" \ + "$STATE_DIR/demo-current-account" \ + "$STATE_DIR/demo-tick.log" \ + "$STATE_DIR/fleet-tab-counters.json" \ + "$STATE_DIR/fleet-quality-scores.json" \ + "$STATE_DIR/plan-tree-pin.txt" +rm -rf "$STATE_DIR/demo-panes" + +# Runtime plan copy (template stays in scripts/codex-fleet/demo/scenarios/). +if [[ -f "$PLAN_RUNTIME" ]]; then + rm -f "$PLAN_RUNTIME" + rmdir "$(dirname "$PLAN_RUNTIME")" 2>/dev/null || true +fi + +echo "demo down." diff --git a/scripts/codex-fleet/demo/scenarios/refactor-wave/plan.json b/scripts/codex-fleet/demo/scenarios/refactor-wave/plan.json new file mode 100644 index 0000000..35ac8c7 --- /dev/null +++ b/scripts/codex-fleet/demo/scenarios/refactor-wave/plan.json @@ -0,0 +1,222 @@ +{ + "schema_version": 1, + "plan_slug": "demo-refactor-wave-2026-05-16", + "title": "DEMO — Synthetic refactor wave for 8-agent visualization", + "problem": "Fictional plan used by scripts/codex-fleet/demo/ to drive the fleet dashboards with synthetic data. NOT a real piece of work. 12 tasks across 3 dependency waves so the fleet-waves spawn timeline and fleet-plan-tree topo view both have something to render. 8 agents claim and complete tasks on a script; the tick simulator mutates this file in place to advance the animation.", + "acceptance_criteria": [ + "Demo only — no merge criteria." + ], + "roles": [ + "planner", + "architect", + "executor", + "writer", + "verifier" + ], + "tasks": [ + { + "subtask_index": 0, + "title": "Extract render_pill helper from tab_strip", + "description": "Pull the duplicated row-0 / row-1 pill loop in fleet-ui/src/tab_strip.rs into a single render_pill(frame, x, y, PillSpec) helper.", + "file_scope": [ + "rust/fleet-ui/src/tab_strip.rs" + ], + "depends_on": [], + "spec_row_id": null, + "capability_hint": "ui_work", + "status": "available", + "claimed_by_session_id": null, + "claimed_by_agent": null, + "completed_summary": null + }, + { + "subtask_index": 1, + "title": "Extract Kahn toposort to fleet-data", + "description": "Move the duplicated waves() function out of fleet-waves and fleet-plan-tree into a shared fleet-data::toposort module.", + "file_scope": [ + "rust/fleet-waves/src/main.rs", + "rust/fleet-plan-tree/src/main.rs", + "rust/fleet-data/src/toposort.rs" + ], + "depends_on": [], + "spec_row_id": null, + "capability_hint": "rust_work", + "status": "available", + "claimed_by_session_id": null, + "claimed_by_agent": null, + "completed_summary": null + }, + { + "subtask_index": 2, + "title": "Extract scrape_activity to fleet-data::scrape", + "description": "Move scrape_activity + PaneActivity out of fleet.rs into a dedicated scrape.rs module with per-field extractor helpers.", + "file_scope": [ + "rust/fleet-data/src/fleet.rs", + "rust/fleet-data/src/scrape.rs" + ], + "depends_on": [], + "spec_row_id": null, + "capability_hint": "rust_work", + "status": "available", + "claimed_by_session_id": null, + "claimed_by_agent": null, + "completed_summary": null + }, + { + "subtask_index": 3, + "title": "Shared bash helper lib", + "description": "Pull derive_aid + ios_visible_len + pct_color out of the big scripts into scripts/codex-fleet/lib/.", + "file_scope": [ + "scripts/codex-fleet/lib/agents.sh", + "scripts/codex-fleet/lib/ui-helpers.sh" + ], + "depends_on": [], + "spec_row_id": null, + "capability_hint": "infra_work", + "status": "available", + "claimed_by_session_id": null, + "claimed_by_agent": null, + "completed_summary": null + }, + { + "subtask_index": 4, + "title": "CliConvention trait in fleet-launcher", + "description": "Replace the per-CLI match in build_command_line() with a CliConvention trait, one impl per CliKind variant.", + "file_scope": [ + "rust/fleet-launcher/src/lib.rs" + ], + "depends_on": [], + "spec_row_id": null, + "capability_hint": "rust_work", + "status": "available", + "claimed_by_session_id": null, + "claimed_by_agent": null, + "completed_summary": null + }, + { + "subtask_index": 5, + "title": "Wire fleet-data::toposort into fleet-watcher", + "description": "Once the toposort module lands, update fleet-watcher to use it instead of its own inlined copy.", + "file_scope": [ + "rust/fleet-watcher/src/main.rs" + ], + "depends_on": [ + 1 + ], + "spec_row_id": null, + "capability_hint": "rust_work", + "status": "available", + "claimed_by_session_id": null, + "claimed_by_agent": null, + "completed_summary": null + }, + { + "subtask_index": 6, + "title": "Add scrape unit tests for ANSI scrollback", + "description": "After scrape.rs is extracted, add fixtures covering ANSI escape sequences and multi-line headlines.", + "file_scope": [ + "rust/fleet-data/src/scrape.rs" + ], + "depends_on": [ + 2 + ], + "spec_row_id": null, + "capability_hint": "test_work", + "status": "available", + "claimed_by_session_id": null, + "claimed_by_agent": null, + "completed_summary": null + }, + { + "subtask_index": 7, + "title": "Source agents.sh + ui-helpers.sh from full-bringup.sh", + "description": "Replace the inlined helpers in full-bringup.sh with source statements.", + "file_scope": [ + "scripts/codex-fleet/full-bringup.sh" + ], + "depends_on": [ + 3 + ], + "spec_row_id": null, + "capability_hint": "infra_work", + "status": "available", + "claimed_by_session_id": null, + "claimed_by_agent": null, + "completed_summary": null + }, + { + "subtask_index": 8, + "title": "Source helper lib from fleet-tick.sh", + "description": "Same dedup pass for fleet-tick.sh.", + "file_scope": [ + "scripts/codex-fleet/fleet-tick.sh" + ], + "depends_on": [ + 3 + ], + "spec_row_id": null, + "capability_hint": "infra_work", + "status": "available", + "claimed_by_session_id": null, + "claimed_by_agent": null, + "completed_summary": null + }, + { + "subtask_index": 9, + "title": "Migrate tab_strip render to use layout module", + "description": "Move geometry / wrap calculations out of render() into tab_strip/layout.rs now that render_pill is extracted.", + "file_scope": [ + "rust/fleet-ui/src/tab_strip/layout.rs", + "rust/fleet-ui/src/tab_strip/render.rs" + ], + "depends_on": [ + 0 + ], + "spec_row_id": null, + "capability_hint": "ui_work", + "status": "available", + "claimed_by_session_id": null, + "claimed_by_agent": null, + "completed_summary": null + }, + { + "subtask_index": 10, + "title": "Document refactor wave + verify cargo check workspace", + "description": "Wrap-up task: write the changelog entry and confirm cargo check --workspace is clean across all lanes.", + "file_scope": [ + "CHANGELOG.md" + ], + "depends_on": [ + 5, + 6, + 7, + 8, + 9 + ], + "spec_row_id": null, + "capability_hint": "doc_work", + "status": "available", + "claimed_by_session_id": null, + "claimed_by_agent": null, + "completed_summary": null + }, + { + "subtask_index": 11, + "title": "Smoke-test full bringup with refactored scripts", + "description": "Final gate: run scripts/codex-fleet/full-bringup.sh --n 4 --no-attach to confirm the helper-lib dedup did not regress bringup.", + "file_scope": [ + "scripts/codex-fleet/full-bringup.sh" + ], + "depends_on": [ + 7, + 8 + ], + "spec_row_id": null, + "capability_hint": "smoke_work", + "status": "available", + "claimed_by_session_id": null, + "claimed_by_agent": null, + "completed_summary": null + } + ] +} diff --git a/scripts/codex-fleet/demo/tick.sh b/scripts/codex-fleet/demo/tick.sh new file mode 100755 index 0000000..8310604 --- /dev/null +++ b/scripts/codex-fleet/demo/tick.sh @@ -0,0 +1,251 @@ +#!/usr/bin/env bash +# Demo tick simulator. Mutates the synthetic plan + pane scrollback every +# few seconds so the dashboards animate. Run in the background by +# scripts/codex-fleet/demo/up.sh. +# +# State machine per task: available → claimed → in_progress → completed. +# Each tick: pick one ready task per idle agent, advance one in-progress +# task toward completion, refresh runtimes/headlines in the pane fixtures, +# rewrite the counters file. +set -euo pipefail + +DEMO_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd -- "$DEMO_DIR/../../.." && pwd)" +PLAN_SLUG="demo-refactor-wave-2026-05-16" +# Runtime plan copy lives in openspec/plans/ (up.sh seeds it from the +# template under scripts/codex-fleet/demo/scenarios/). tick.sh mutates this +# copy in place; down.sh removes it. +PLAN_FILE="$REPO_ROOT/openspec/plans/$PLAN_SLUG/plan.json" +STATE_DIR="/tmp/claude-viz" +PANES_DIR="$STATE_DIR/demo-panes" +TICK_INTERVAL="${CODEX_FLEET_DEMO_TICK_INTERVAL:-3}" +LOOP_ON_DONE="${CODEX_FLEET_DEMO_LOOP:-1}" + +AIDS=(magnolia sumac yarrow clover thistle fennel mallow borage) +EMOJIS=("●" "◐" "◑" "◒" "◓" "◔" "◕" "○") + +trap 'echo "tick: stopped"; exit 0' INT TERM + +is_demo_active() { [[ -f "$STATE_DIR/demo-active" ]]; } + +reset_plan() { + jq '(.tasks[]) |= (.status = "available" + | .claimed_by_agent = null + | .claimed_by_session_id = null + | .completed_summary = null)' \ + "$PLAN_FILE" > "$PLAN_FILE.tmp" && mv "$PLAN_FILE.tmp" "$PLAN_FILE" +} + +deps_satisfied() { + local idx="$1" + local unmet + unmet=$(jq --argjson idx "$idx" ' + .tasks[$idx].depends_on + | map(. as $d | $d as $needle + | ($needle | (. != null and . != "")) + | if . then $needle else empty end) + | map(. as $d | select( + ($d | type == "number") and + ([(input | .tasks[] | select(.subtask_index == $d) | .status)] | first != "completed") + )) + | length + ' "$PLAN_FILE" "$PLAN_FILE" 2>/dev/null || echo 1) + [[ "$unmet" == "0" ]] +} + +# Find next available task whose deps are met. Returns subtask_index or empty. +next_ready_task() { + local n + n=$(jq '.tasks | length' "$PLAN_FILE") + local i + for ((i=0; i "$PLAN_FILE.tmp" && mv "$PLAN_FILE.tmp" "$PLAN_FILE" +} + +advance_task() { + # Move one randomly-picked claimed/in_progress task to the next state. + local idx="$1" next_status="$2" + jq --argjson idx "$idx" --arg s "$next_status" \ + '(.tasks[] | select(.subtask_index == $idx) | .status) = $s' \ + "$PLAN_FILE" > "$PLAN_FILE.tmp" && mv "$PLAN_FILE.tmp" "$PLAN_FILE" +} + +complete_task() { + local idx="$1" + jq --argjson idx "$idx" \ + '(.tasks[] | select(.subtask_index == $idx) | .status) = "completed" + | (.tasks[] | select(.subtask_index == $idx) | .completed_summary) = + "Demo: synthetic completion at \(now | strftime("%H:%M:%S"))"' \ + "$PLAN_FILE" > "$PLAN_FILE.tmp" && mv "$PLAN_FILE.tmp" "$PLAN_FILE" +} + +write_pane_fixture() { + local aid="$1" idx="$2" runtime_s="$3" + local title + title=$(jq -r --argjson idx "$idx" '.tasks[] | select(.subtask_index == $idx) | .title' "$PLAN_FILE") + local minutes=$((runtime_s / 60)) + local seconds=$((runtime_s % 60)) + cat > "$PANES_DIR/$aid.txt" < demo-refactor-wave-2026-05-16 / subtask $idx + ${title} + +gpt-5.5 high +Working (${minutes}m ${seconds}s) +EOF +} + +write_pane_idle() { + local aid="$1" + cat > "$PANES_DIR/$aid.txt" < "$PANES_DIR/$aid.txt" < "$STATE_DIR/fleet-tab-counters.json" +} + +# Track per-task elapsed runtime in seconds since claim. +declare -A task_runtime + +tick_once() { + # 1. Hand out tasks to idle agents. + local aid + while IFS= read -r aid; do + [[ -z "$aid" ]] && continue + local idx + idx=$(next_ready_task) + if [[ -n "$idx" ]]; then + assign_task "$idx" "$aid" + task_runtime[$idx]=0 + else + # No ready task — show idle scrollback unless this aid is "capped" + if [[ "$aid" == "clover" ]]; then + write_pane_capped "$aid" + else + write_pane_idle "$aid" + fi + fi + done < <(agents_idle) + + # 2. Advance every claimed/in_progress task by one tick. + local rows + rows=$(jq -c '.tasks[] | select(.status == "claimed" or .status == "in_progress") | {idx:.subtask_index, aid:.claimed_by_agent, status:.status}' "$PLAN_FILE") + while IFS= read -r row; do + [[ -z "$row" ]] && continue + local idx status aid + idx=$(echo "$row" | jq -r '.idx') + status=$(echo "$row" | jq -r '.status') + aid=$(echo "$row" | jq -r '.aid' | sed 's/^codex-//') + local rt="${task_runtime[$idx]:-0}" + rt=$((rt + TICK_INTERVAL)) + task_runtime[$idx]=$rt + write_pane_fixture "$aid" "$idx" "$rt" + + if [[ "$status" == "claimed" && "$rt" -ge 4 ]]; then + advance_task "$idx" "in_progress" + elif [[ "$status" == "in_progress" && "$rt" -ge 18 ]]; then + complete_task "$idx" + unset 'task_runtime[$idx]' + fi + done <<<"$rows" + + write_counters +} + +main() { + # Wait briefly for up.sh to finish writing initial state. + sleep 1 + reset_plan + + while is_demo_active; do + tick_once + + # Loop scenario: if all tasks done, reset and start over. + local done_count total + done_count=$(jq '[.tasks[] | select(.status == "completed")] | length' "$PLAN_FILE") + total=$(jq '.tasks | length' "$PLAN_FILE") + if [[ "$done_count" == "$total" ]]; then + if [[ "$LOOP_ON_DONE" == "1" ]]; then + sleep 4 + reset_plan + task_runtime=() + else + echo "tick: all tasks complete, exiting (CODEX_FLEET_DEMO_LOOP=0)" + exit 0 + fi + fi + + sleep "$TICK_INTERVAL" + done +} + +main "$@" diff --git a/scripts/codex-fleet/demo/up.sh b/scripts/codex-fleet/demo/up.sh new file mode 100755 index 0000000..1f7200c --- /dev/null +++ b/scripts/codex-fleet/demo/up.sh @@ -0,0 +1,215 @@ +#!/usr/bin/env bash +# Bring up the codex-fleet demo: synthetic plan + 8 fake worker panes + the +# real fleet-state / fleet-tab-strip / fleet-plan-tree / fleet-waves +# binaries rendering against the fake state. No real codex sessions, no API +# spend. +# +# Usage: +# bash scripts/codex-fleet/demo/up.sh # bring up + attach +# bash scripts/codex-fleet/demo/up.sh --no-attach +# bash scripts/codex-fleet/demo/up.sh --no-tick # no auto-animation +# +# Teardown: +# bash scripts/codex-fleet/demo/down.sh +set -euo pipefail + +DEMO_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd -- "$DEMO_DIR/../../.." && pwd)" +RUST_DIR="$REPO_ROOT/rust" +RELEASE_DIR="$RUST_DIR/target/release" +DEBUG_DIR="$RUST_DIR/target/debug" + +SOCKET="${CODEX_FLEET_DEMO_SOCKET:-codex-fleet-demo}" +SESSION="codex-fleet-demo" +STATE_DIR="/tmp/claude-viz" +DEMO_TAG_FILE="$STATE_DIR/demo-active" +PLAN_SLUG="demo-refactor-wave-2026-05-16" +SCENARIO="${CODEX_FLEET_DEMO_SCENARIO:-refactor-wave}" +SCENARIO_DIR="$DEMO_DIR/scenarios/$SCENARIO" +PLAN_TEMPLATE="$SCENARIO_DIR/plan.json" +PLAN_RUNTIME="$REPO_ROOT/openspec/plans/$PLAN_SLUG/plan.json" +WORKERS=8 + +attach=1 +tick=1 +while [[ $# -gt 0 ]]; do + case "$1" in + --no-attach) attach=0 ;; + --no-tick) tick=0 ;; + -h|--help) + sed -n '2,15p' "$0" + exit 0 + ;; + *) echo "unknown flag: $1" >&2; exit 64 ;; + esac + shift +done + +# --- prerequisites ----------------------------------------------------- + +need() { command -v "$1" >/dev/null 2>&1 || { echo "demo: missing \`$1\`" >&2; exit 1; }; } +need tmux +need jq + +resolve_bin() { + local name="$1" + if [[ -x "$RELEASE_DIR/$name" ]]; then echo "$RELEASE_DIR/$name" + elif [[ -x "$DEBUG_DIR/$name" ]]; then echo "$DEBUG_DIR/$name" + else return 1 + fi +} + +for bin in fleet-state fleet-plan-tree fleet-waves; do + if ! resolve_bin "$bin" >/dev/null; then + echo "demo: missing $bin — run \`cargo build --release -p $bin\` from $RUST_DIR" >&2 + exit 1 + fi +done + +FLEET_STATE_BIN="$(resolve_bin fleet-state)" +FLEET_PLAN_TREE_BIN="$(resolve_bin fleet-plan-tree)" +FLEET_WAVES_BIN="$(resolve_bin fleet-waves)" +FLEET_WATCHER_BIN="$(resolve_bin fleet-watcher || true)" + +# --- state directory --------------------------------------------------- + +mkdir -p "$STATE_DIR" +echo "$PLAN_SLUG" >"$STATE_DIR/plan-tree-pin.txt" +echo "$$" >"$DEMO_TAG_FILE" + +# Copy the pristine plan template into openspec/plans/ so fleet-plan-tree +# and fleet-waves can discover it. The runtime copy will be mutated by +# tick.sh; down.sh removes it on teardown so the working tree stays clean. +if [[ ! -r "$PLAN_TEMPLATE" ]]; then + echo "demo: scenario template missing: $PLAN_TEMPLATE" >&2 + exit 1 +fi +mkdir -p "$(dirname "$PLAN_RUNTIME")" +cp "$PLAN_TEMPLATE" "$PLAN_RUNTIME" + +write_counters() { + local total ready in_prog blocked + total=$(jq '.tasks | length' "$PLAN_RUNTIME") + ready=$(jq '[.tasks[] | select(.status == "available")] | length' "$PLAN_RUNTIME") + in_prog=$(jq '[.tasks[] | select(.status == "in_progress" or .status == "claimed")] | length' "$PLAN_RUNTIME") + blocked=$((total - ready - in_prog)) + jq -n \ + --argjson overview "$WORKERS" \ + --argjson fleet "$WORKERS" \ + --argjson plan "$total" \ + --argjson waves "$in_prog" \ + --argjson review "$blocked" \ + --argjson ts "$(date +%s)" \ + '{overview:$overview, fleet:$fleet, plan:$plan, waves:$waves, review:$review, updated_at:$ts}' \ + > "$STATE_DIR/fleet-tab-counters.json" +} + +write_quality_scores() { + jq -n --argjson ts "$(date +%s)" ' + { + generated_at: ($ts | tostring), + scores: { + "magnolia": {score:92, agent_id:"magnolia", pr_number:154, pr_title:"refactor(fleet-data): toposort", branch:"agent/refactor-toposort-extract", plan_slug:"demo-refactor-wave-2026-05-16", criteria_met:["tests pass","public api stable"], criteria_missed:[], reasoning:"Clean extraction, 60/60 tests.", scored_at:($ts|tostring)}, + "clover": {score:88, agent_id:"clover", pr_number:155, pr_title:"refactor(fleet-data): scrape", branch:"agent/refactor-scrape-extract", plan_slug:"demo-refactor-wave-2026-05-16", criteria_met:["tests pass"], criteria_missed:["docs missing"], reasoning:"Solid split.", scored_at:($ts|tostring)}, + "borage": {score:74, agent_id:"borage", pr_number:156, pr_title:"refactor(fleet-ui): tab_strip", branch:"agent/refactor-tab-strip-split", plan_slug:"demo-refactor-wave-2026-05-16", criteria_met:["render_pill extracted"], criteria_missed:["snapshot not updated"], reasoning:"Needs snapshot regen.", scored_at:($ts|tostring)} + } + }' > "$STATE_DIR/fleet-quality-scores.json" +} + +write_counters +write_quality_scores + +# Pick the "current" account so the agent-auth shim marks it with `*`. +echo "admin-magnolia@example.dev" > "$STATE_DIR/demo-current-account" + +# --- tmux session ------------------------------------------------------ + +tmux -L "$SOCKET" kill-session -t "$SESSION" 2>/dev/null || true +tmux -L "$SOCKET" new-session -d -s "$SESSION" -n overview -x 220 -y 60 + +# Worker pane factory: 8 panes, each labeled @panel=[codex-]. +# Aids match the agent-auth shim emails (admin-@…). +AIDS=(magnolia sumac yarrow clover thistle fennel mallow borage) + +# We need a working dir on PATH that has the agent-auth shim FIRST so +# fleet-state's subprocess call resolves to our fake. +DEMO_PATH="$DEMO_DIR:$PATH" + +# Worker pane content: a per-aid script that prints fake codex scrollback +# matching the shapes fleet-data::scrape and panes::classify expect, then +# tail -f's a per-aid scrollback file that tick.sh rewrites. +mkdir -p "$STATE_DIR/demo-panes" +for aid in "${AIDS[@]}"; do + cat > "$STATE_DIR/demo-panes/$aid.txt" < demo-refactor-wave-2026-05-16 / subtask 0 + Extract render_pill helper from tab_strip + +gpt-5.5 high +Working (0m 12s) +EOF +done + +# Lay out the overview window as a 4x2 grid of 8 worker panes. +# (The standalone fleet-tab-strip binary was removed by PR #107; the tab +# strip now renders inline inside fleet-state / fleet-plan-tree / etc. +# via fleet_ui::tab_strip reading fleet-tab-counters.json.) +root_pane="$(tmux -L "$SOCKET" display-message -p -t "$SESSION:overview.0" '#{pane_id}')" + +# Split: 1 vertical → 2 cols, then 3 horizontal splits per col → 8 panes. +tmux -L "$SOCKET" split-window -h -t "$root_pane" "sleep 86400" +right_col="$(tmux -L "$SOCKET" display-message -p -t "$SESSION:overview" '#{pane_id}')" + +for col in "$root_pane" "$right_col"; do + for _ in 1 2 3; do + tmux -L "$SOCKET" split-window -v -t "$col" "sleep 86400" + done +done +tmux -L "$SOCKET" select-layout -t "$SESSION:overview" tiled >/dev/null + +mapfile -t worker_panes < <(tmux -L "$SOCKET" list-panes -t "$SESSION:overview" -F '#{pane_id}') + +if [[ "${#worker_panes[@]}" -lt $WORKERS ]]; then + echo "demo: only got ${#worker_panes[@]} worker panes, expected $WORKERS" >&2 + exit 1 +fi + +for i in "${!AIDS[@]}"; do + aid="${AIDS[$i]}" + pane="${worker_panes[$i]}" + tmux -L "$SOCKET" set-option -p -t "$pane" '@panel' "[codex-$aid]" >/dev/null + tmux -L "$SOCKET" respawn-pane -k -t "$pane" \ + "bash -c 'cat \"$STATE_DIR/demo-panes/$aid.txt\"; tail -F \"$STATE_DIR/demo-panes/$aid.txt\" 2>/dev/null'" +done + +# Dashboard windows running the real binaries. +tmux -L "$SOCKET" new-window -t "$SESSION:" -n fleet \ + "env PATH='$DEMO_PATH' '$FLEET_STATE_BIN'; sleep 86400" +tmux -L "$SOCKET" new-window -t "$SESSION:" -n plan \ + "env PATH='$DEMO_PATH' CODEX_FLEET_PLAN_REPO_ROOT='$REPO_ROOT' '$FLEET_PLAN_TREE_BIN'; sleep 86400" +tmux -L "$SOCKET" new-window -t "$SESSION:" -n waves \ + "env PATH='$DEMO_PATH' CODEX_FLEET_PLAN_REPO_ROOT='$REPO_ROOT' '$FLEET_WAVES_BIN'; sleep 86400" + +if [[ -n "${FLEET_WATCHER_BIN:-}" ]]; then + tmux -L "$SOCKET" new-window -t "$SESSION:" -n watcher \ + "env PATH='$DEMO_PATH' CODEX_FLEET_PLAN_REPO_ROOT='$REPO_ROOT' '$FLEET_WATCHER_BIN'; sleep 86400" +fi + +tmux -L "$SOCKET" select-window -t "$SESSION:overview" + +# --- tick simulator ---------------------------------------------------- + +if [[ "$tick" -eq 1 ]]; then + nohup bash "$DEMO_DIR/tick.sh" >/tmp/claude-viz/demo-tick.log 2>&1 & + echo "$!" > "$STATE_DIR/demo-tick.pid" +fi + +echo "demo up. session=$SESSION socket=$SOCKET plan=$PLAN_SLUG" +echo "attach: tmux -L $SOCKET attach -t $SESSION" +echo "tear down: bash scripts/codex-fleet/demo/down.sh" + +if [[ "$attach" -eq 1 ]]; then + exec tmux -L "$SOCKET" attach -t "$SESSION" +fi