
- **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.
