# PRD 0054: Named / Labelled Agents - **Status:** Active - **Author:** didericis - **Created:** 2026-06-03 - **Issue:** #171 ## Summary At agent launch time, present the operator with a curses modal to optionally set a human-readable label and color for the agent before it launches. The modal pre-fills the label with the current agent name pattern (e.g. `implementer-a3f9`) and leaves color unset; Enter with no changes accepts those defaults. Store both in the bottle's `metadata.json`. Display the label — rendered in the chosen ANSI color — in `cli list active` output, 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 `cli list active` 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 output shows: ``` docker a3f9 implementer egress,pipelock docker b81c implementer egress,pipelock docker d220 implementer 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 (picker or CLI argument) and backend, a curses modal appears before the preflight. The modal pre-fills the label with `-` (the same pattern currently shown in `list active`). No color is pre-selected. 2. In the modal, any printable keystroke immediately replaces the pre-filled label and starts building the new name. Backspace edits normally. Enter at any point confirms — accepting the pre-fill if nothing was typed, or the in-progress text otherwise. 3. After the label field is confirmed, the modal presents color selection: a list of the 16 ANSI color names the operator can navigate with arrow keys, or Enter / Esc with no selection to skip color entirely. 4. `label` and `color` are stored in `BottleMetadata` and written to the bottle's `metadata.json`. Both fields default to `""` (empty / unset). 5. `ActiveAgent` carries `label` and `color`; `enumerate_active()` reads them from `metadata.json`. 6. `cli list active` shows the label when non-empty (falling back to `agent_name`). If a non-empty color is set and the terminal supports it, the label is prefixed with the appropriate ANSI escape code and reset afterward. 7. `BottleSpec` carries `label` and `color`; both backends' `prepare` steps copy them into `BottleMetadata`. 8. `ClaudeAgentProvider.provision_plan()` writes `label` → `"name"` and `color` → `"color"` into the generated `claude.json`. Fields are omitted when empty. 9. `cmd_start` calls `name_color_modal` after backend selection and before `_launch_bottle`; passes `label` / `color` into `BottleSpec`. 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. - Validating or constraining label content beyond the 64-byte printable cap. - Editing the label or color of an already-running bottle. ## Design ### Data flow ``` operator input (modal) │ ▼ BottleSpec.label, BottleSpec.color │ ├─► docker/prepare.py → BottleMetadata.label / .color → metadata.json ├─► smolmachines/prepare.py → BottleMetadata.label / .color → metadata.json │ └─► contrib/claude/agent_provider.py → claude.json {"name": label, "color": color} (omitted when empty) cli list active │ ▼ enumerate_active() → read_metadata(slug) → ActiveAgent.label / .color │ ▼ cmd_list → label (with ANSI color) 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. ### `cli list active` rendering The current row format is tab-separated: `{backend}\t{slug}\t{agent_name}\t{services}` With labels it becomes: ```python display_name = a.label if a.label else a.agent_name ``` Color is rendered via ANSI escape codes. A small `_ansi_color(color_name)` helper returns the appropriate escape prefix for the 16 named colors, or `""` when the name is unrecognised or the terminal doesn't support color (`NO_COLOR` env var or `not sys.stdout.isatty()`). The 16 ANSI color name → escape mapping: | Name | ANSI code | |------|-----------| | `black` | `\033[30m` | | `red` | `\033[31m` | | `green` | `\033[32m` | | `yellow` | `\033[33m` | | `blue` | `\033[34m` | | `magenta` | `\033[35m` | | `cyan` | `\033[36m` | | `white` | `\033[37m` | | `bright-black` | `\033[90m` | | `bright-red` | `\033[91m` | | `bright-green` | `\033[92m` | | `bright-yellow` | `\033[93m` | | `bright-blue` | `\033[94m` | | `bright-magenta` | `\033[95m` | | `bright-cyan` | `\033[96m` | | `bright-white` | `\033[97m` | Reset is `\033[0m`. Applied around the label substring only. ### The label+color modal A single curses modal (`name_color_modal` in `bot_bottle/cli/tui.py`) handles both label and color in two sequential steps within the same window. ```python label, color = name_color_modal(default_label=f"{agent_name}-{slug_suffix}") ``` **Step 1 — label.** The window renders: ``` Name agent ────────────────────────────────────── implementer-a3f9 ────────────────────────────────────── [any key] edit [Enter] confirm ``` The pre-filled text is shown in the input field. Any printable keystroke immediately clears the pre-fill and starts a new name from that character (first-keystroke-replaces semantics). Subsequent keystrokes append normally. Backspace edits from the right. Enter confirms — accepting the pre-fill if the field was never edited, or the typed text otherwise. **Step 2 — color.** After confirming the label, the window transitions to: ``` Name agent ────────────────────────────────────── implementer-a3f9 ← confirmed label ────────────────────────────────────── Color (optional) > (none) red green blue … ────────────────────────────────────── [↑↓] move [Enter] select [Esc] skip ``` The list starts with `(none)` selected. Arrow keys move the cursor; Enter confirms the highlighted choice; Esc or `q` skips color. Each color name in the list is rendered in its own curses color so the operator can preview the palette. The function returns `(label, color)` — both strings, `color` is `""` when `(none)` is selected or the step is skipped. ### Slug suffix for the default label The default label is `-`, where `slug_suffix` is the last four characters of the slug (the same short hash shown in `list active`). In `cmd_start` the slug is minted inside `prepare`, after the modal appears. The modal is therefore called with the manifest agent key as a fallback (`default_label=agent_name`). Once `prepare` returns the plan (which contains the slug), the `BottleSpec` is not reconstructed — the label entered by the operator is already in the spec. The full `-` form is only available for display in subsequent `list active` calls once the bottle is running. ### 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`. Both `prepare.py` modules pass `spec.label` / `spec.color`; `CodexAgentProvider` accepts the params and ignores them. In `ClaudeAgentProvider.provision_plan()`: ```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` and `smolmachines/prepare.py`: copy `spec.label` / `spec.color` into `BottleMetadata`; pass them to `agent_provision_plan()`. - `docker/enumerate.py` and smolmachines equivalent: 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. `CodexAgentProvider` accepts the params and ignores them. - `cmd_list`: update `list active` row to use `label` when non-empty, with ANSI color escape codes. - No prompt changes; no UI changes. All existing behavior is identical. ### Chunk 2 — modal - `bot_bottle/cli/tui.py`: add `name_color_modal(default_label)` implementing the two-step curses window described above. - `cmd_start`: call `name_color_modal(default_label=agent_name)` after backend selection and before `_launch_bottle`; pass `label` / `color` into `BottleSpec`. ## Open questions None.