From db54f3d0b4c474fbdb3721fb9b145beae8c18b76 Mon Sep 17 00:00:00 2001 From: claude Date: Thu, 4 Jun 2026 01:46:57 +0000 Subject: [PATCH] docs(prd): add PRD 0051 (named/labelled agents, renumbered from 0049) --- docs/prds/0051-named-labelled-agents.md | 294 ++++++++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 docs/prds/0051-named-labelled-agents.md diff --git a/docs/prds/0051-named-labelled-agents.md b/docs/prds/0051-named-labelled-agents.md new file mode 100644 index 0000000..dfcf543 --- /dev/null +++ b/docs/prds/0051-named-labelled-agents.md @@ -0,0 +1,294 @@ +# PRD 0051: Named / Labelled Agents + +- **Status:** Draft +- **Author:** didericis +- **Created:** 2026-06-03 +- **Issue:** #171 + +## Summary + +At agent launch time, prompt the operator for a short human-readable label +(defaulting to the manifest agent key) and an optional color from the 16-color +ANSI palette. Store both in the bottle's `metadata.json`. Display the label — +rendered in the chosen color — in the dashboard's active-agents pane, replacing +the bare manifest key. Inject the label and color into the in-container +`claude.json` as `name` / `color` so Claude Code can surface them in its own +harness when upstream support lands. + +## Problem + +The dashboard's agents pane identifies each running instance by its manifest +agent key (e.g., `implementer`) plus a random slug suffix. When an operator +runs three `implementer` bottles simultaneously — one each for three different +repos — the pane shows: + +``` + [docker] a3f9 implementer started 14:02:11 [egress,pipelock] + [docker] b81c implementer started 14:03:45 [egress,pipelock] + [docker] d220 implementer started 14:05:01 [egress,pipelock] +``` + +There is no way to tell which bottle is working on which task without attaching +to each one in turn. The slug is opaque; the manifest key is shared. Operators +working a multi-bottle session resort to keeping a mental map of slug→task, +which breaks the moment they switch windows. + +## Goals / Success Criteria + +1. After the operator selects an agent name (dashboard picker or CLI argument), + they are prompted for a label. The prompt suggests the manifest key as the + default; pressing Enter (or providing no input) accepts it. The label may + contain any printable characters up to 64 bytes. +2. After the label prompt, the operator is optionally prompted for a color from + the 16-color ANSI palette (names: `black`, `red`, `green`, `yellow`, `blue`, + `magenta`, `cyan`, `white`, `bright-black`, `bright-red`, `bright-green`, + `bright-yellow`, `bright-blue`, `bright-magenta`, `bright-cyan`, + `bright-white`). Pressing Enter without a selection skips color entirely. +3. `label` and `color` are stored in `BottleMetadata` and written to the + bottle's `metadata.json`. Both fields default to `""` (empty / unset). +4. `ActiveAgent` carries `label` and `color`; `enumerate_active()` reads them + from `metadata.json`. +5. `_format_agent_row` uses the label when non-empty (falling back to + `agent_name`). If a non-empty color is set and the terminal supports it, the + label substring is rendered in that color. +6. `BottleSpec` carries `label` and `color`; the docker backend's `prepare` + step copies them into `BottleMetadata`. +7. `ClaudeAgentProvider.provision_plan()` in + `bot_bottle/contrib/claude/agent_provider.py` writes `label` → `"name"` and + `color` → `"color"` into the generated `claude.json`, alongside the existing + fields. Fields are omitted when empty. +8. The dashboard's `_new_agent_flow` (PRD 0020) includes the label+color step + between agent selection and the backend picker. +9. `cmd_start` (CLI) includes the label+color step after argument validation + and before prepare-with-preflight. +10. All existing unit tests stay green; no new tests are required for this + change (the label/color fields are thin plumbing with no branching logic + worth unit-testing beyond the already-tested metadata read/write path). + +## Non-goals + +- Showing the agent label inside the Claude Code TUI (status line, terminal + title, custom header). That requires upstream Claude Code / codex support. + Writing to `claude.json` is best-effort scaffolding for when that lands. +- Per-bottle color affecting anything outside the dashboard agents pane (e.g., + proposal-pane highlights, log prefixes). +- Validating or constraining label content beyond the 64-byte printable cap. +- Persisting color-pair state across dashboard restarts (color pairs are + initialized fresh each session). +- Editing the label or color of an already-running bottle. +- Exposing label/color via `./cli.py list` (out of scope for v1; trivial to + add later since the field will be in metadata). + +## Design + +### Data flow + +``` +operator input + │ + ▼ +BottleSpec.label, BottleSpec.color + │ + ├─► docker/prepare.py → BottleMetadata.label / .color → metadata.json + │ + └─► contrib/claude/agent_provider.py → claude.json {"name": label, "color": color} + (omitted when empty) + +dashboard refresh + │ + ▼ +enumerate_active() → read_metadata(slug) → ActiveAgent.label / .color + │ + ▼ +_format_agent_row → label (colored) in the row string +``` + +### BottleSpec changes + +```python +@dataclass(frozen=True) +class BottleSpec: + manifest: Manifest + agent_name: str + copy_cwd: bool + user_cwd: str + identity: str = "" + label: str = "" # operator-chosen display name; defaults to agent_name at render time + color: str = "" # one of the 16 ANSI color names, or "" for terminal default +``` + +`label` and `color` default to `""` so all existing callers remain valid with +no changes. + +### BottleMetadata changes + +Add two new fields with backward-compatible defaults: + +```python +@dataclass +class BottleMetadata: + identity: str + agent_name: str + cwd: str + copy_cwd: bool + started_at: str + compose_project: str + backend: str + label: str = "" + color: str = "" +``` + +`metadata.json` written by older bot-bottle versions won't have these keys; +`read_metadata` already uses `dict.get` with defaults, so existing slugs load +cleanly with `label=""`, `color=""`. + +### ActiveAgent changes + +```python +@dataclass(frozen=True) +class ActiveAgent: + backend_name: str + slug: str + agent_name: str + started_at: str + services: tuple[str, ...] + label: str = "" + color: str = "" +``` + +`enumerate_active()` copies `label` and `color` out of `BottleMetadata` when +constructing each `ActiveAgent`. The smolmachines backend gets the same +additions for symmetry; it reads from its own metadata path. + +### Dashboard row rendering + +`_format_agent_row` already falls through cleanly on missing fields. The +change is: + +```python +display_name = a.label if a.label else a.agent_name +``` + +Color rendering uses the existing `_try_init_green()` pattern as a model. +A `_color_pair_for(color_name)` helper initialises a fresh curses color pair +for the requested named color and returns its attr (or 0 on failure). Each +unique color in the active agent list gets its own pair index. Color pairs are +allocated lazily and cached in a `dict[str, int]` that lives for the duration +of the dashboard session. + +The 16 ANSI color name → curses constant mapping: + +| Name | curses constant | +|------|----------------| +| `black` | `curses.COLOR_BLACK` | +| `red` | `curses.COLOR_RED` | +| `green` | `curses.COLOR_GREEN` | +| `yellow` | `curses.COLOR_YELLOW` | +| `blue` | `curses.COLOR_BLUE` | +| `magenta` | `curses.COLOR_MAGENTA` | +| `cyan` | `curses.COLOR_CYAN` | +| `white` | `curses.COLOR_WHITE` | +| `bright-*` | same constant + `curses.A_BOLD` | + +Terminals that don't support color fall back to plain text (the helper returns +0, which ORed in is a no-op — same pattern as `_try_init_green`). + +### Label + color prompt — dashboard + +In `_new_agent_flow`, after `_picker_modal` returns a non-None name and before +`_backend_picker_modal`: + +```python +label, color = _label_color_modal(stdscr, default_label=picked) +``` + +`_label_color_modal` uses `curses.endwin()` → text-mode prompts → restore +(the same drop-and-resume pattern as the existing editor flow and preflight +Y/N). Two sequential prompts: + +``` +bot-bottle: agent label [implementer]: +bot-bottle: color (red/green/blue/… or Enter to skip): +``` + +Invalid color names are silently ignored (treated as empty). The function +returns `(label, color)` — both strings, both possibly `""`. + +### Label + color prompt — CLI + +In `cmd_start`, after argument parsing and before `_launch_bottle`: + +```python +label = _text_prompt_label(args.name) +color = _text_prompt_color() +``` + +`_text_prompt_label(default)` writes `"bot-bottle: agent label [{default}]: "` +to stderr and returns the stripped input (or `default` if blank). +`_text_prompt_color()` writes the color prompt and returns the stripped input +(or `""` if blank or invalid). + +Both use `read_tty_line()` (already in `start.py`) for the read. + +### Claude Code config injection + +Per PRD 0050, the `claude.json` trust-marker file is written by +`ClaudeAgentProvider.provision_plan()` in +`bot_bottle/contrib/claude/agent_provider.py`. Add `label: str = ""` and +`color: str = ""` keyword parameters to `provision_plan()` on both the +`AgentProvider` ABC and `ClaudeAgentProvider`, and to the +`agent_provision_plan()` shim in `agent_provider.py`. The docker and +smolmachines `prepare.py` modules pass `spec.label` / `spec.color` when +calling `agent_provision_plan()`; other providers accept the params and ignore +them. + +In `ClaudeAgentProvider.provision_plan()`, expand the JSON payload +conditionally: + +```python +payload = { + "hasCompletedOnboarding": True, + "theme": "dark", + "bypassPermissionsModeAccepted": True, + "projects": claude_projects, +} +if label: + payload["name"] = label +if color: + payload["color"] = color +claude_config.write_text(json.dumps(payload, indent=2) + "\n") +``` + +## Implementation chunks + +Two PRs, each independently mergeable. + +### Chunk 1 — schema + storage + +- Add `label: str = ""` and `color: str = ""` to `BottleSpec`, + `BottleMetadata`, and `ActiveAgent`. +- `docker/prepare.py`: copy `spec.label` / `spec.color` into `BottleMetadata`. +- `docker/enumerate.py`: copy `metadata.label` / `metadata.color` into + `ActiveAgent`. +- Add `label: str = ""` and `color: str = ""` keyword params to + `AgentProvider.provision_plan()` (ABC), `ClaudeAgentProvider.provision_plan()` + (uses them in the `claude.json` write), and the `agent_provision_plan()` shim. + Other providers (`CodexAgentProvider`) accept the params and ignore them. +- `docker/prepare.py` and `smolmachines/prepare.py`: pass `spec.label` / + `spec.color` to `agent_provision_plan()`. +- Smolmachines backend: parallel changes to metadata read/write and + `ActiveAgent` construction. +- No prompt changes; no UI changes. All existing behavior is identical. + +### Chunk 2 — prompts + display + +- `start.py`: add `_text_prompt_label` and `_text_prompt_color`; call them in + `cmd_start` before `_launch_bottle`; pass `label` / `color` into `BottleSpec`. +- `dashboard.py`: add `_label_color_modal` (drop-and-resume); call it in + `_new_agent_flow`; pass label/color into `BottleSpec`; add + `_color_pair_for` helper; update `_format_agent_row` to use `a.label` with + color rendering. + +## Open questions + +None.