docs(prd): add PRD 0050 -- strip dashboard to supervisor tui
This commit is contained in:
@@ -0,0 +1,346 @@
|
||||
|
||||
- **Status:** Draft
|
||||
- **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 +
|
||||
green highlight 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`, `_is_recent`, `_detail_lines`,
|
||||
`_failed_url_host`, `_proposed_payload_label`,
|
||||
`_suffix_for_tool`, `_REFRESH_INTERVAL_MS`,
|
||||
`_NEW_PROPOSAL_HIGHLIGHT_SEC`) 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 + green highlight + (if in tmux) the
|
||||
`tmux select-pane` jump back to the supervise pane stay,
|
||||
because they're real wins for the operator's "I was typing at
|
||||
claude and a proposal landed" case. They don't require any of
|
||||
the pane-management code being removed.
|
||||
|
||||
### 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,
|
||||
_is_recent, _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`,
|
||||
`_is_recent`, `_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.
|
||||
Reference in New Issue
Block a user