docs(prd-0019): active agents in dashboard + agent-scoped edit verbs
Draft a PRD that adds an "active agents" pane to the dashboard TUI (below the existing proposals pane) and reshapes the operator `routes edit` (e) / `pipelock edit` (p) verbs to be agent-scoped when the cursor is in the agents pane — no more global discover + disambiguation prompt on every press. Tab toggles which pane nav keys move through. Sized into 4 chunks (discovery helper → render pane → selection state → agent-scoped verbs). Six open questions called out, the biggest being whether per-bottle `compose ps` on every 1s tick scales for hosts with many bottles (answer leans toward one label-filtered `docker ps`).
This commit is contained in:
@@ -0,0 +1,241 @@
|
|||||||
|
# PRD 0019: Active agents in the dashboard, agent-scoped edit verbs
|
||||||
|
|
||||||
|
- **Status:** Draft
|
||||||
|
- **Author:** didericis
|
||||||
|
- **Created:** 2026-05-26
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
The dashboard today is proposal-centric: it lists every pending
|
||||||
|
supervise tool call across every running bottle and lets the
|
||||||
|
operator approve / modify / reject from one place. The operator-
|
||||||
|
initiated `routes edit` (`e`) and `pipelock edit` (`p`) verbs are
|
||||||
|
*global* — they discover every running sidecar of that kind and
|
||||||
|
prompt for which bottle to edit if more than one is up.
|
||||||
|
|
||||||
|
This PRD adds a first-class "active agents" view to the dashboard
|
||||||
|
and reshapes the edit verbs to be **agent-scoped**: the operator
|
||||||
|
picks an agent, then `e` / `p` (and any future per-agent verbs)
|
||||||
|
target that agent without a separate prompt.
|
||||||
|
|
||||||
|
After this PRD the dashboard answers two questions in one screen:
|
||||||
|
|
||||||
|
1. What's queued for me to act on? (existing proposals view)
|
||||||
|
2. What's currently running, and what would I act on if I
|
||||||
|
wanted to push a config edit without an agent prompt?
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Two rough edges in the current dashboard:
|
||||||
|
|
||||||
|
1. **No visibility into what's actually running.** The dashboard
|
||||||
|
shows only pending proposals. If no agent has called a tool,
|
||||||
|
the screen reads "no pending proposals" — even when five
|
||||||
|
bottles are quietly working. The operator has to `docker
|
||||||
|
compose ls` (or `./cli.py cleanup -n` to see the y/N preview)
|
||||||
|
to find out what's actually live.
|
||||||
|
|
||||||
|
2. **`e` / `p` re-discover-and-disambiguate every invocation.**
|
||||||
|
Today each press of `e` runs `discover_egress_slugs()`, finds
|
||||||
|
the running egress sidecars, and prompts if there's more than
|
||||||
|
one. The prompt interrupts the keyboard flow — and once the
|
||||||
|
operator picks a bottle, there's no carry-over to the next
|
||||||
|
edit. Editing pipelock for the same bottle right after is
|
||||||
|
another prompt.
|
||||||
|
|
||||||
|
The proposal-centric design is fine for the "agent triggered a
|
||||||
|
remediation" case but flips the relationship the wrong way for
|
||||||
|
the "operator wants to make an unprompted change" case.
|
||||||
|
|
||||||
|
## Goals / Success Criteria
|
||||||
|
|
||||||
|
1. The dashboard's main screen shows two lists: pending proposals
|
||||||
|
(above) and active agents (below) — both visible at once, no
|
||||||
|
tab / mode switch.
|
||||||
|
2. Each active-agent row shows enough for the operator to
|
||||||
|
recognize the bottle at a glance: identity (slug),
|
||||||
|
agent_name (from metadata.json), started_at, and which
|
||||||
|
sidecars are up.
|
||||||
|
3. The operator can select an agent row with `j` / `k` /
|
||||||
|
arrow keys (the same nav keys already in use for proposals),
|
||||||
|
with a clear keystroke that swaps the active list (e.g.,
|
||||||
|
`Tab` toggles which list `j` / `k` moves through).
|
||||||
|
4. Pressing `e` (routes edit) or `p` (pipelock edit) with an
|
||||||
|
agent selected targets that agent. No disambiguation prompt;
|
||||||
|
no global discover.
|
||||||
|
5. Pressing `e` / `p` with NO agent selected (e.g., zero agents
|
||||||
|
running, or the cursor is in the proposals pane) falls back
|
||||||
|
to today's global behavior — discover, prompt if multiple,
|
||||||
|
abort if none.
|
||||||
|
6. The active-agents list refreshes on the same ~1s tick as the
|
||||||
|
proposals list so an agent starting / stopping is reflected
|
||||||
|
without operator action.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- **Per-agent proposal filtering.** The proposals list stays
|
||||||
|
global across bottles. Filtering ("show me only this agent's
|
||||||
|
proposals") might be a follow-up but isn't this PRD.
|
||||||
|
- **Agent lifecycle from the dashboard.** Starting / stopping
|
||||||
|
agents stays in `./cli.py start` / `./cli.py cleanup`. The
|
||||||
|
dashboard reads state; it doesn't change it.
|
||||||
|
- **Preserved-but-not-running bottles.** The active-agents list
|
||||||
|
is strictly "what's running now" (cross-referenced from
|
||||||
|
`docker compose ls`). Preserved state dirs without a live
|
||||||
|
project don't appear — `./cli.py resume <identity>` is the
|
||||||
|
path for those.
|
||||||
|
- **A separate per-agent detail view.** The agent rows are
|
||||||
|
one-line summaries. Pressing Enter on a proposal still drops
|
||||||
|
into proposal-detail; we don't add an analogous agent-detail
|
||||||
|
screen in v1.
|
||||||
|
- **Replacing the existing `--once` mode.** `dashboard --once`
|
||||||
|
stays a proposal-only listing. No active-agents output there
|
||||||
|
(different consumers — `--once` is for scripts; the agents
|
||||||
|
view is for the interactive TUI).
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### In scope
|
||||||
|
|
||||||
|
- A new "active agents" pane in the curses TUI, rendered below
|
||||||
|
the proposals pane.
|
||||||
|
- A discovery helper that returns `(slug, agent_name,
|
||||||
|
started_at, services_up)` per active compose project. Reads
|
||||||
|
agent_name + started_at from each project's `metadata.json`,
|
||||||
|
cross-references `docker compose ls` for the live list.
|
||||||
|
- Tab-toggle selection state: which pane the cursor is in. `j`
|
||||||
|
/ `k` / arrow keys move within that pane.
|
||||||
|
- Rewire `_operator_edit_routes_flow` and
|
||||||
|
`_operator_edit_allowlist_flow` to accept an optional
|
||||||
|
"preselected slug". When the cursor is in the agents pane,
|
||||||
|
pass that slug; otherwise keep today's discover-and-prompt
|
||||||
|
behavior.
|
||||||
|
- Status-line indicator showing which agent is selected (or
|
||||||
|
"no agent selected" when in the proposals pane).
|
||||||
|
- Tests for the new discovery helper.
|
||||||
|
|
||||||
|
### Out of scope
|
||||||
|
|
||||||
|
- Changes to proposal handling (`a` / `m` / `r` / Enter all
|
||||||
|
unchanged).
|
||||||
|
- Changes to the queue-dir / supervise sidecar protocol.
|
||||||
|
- New CLI surface beyond what's in `./cli.py dashboard`.
|
||||||
|
- Touching the manifest, compose renderer, launch lifecycle.
|
||||||
|
|
||||||
|
## Proposed design
|
||||||
|
|
||||||
|
### Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
claude-bottle dashboard (3 pending, 2 active)
|
||||||
|
─────────────────────────────────────────────────────────
|
||||||
|
proposals:
|
||||||
|
03:14:22 [implementer-cy7a6] egress-block abc123…
|
||||||
|
03:13:55 [researcher-9xqs1] pipelock-block def456…
|
||||||
|
03:13:10 [implementer-cy7a6] capability-block ghi789…
|
||||||
|
|
||||||
|
active agents:
|
||||||
|
> implementer-cy7a6 implementer started 02:55:01 [pipelock,egress,git-gate,supervise]
|
||||||
|
researcher-9xqs1 researcher started 02:58:14 [pipelock,supervise]
|
||||||
|
|
||||||
|
[selected: implementer-cy7a6] q quit Tab switch j/k nav e routes p pipelock a/m/r/Enter
|
||||||
|
```
|
||||||
|
|
||||||
|
- One screen, two lists. Header counts both totals.
|
||||||
|
- A `>` cursor and reverse-video highlight mark the currently
|
||||||
|
selected row in the active pane.
|
||||||
|
- Status footer carries `[selected: <slug>]` (or `[no agent
|
||||||
|
selected]`) so it's always clear what `e` / `p` will target.
|
||||||
|
|
||||||
|
### Selection model
|
||||||
|
|
||||||
|
- `Tab` (or Shift-Tab) toggles which pane `j` / `k` /
|
||||||
|
arrow keys move through.
|
||||||
|
- Each pane keeps its own selection index. Switching panes
|
||||||
|
doesn't lose the position in the other.
|
||||||
|
- `e` / `p`:
|
||||||
|
- Cursor in the agents pane → use that agent's slug.
|
||||||
|
- Cursor in the proposals pane → today's discover-and-prompt
|
||||||
|
behavior (so the muscle memory still works for operators
|
||||||
|
who didn't switch panes first).
|
||||||
|
|
||||||
|
### Active-agent discovery
|
||||||
|
|
||||||
|
A new helper `discover_active_agents()` in dashboard.py
|
||||||
|
returns a list of `ActiveAgent(slug, agent_name, started_at,
|
||||||
|
services)`:
|
||||||
|
|
||||||
|
1. `list_active_slugs()` (already in
|
||||||
|
`backend/docker/compose.py`) → list of slugs.
|
||||||
|
2. For each slug: read `state/<slug>/metadata.json` →
|
||||||
|
`agent_name`, `started_at`.
|
||||||
|
3. For each slug: `docker compose -p <project> ps --format
|
||||||
|
json` → set of running service names.
|
||||||
|
|
||||||
|
Step 3 is the part that's per-bottle and could be slow on
|
||||||
|
hosts with many bottles. Open question below.
|
||||||
|
|
||||||
|
### Implementation chunks
|
||||||
|
|
||||||
|
Sized small.
|
||||||
|
|
||||||
|
1. **Discovery helper + dataclass.** Pure-ish: takes
|
||||||
|
`list_active_slugs()` as injected, reads metadata + queries
|
||||||
|
compose ps. Unit-test with mocked subprocess. No UI yet.
|
||||||
|
2. **Render the agents pane.** Wire `discover_active_agents`
|
||||||
|
into `_main_loop`'s tick, render below proposals, no
|
||||||
|
selection model yet (cursor stays in proposals).
|
||||||
|
3. **Selection state + Tab toggle.** Add the `which_pane`
|
||||||
|
variable, route `j/k/arrow` based on it, status footer.
|
||||||
|
4. **Agent-scoped `e` / `p`.** Pass selected slug into the
|
||||||
|
edit flows when the agents pane is focused; keep today's
|
||||||
|
global behavior when the proposals pane is focused.
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
1. **`compose ps` per bottle: too slow?** On a host with
|
||||||
|
10+ active bottles, calling `docker compose -p <X> ps` per
|
||||||
|
project on every 1s tick is 10+ subprocess calls per
|
||||||
|
second. Options: (a) cache the services list and refresh
|
||||||
|
on a slower cadence (e.g., every 5s); (b) skip the
|
||||||
|
per-bottle services column and just show the slug + agent
|
||||||
|
name; (c) one `docker ps --filter label=...` call that
|
||||||
|
buckets containers by `com.docker.compose.project` label.
|
||||||
|
Probably (c) — one call, no per-bottle fanout.
|
||||||
|
|
||||||
|
2. **What if `metadata.json` is missing or stale?** For a
|
||||||
|
bottle started by pre-chunk-3 code (no `compose_project`
|
||||||
|
field), or a state dir written by a tool we don't know
|
||||||
|
about, the metadata read can fail. Render with
|
||||||
|
`agent_name = ?` rather than dropping the row.
|
||||||
|
|
||||||
|
3. **Selection persistence across refresh ticks.** If the
|
||||||
|
currently-selected agent is no longer running (it exited
|
||||||
|
between ticks), the selection should fall back to the
|
||||||
|
previous row, not jump to the top. Mirrors the existing
|
||||||
|
proposals-list behavior.
|
||||||
|
|
||||||
|
4. **Should `e` / `p` still prompt when the cursor is in the
|
||||||
|
proposals pane but only one agent is running?** Today the
|
||||||
|
single-bottle case skips the prompt; we'd keep that. Two
|
||||||
|
or more bottles still prompts in proposals-pane mode.
|
||||||
|
|
||||||
|
5. **Color / highlight for the selected agent.** The proposals
|
||||||
|
pane uses green for newly-arrived. Agents could use a
|
||||||
|
different attribute (e.g., reverse video for selection,
|
||||||
|
no color for the row itself). Aesthetic decision; pick
|
||||||
|
something readable in the standard 8-color palette.
|
||||||
|
|
||||||
|
6. **Selecting a proposal cross-selects its agent?** Possible
|
||||||
|
UX: highlighting a proposal in the proposals pane could
|
||||||
|
auto-move the agents-pane cursor to that proposal's
|
||||||
|
bottle. Cute, but probably confusing — the explicit Tab
|
||||||
|
toggle is clearer. Out of v1.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- PRD 0013 — supervise sidecar (proposals + queue)
|
||||||
|
- PRD 0014 / 0015 / 0016 — the apply flows the edit verbs
|
||||||
|
drive
|
||||||
|
- PRD 0018 — compose-per-instance; `list_active_slugs` +
|
||||||
|
metadata.json source-of-truth
|
||||||
Reference in New Issue
Block a user