15 KiB
- Status: Active
- Author: didericis
- Created: 2026-06-03
- Issue: #174
Summary
The ./cli.py dashboard command has grown from its PRD 0013 roots
(triage supervise proposals) into a parallel-agent control surface
(PRDs 0019/0020/0021): an active-agents pane, agent picker + start,
re-attach, per-bottle stop, tmux split-pane handoff, operator-
initiated routes/pipelock edits. Each chunk is reasonable on its
own; together they make the dashboard the largest CLI file in the
repo and the thing most likely to break on a rough edge (curses /
tmux / docker-exec / metadata-discovery interactions).
This PRD reverses that scope creep. The dashboard is reduced to the
supervise-plane triage TUI it was in PRDs 0013–0016: list pending
proposals, approve / modify / reject each one, write audit entries,
deliver the response that unblocks the agent's tool call. Everything
that's about starting / re-entering / stopping bottles, or about
operator-initiated config edits, comes out. The command is renamed
./cli.py supervise so the name matches what it does after the cut.
Future agent-management UX is explicitly punted: if and when a control surface for parallel agents resurfaces, the working assumption (per the issue) is that a web GUI — usable from mobile — is a better second pass than another round of curses iteration. That decision is not in this PRD's scope; this PRD only removes the half-built local-curses path so we stop maintaining it.
Problem
Three concrete pains, all downstream of the dashboard's growth:
- Surface area vs. polish.
dashboard.pyis ~1740 lines;dashboard_model.pyadds another ~420. The interactions among curses, modals, tmux split-pane, docker-exec handoff, agent provider templates, metadata-driven re-attach, and ExitStack-free bottle ownership are intricate enough that shipping the next polish increment costs more than it returns. - No clear ownership of "starts and stops bottles". Today
that responsibility is split:
./cli.py startowns one-shot sessions; the dashboard owns multi-session bottles it started itself;./cli.py cleanupowns everything else. The dashboard tracking its ownbottles: dict[str, (cm, bottle, identity)]that doesn't survive a quit is a confusing third lane. - Wrong target shape for a "manage many agents" UI. The parallel-agent experience the dashboard reaches for is mobile- meaningful — checking in on agents from a phone is the high- value case — and curses inside an SSH session is the wrong tool for that. Continuing to polish a local-only TUI delays the right next investment.
The triage half of the dashboard isn't suffering from any of these. Pending proposals are a small, well-scoped, real workload, and the PRD 0013–0016 surface for handling them is the right shape. The problem is everything that got bolted onto that core after.
Goals / Success Criteria
- The supervise TUI starts up, lists pending proposals across all
running bottles, and supports approve / modify / reject + the
--oncenon-interactive mode — exactly as PRDs 0013–0016 specified, minus everything 0019/0020/0021 added. - The CLI subcommand is renamed
supervise(wasdashboard). The old name is not aliased — this PRD is intentionally a compat/breaking change (the issue carries theCompat/Breakinglabel). dashboard.pyshrinks to a single proposal-triage curses loop: no agents pane, no Tab pane switching, no agent picker, no start / re-attach / stop verbs, no tmux split-pane, noe/poperator-edit verbs, no per-processbottlesdict.dashboard_model.pyis collapsed into whateversupervise.py(CLI) needs; the model module is removed if it has no purpose after the cut.- The proposal-side apply paths in
bot_bottle/backend/docker/ egress_apply.py,pipelock_apply.py, andcapability_apply.pyare unchanged — they are still called by the approve path. - The supervise-sidecar / proposal-queue protocol (PRD 0013) is unchanged: the agent's experience is identical.
- The previously-active PRDs that this one undoes are marked
Superseded by PRD 0049:
- PRD 0019 — active-agents pane + agent-scoped edit verbs
- PRD 0020 — start / re-attach / stop from the dashboard
- PRD 0021 — tmux split-pane
Non-goals
- A web GUI for managing agents. The issue floats this as a second pass; this PRD does not design or commit to it. The cut is "remove the path we no longer want to invest in", not "build the replacement".
- A separate CLI for operator-initiated routes / pipelock
edits. Today those edits live as
e/pkeys inside the dashboard. After this PRD they don't exist anywhere — operators who need ad-hoc edits use the same path the agents do (call the supervise tool from inside the bottle) or hand-edit the host- side files and restart the sidecar. Adding a./cli.py routes edit <slug>verb is a follow-up if the loss bites. - Removing
./cli.py startor changing its semantics. Start remains the one-shot launch path. PRD 0020's bottle-outlives- process model is removed; the only path to a long-running bottle is./cli.py start(foreground) pluscli.py cleanupfor teardown. - Removing the supervise-sidecar protocol or any of the three block-remediation engines. PRDs 0013–0016 stay Active. The agent's view of the world doesn't change.
- Renaming
dashboardanywhere other than the CLI entry point. The dashboard-related docs (PRDs, decision records, research notes) keep their historical references — they describe the state of the world at the time they were written, and the Status: Superseded line is the marker that the world has moved on. - Migrating the proposal-queue file layout. The queue still
lives at
~/.bot-bottle/queue/<slug>/; the audit log still lives at~/.bot-bottle/audit/<component>-<slug>.log. The CLI surface changes; the on-disk surface does not.
Scope
In scope
- Rename the subcommand.
./cli.py dashboardbecomes./cli.py supervise. The module moves frombot_bottle/cli/ dashboard.pytobot_bottle/cli/supervise.py. The dispatcher inbot_bottle/cli/__init__.pyand the help text both update. - Strip the curses loop to proposal-only. The remaining
surface is: list pending proposals (with the new-arrival bell
from PRD 0013), Enter for detail view,
a/m/rfor approve / modify / reject,qto quit. No agents pane, no Tab, no agent picker, non/x/e/p, no tmux dispatch, nobottlesdict on the main loop. - Drop unused helpers.
_picker_modal,_preflight_modal,_backend_picker_modal,_new_agent_flow,_attach_to_bottle,_attach_in_tmux,_attach_via_handoff,_tmux_*,_ensure_right_pane,_redirect_stderr_to_file,_route_op_to_right_pane,_stop_bottle_flow,_operator_edit_*_flow,operator_edit_routes,operator_edit_allowlist, and their imports come out. - Collapse the model module.
dashboard_model.py's proposal-side helpers (QueuedProposal,discover_pending,_approval_status,_detail_lines,_failed_url_host,_proposed_payload_label,_suffix_for_tool,_REFRESH_INTERVAL_MS) move back intosupervise.py(CLI) or intobot_bottle/supervise.py(the daemon-side module) — wherever they fit. The agents / picker / tmux helpers in that module (PANE_*,_filter_agents,_running_counts,_format_agent_row,_selection_status,_selected_agent,_bottle_for_slug,_pick_next_after_stop,_agent_runtime_args,_build_resume_argv_with_fallback,_build_split_pane_argv,_build_respawn_pane_argv,_in_tmux,discover_active_agents) are deleted. - Mark superseded PRDs. The Status line on PRDs 0019, 0020,
and 0021 changes to
Superseded by [PRD 0049](0049-strip- dashboard-to-supervisor-tui.md). - Test cleanup. Any test that targets a removed surface (the
agent picker, the tmux split helpers, the start-from-dashboard
flow, the operator-edit flows,
discover_active_agents) comes out. Tests covering proposal triage stay. - Help / usage strings.
bot_bottle/cli/__init__.py's usage block updates the command name and one-liner.
Out of scope
- Any new feature in the supervise TUI. The cut is purely subtractive (except for the rename).
- Behavior changes in
./cli.py start,cli.py cleanup,cli.py resume,cli.py list,cli.py info,cli.py edit,cli.py init— unchanged. - Changes to the supervise sidecar (
supervise_server.py,supervise.pydaemon module). The wire protocol stays. - Changes to the routes / pipelock / capability apply engines.
- Migration helpers, deprecation warnings, or a transitional
dashboardalias forsupervise. The label on the issue says Compat/Breaking; the rename is a hard cutover.
Proposed design
Final shape of the TUI
After this PRD the ./cli.py supervise curses surface is:
bot-bottle supervise (3 pending)
─────────────────────────────────────────────────────────
> 03:14:22 [implementer-cy7a6] egress-block abc123… add
github.com/foo
03:13:55 [researcher-9xqs1] pipelock-block def456… allow
registry.npmjs.org
03:13:10 [implementer-cy7a6] capability-block ghi789… install
ripgrep
─────────────────────────────────────────────────────────
[j/k] move [Enter] view [a] approve [m] modify [r] reject [q] quit
- One pane. No Tab.
j/k/ arrows move through the queue. - Enter opens the existing detail view (justification +
proposed-file body + the green pipelock host-extraction hint).
a/m/rwork from both the list view and the detail view, same as today. q/ Esc quits. There are no dashboard-owned bottles, so no per-process teardown decision —qjust exits.- The new-arrival bell stays, because it is a real win for the operator's "I was typing at claude and a proposal landed" case. No tmux-specific focus management remains.
Code organisation
After the cut, the CLI module looks roughly like:
bot_bottle/cli/supervise.py
- cmd_supervise(argv)
- _list_once() # --once mode
- _main_loop(stdscr) # proposal-only
- _render(stdscr, pending, ...)
- _detail_view(stdscr, qp, ...)
- _modify(stdscr, qp)
- _prompt(stdscr, label)
- _write_crash_log(exc)
- approve(qp, *, notes, final_file)
- reject(qp, *, reason)
- QueuedProposal, discover_pending
- _detail_lines, _approval_status,
_failed_url_host,
_proposed_payload_label,
_suffix_for_tool
dashboard_model.py has no purpose once the agents / picker /
tmux helpers are gone, so it is removed and the surviving
proposal-side helpers move into supervise.py directly. The
PRD-0013 refactor that split model out (refactor: extract dashboard state/model layer into dashboard_model.py) was
load-bearing for the bigger dashboard surface; with the surface
shrunk back, the split is no longer justified.
Removed PRDs: how to mark them
The three superseded PRDs keep their bodies intact. Only the Status line at the top changes:
- **Status:** Superseded by [PRD
0049](0049-strip-dashboard-to-supervisor-tui.md)
The PRD's own Goals / Success Criteria are left as the historical record of what the feature shipped — readers tracing back from the code or the git log land in a PRD that explains what once was, with a clear pointer forward. No PRD body is rewritten.
Tests to keep, tests to remove
Keep:
tests/cli/test_dashboard*.pycases that exercisediscover_pending,approve,reject,_detail_lines,_approval_status,_failed_url_host,_proposed_payload_label,_suffix_for_tool,_modify/edit_in_editor.tests/cli/test_dashboard_once.py(or equivalent) — the--oncelisting mode.
Remove:
- Any test of
_picker_modal,_preflight_modal,_backend_picker_modal,_new_agent_flow,_attach_*,_tmux_*,_route_op_to_right_pane,_redirect_stderr_to_file,_stop_bottle_flow,_operator_edit_*,_filter_agents,_running_counts,_format_agent_row,_selection_status,_selected_agent,_bottle_for_slug,_pick_next_after_stop,_agent_runtime_args,_build_*_argv,discover_active_agents. - The test files that exist solely to cover those (e.g.,
test_dashboard_picker.py,test_dashboard_tmux.py,test_dashboard_attach.py,test_dashboard_agents.py— whichever of these exist after the file walk).
Files are renamed test_supervise_*.py to mirror the module
rename. The rename is mechanical; no test logic changes.
Implementation chunks
Sized for a single PR each.
- Strip + rename in one cut. Move
bot_bottle/cli/ dashboard.pytobot_bottle/cli/supervise.py, delete the removed helpers, deletedashboard_model.py, inline the surviving helpers, update the dispatcher + usage inbot_bottle/cli/__init__.py, rename tests to match, mark PRDs 0019/0020/0021 as superseded. One commit per logical piece inside the PR (rename, strip, supersede notes, tests). - Activate PRD 0049. Flip this PRD's Status line from
Draft to Active in the same PR as chunk 1 once the
implementation lands. (The repo convention is that a PRD's
shipping commit is also the Status flip — see the recent
docs(prd): activate PRD 0048…commit shape.)
The PR closes issue #174.
Open questions
-
e/poperator-initiated edits — gone for good or moved to a separate CLI verb? The PRD removes them with no replacement. The simplest replacement is./cli.py routes edit <slug>and./cli.py pipelock edit <slug>, sharing the existingapply_routes_change/apply_allowlist_changeengines. If the loss is felt within the first parallel run after this lands, that follow-up is a small PR. Leaving it for a separate PRD so this one stays subtractive. -
--onceoutput shape. The text listing today emits one proposal per line. Worth keeping exactly as-is for scripting consumers; this PRD does not change it. Flagging only because the rename could tempt a tweak. -
Audit-log entry shape for an unprompted edit applied via a future
routes editCLI verb. Today'soperator_edit_routeswrites anACTION_OPERATOR_EDITaudit entry. With those flows removed the constant has no callers inside this PRD's scope. Keep the constant exported fromsupervise.py(it's already an__all__member) so a follow-up CLI verb can re-use the same audit shape without re-introducing dead code first.
References
- Issue #174 — the request: "strip the dashboard down into just a TUI for managing agent requests for new egress routes and new capabilities."
- PRD 0013 — supervise plane foundation (the floor this PRD reverts the dashboard to).
- PRDs 0014 / 0015 / 0016 — block-remediation engines that the supervise TUI continues to drive on approve.
- PRDs 0019 / 0020 / 0021 — the bolted-on capabilities this PRD removes.