344 lines
15 KiB
Markdown
344 lines
15 KiB
Markdown
|
||
- **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:
|
||
|
||
1. **Surface area vs. polish.** `dashboard.py` is ~1740 lines;
|
||
`dashboard_model.py` adds 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.
|
||
2. **No clear ownership of "starts and stops bottles".** Today
|
||
that responsibility is split: `./cli.py start` owns one-shot
|
||
sessions; the dashboard owns multi-session bottles it started
|
||
itself; `./cli.py cleanup` owns everything else. The dashboard
|
||
tracking its own `bottles: dict[str, (cm, bottle, identity)]`
|
||
that doesn't survive a quit is a confusing third lane.
|
||
3. **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
|
||
|
||
1. The supervise TUI starts up, lists pending proposals across all
|
||
running bottles, and supports approve / modify / reject + the
|
||
`--once` non-interactive mode — exactly as PRDs 0013–0016
|
||
specified, minus everything 0019/0020/0021 added.
|
||
2. The CLI subcommand is renamed `supervise` (was `dashboard`). The
|
||
old name is not aliased — this PRD is intentionally a
|
||
compat/breaking change (the issue carries the
|
||
`Compat/Breaking` label).
|
||
3. `dashboard.py` shrinks 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, no
|
||
`e`/`p` operator-edit verbs, no per-process `bottles` dict.
|
||
4. `dashboard_model.py` is collapsed into whatever
|
||
`supervise.py` (CLI) needs; the model module is removed if it
|
||
has no purpose after the cut.
|
||
5. The proposal-side apply paths in `bot_bottle/backend/docker/
|
||
egress_apply.py`, `pipelock_apply.py`, and `capability_apply.py`
|
||
are unchanged — they are still called by the approve path.
|
||
6. The supervise-sidecar / proposal-queue protocol (PRD 0013) is
|
||
unchanged: the agent's experience is identical.
|
||
7. 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` / `p` keys 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 start` or 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) plus `cli.py cleanup`
|
||
for 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 `dashboard` anywhere 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 dashboard` becomes
|
||
`./cli.py supervise`. The module moves from `bot_bottle/cli/
|
||
dashboard.py` to `bot_bottle/cli/supervise.py`. The dispatcher
|
||
in `bot_bottle/cli/__init__.py` and 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`/`r` for approve / modify / reject, `q` to quit. No
|
||
agents pane, no Tab, no agent picker, no `n`/`x`/`e`/`p`, no
|
||
tmux dispatch, no `bottles` dict 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 into
|
||
`supervise.py` (CLI) or into `bot_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.py` daemon module). The wire protocol stays.
|
||
- Changes to the routes / pipelock / capability apply engines.
|
||
- Migration helpers, deprecation warnings, or a transitional
|
||
`dashboard` alias for `supervise`. 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` / `r` work 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 — `q` just 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*.py` cases that exercise
|
||
`discover_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
|
||
`--once` listing 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.
|
||
|
||
1. **Strip + rename in one cut.** Move `bot_bottle/cli/
|
||
dashboard.py` to `bot_bottle/cli/supervise.py`, delete the
|
||
removed helpers, delete `dashboard_model.py`, inline the
|
||
surviving helpers, update the dispatcher + usage in
|
||
`bot_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).
|
||
2. **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
|
||
|
||
1. **`e` / `p` operator-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 existing `apply_routes_change` / `apply_allowlist_change`
|
||
engines. 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.
|
||
|
||
2. **`--once` output 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.
|
||
|
||
3. **Audit-log entry shape for an unprompted edit applied via
|
||
a future `routes edit` CLI verb.** Today's
|
||
`operator_edit_routes` writes an `ACTION_OPERATOR_EDIT`
|
||
audit entry. With those flows removed the constant has no
|
||
callers inside this PRD's scope. Keep the constant exported
|
||
from `supervise.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](https://gitea.dideric.is/didericis/bot-bottle/issues/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.
|