From db54f3d0b4c474fbdb3721fb9b145beae8c18b76 Mon Sep 17 00:00:00 2001 From: claude Date: Thu, 4 Jun 2026 01:46:57 +0000 Subject: [PATCH 1/6] 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. -- 2.52.0 From 3a10c38511444eed9b898c83385ba0e18dab8d1c Mon Sep 17 00:00:00 2001 From: didericis Date: Sat, 6 Jun 2026 16:24:27 -0400 Subject: [PATCH 2/6] =?UTF-8?q?docs(prd):=20renumber=20PRD=200051=20?= =?UTF-8?q?=E2=86=92=200054=20(0051=20slot=20taken=20by=20launch-selector?= =?UTF-8?q?=20on=20main)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...1-named-labelled-agents.md => 0054-named-labelled-agents.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename docs/prds/{0051-named-labelled-agents.md => 0054-named-labelled-agents.md} (99%) diff --git a/docs/prds/0051-named-labelled-agents.md b/docs/prds/0054-named-labelled-agents.md similarity index 99% rename from docs/prds/0051-named-labelled-agents.md rename to docs/prds/0054-named-labelled-agents.md index dfcf543..ea778d0 100644 --- a/docs/prds/0051-named-labelled-agents.md +++ b/docs/prds/0054-named-labelled-agents.md @@ -1,4 +1,4 @@ -# PRD 0051: Named / Labelled Agents +# PRD 0054: Named / Labelled Agents - **Status:** Draft - **Author:** didericis -- 2.52.0 From 299579ab7bfd1c25327abda907c03323b7ef954c Mon Sep 17 00:00:00 2001 From: didericis Date: Sat, 6 Jun 2026 22:10:26 -0400 Subject: [PATCH 3/6] ci(prd): rename PRD to prd-new placeholder per new convention --- ...amed-labelled-agents.md => prd-new-named-labelled-agents.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename docs/prds/{0054-named-labelled-agents.md => prd-new-named-labelled-agents.md} (99%) diff --git a/docs/prds/0054-named-labelled-agents.md b/docs/prds/prd-new-named-labelled-agents.md similarity index 99% rename from docs/prds/0054-named-labelled-agents.md rename to docs/prds/prd-new-named-labelled-agents.md index ea778d0..b5453ad 100644 --- a/docs/prds/0054-named-labelled-agents.md +++ b/docs/prds/prd-new-named-labelled-agents.md @@ -1,4 +1,4 @@ -# PRD 0054: Named / Labelled Agents +# PRD prd-new: Named / Labelled Agents - **Status:** Draft - **Author:** didericis -- 2.52.0 From 39e0976ace89583b3b95655aaee61038bf30bb42 Mon Sep 17 00:00:00 2001 From: didericis Date: Sun, 7 Jun 2026 12:01:11 -0400 Subject: [PATCH 4/6] docs(prd): redesign label+color prompt as a curses modal window - Single modal with two steps (label then color) instead of bare text prompts dropped to terminal - Default label is -; first keystroke replaces the pre-fill rather than appending to it - Color step shows a navigable list with live color preview; (none) selected by default; Esc skips - Modal lives in tui.py and is shared between supervisor flow and cmd_start Co-Authored-By: Claude Sonnet 4.6 --- docs/prds/prd-new-named-labelled-agents.md | 213 ++++++++++++--------- 1 file changed, 119 insertions(+), 94 deletions(-) diff --git a/docs/prds/prd-new-named-labelled-agents.md b/docs/prds/prd-new-named-labelled-agents.md index b5453ad..b15183b 100644 --- a/docs/prds/prd-new-named-labelled-agents.md +++ b/docs/prds/prd-new-named-labelled-agents.md @@ -7,17 +7,19 @@ ## 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. +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 color — in the supervisor'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 +The supervisor'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: @@ -35,33 +37,34 @@ 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 +1. After the operator selects an agent (supervisor picker or CLI argument), a + curses modal appears before the backend picker / preflight. The modal + pre-fills the label with `-` (the same pattern + currently shown in the agents pane). 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 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). -4. `ActiveAgent` carries `label` and `color`; `enumerate_active()` reads them +5. `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 +6. The supervisor's 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. +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. The supervisor's `_new_agent_flow` includes the modal between agent + selection and the backend picker. +10. `cmd_start` (CLI) shows the same modal (via the shared `tui` module) + before `_launch_bottle`; passes `label` / `color` into `BottleSpec`. +11. 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). @@ -70,37 +73,36 @@ which breaks the moment they switch windows. - 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). +- Per-bottle color affecting anything outside the supervisor agents pane. - Validating or constraining label content beyond the 64-byte printable cap. -- Persisting color-pair state across dashboard restarts (color pairs are +- Persisting color-pair state across supervisor 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). +- Exposing label/color via `./cli.py list` (out of scope for v1). ## Design ### Data flow ``` -operator input +operator input (modal) │ ▼ BottleSpec.label, BottleSpec.color │ - ├─► docker/prepare.py → BottleMetadata.label / .color → metadata.json + ├─► 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) -dashboard refresh +supervisor refresh │ ▼ enumerate_active() → read_metadata(slug) → ActiveAgent.label / .color │ ▼ -_format_agent_row → label (colored) in the row string +agent row → label (colored) in the row string ``` ### BottleSpec changes @@ -158,12 +160,11 @@ class ActiveAgent: `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. +additions for symmetry. -### Dashboard row rendering +### Agent row rendering -`_format_agent_row` already falls through cleanly on missing fields. The -change is: +The display name logic becomes: ```python display_name = a.label if a.label else a.agent_name @@ -171,10 +172,9 @@ 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. +for the requested named color and returns its attr (or 0 on failure). Color +pairs are allocated lazily and cached in a `dict[str, int]` that lives for +the duration of the supervisor session. The 16 ANSI color name → curses constant mapping: @@ -191,44 +191,72 @@ The 16 ANSI color name → curses constant mapping: | `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`). +0, which ORed in is a no-op). -### Label + color prompt — dashboard +### The label+color modal -In `_new_agent_flow`, after `_picker_modal` returns a non-None name and before -`_backend_picker_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. It is +called identically from the supervisor flow and from `cmd_start`. ```python -label, color = _label_color_modal(stdscr, default_label=picked) +label, color = name_color_modal(default_label=f"{agent_name}-{slug_suffix}") ``` -`_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: +**Step 1 — label.** The window renders: ``` -bot-bottle: agent label [implementer]: -bot-bottle: color (red/green/blue/… or Enter to skip): + Name agent + ────────────────────────────────────── + implementer-a3f9 + ────────────────────────────────────── + [any key] edit [Enter] confirm ``` -Invalid color names are silently ignored (treated as empty). The function -returns `(label, color)` — both strings, both possibly `""`. +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. -### Label + color prompt — CLI +**Step 2 — color.** After confirming the label, the window transitions to: -In `cmd_start`, after argument parsing and before `_launch_bottle`: - -```python -label = _text_prompt_label(args.name) -color = _text_prompt_color() +``` + Name agent + ────────────────────────────────────── + implementer-a3f9 ← confirmed label + ────────────────────────────────────── + Color (optional) + > (none) + red + green + blue + … + ────────────────────────────────────── + [↑↓] move [Enter] select [Esc] skip ``` -`_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). +The list starts with `(none)` selected. Arrow keys move the cursor; Enter +confirms the highlighted choice; Esc or `q` skips color (equivalent to +selecting `(none)`). Each color name in the list is rendered in its own color +so the operator can preview the palette. -Both use `read_tty_line()` (already in `start.py`) for the read. +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 the agents +pane). Both the supervisor and `cmd_start` have access to the slug after +`bottle_identity()` is called; the default label is computed there and passed +to `name_color_modal`. + +In `cmd_start` the slug is not yet known at the time the modal appears (it is +minted inside `prepare`). The modal is therefore called with +`default_label=args.name` (the manifest agent key) as a simpler fallback; the +supervisor path, which has the slug available from `_new_agent_flow`, uses the +full `-` form. ### Claude Code config injection @@ -237,13 +265,11 @@ Per PRD 0050, the `claude.json` trust-marker file is written by `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. +`agent_provision_plan()` shim in `agent_provider.py`. Both `prepare.py` +modules pass `spec.label` / `spec.color`; other providers accept the params +and ignore them. -In `ClaudeAgentProvider.provision_plan()`, expand the JSON payload -conditionally: +In `ClaudeAgentProvider.provision_plan()`: ```python payload = { @@ -267,27 +293,26 @@ Two PRs, each independently mergeable. - 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`. +- `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. - 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. + `CodexAgentProvider` accepts the params and ignores them. - No prompt changes; no UI changes. All existing behavior is identical. -### Chunk 2 — prompts + display +### Chunk 2 — modal + 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. +- `bot_bottle/cli/tui.py`: add `name_color_modal(default_label)` implementing + the two-step curses window described above. +- Supervisor `_new_agent_flow`: call `name_color_modal` after agent selection, + pass `label` / `color` into `BottleSpec`. +- `cmd_start`: call `name_color_modal(default_label=args.name)` before + `_launch_bottle`; pass `label` / `color` into `BottleSpec`. +- Supervisor agent row: add `_color_pair_for` helper; update row rendering to + use `a.label` with color. ## Open questions -- 2.52.0 From f6f47c2f238e1081c527501e9ab9dffaef4990a4 Mon Sep 17 00:00:00 2001 From: didericis Date: Sun, 7 Jun 2026 12:04:17 -0400 Subject: [PATCH 5/6] docs(prd): remove dashboard references, align with current codebase - Dashboard no longer exists; remove all references to it - Active agent display surface is cli list active, not a TUI pane - Label/color rendered with ANSI escape codes in list output - Modal called from cmd_start only, no supervisor _new_agent_flow - Remove _format_agent_row/_color_pair_for curses design (list is plain text); add _ansi_color() helper design instead - Clarify slug-suffix caveat: modal appears before prepare() mints the slug so default label falls back to agent_name Co-Authored-By: Claude Sonnet 4.6 --- docs/prds/prd-new-named-labelled-agents.md | 145 ++++++++++----------- 1 file changed, 72 insertions(+), 73 deletions(-) diff --git a/docs/prds/prd-new-named-labelled-agents.md b/docs/prds/prd-new-named-labelled-agents.md index b15183b..c75a545 100644 --- a/docs/prds/prd-new-named-labelled-agents.md +++ b/docs/prds/prd-new-named-labelled-agents.md @@ -12,22 +12,22 @@ 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 color — in the supervisor'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. +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 -The supervisor'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: +`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 started 14:02:11 [egress,pipelock] - [docker] b81c implementer started 14:03:45 [egress,pipelock] - [docker] d220 implementer started 14:05:01 [egress,pipelock] +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 @@ -37,34 +37,33 @@ which breaks the moment they switch windows. ## Goals / Success Criteria -1. After the operator selects an agent (supervisor picker or CLI argument), a - curses modal appears before the backend picker / preflight. The modal - pre-fills the label with `-` (the same pattern - currently shown in the agents pane). No color is pre-selected. +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 with no selection to skip color entirely. + 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. The supervisor's agent row uses the label when non-empty (falling back to +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 substring is rendered in that color. + 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. The supervisor's `_new_agent_flow` includes the modal between agent - selection and the backend picker. -10. `cmd_start` (CLI) shows the same modal (via the shared `tui` module) - before `_launch_bottle`; passes `label` / `color` into `BottleSpec`. -11. All existing unit tests stay green; no new tests are required for this +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). @@ -73,12 +72,8 @@ which breaks the moment they switch windows. - 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 supervisor agents pane. - Validating or constraining label content beyond the 64-byte printable cap. -- Persisting color-pair state across supervisor 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). ## Design @@ -96,13 +91,13 @@ BottleSpec.label, BottleSpec.color └─► contrib/claude/agent_provider.py → claude.json {"name": label, "color": color} (omitted when empty) -supervisor refresh +cli list active │ ▼ enumerate_active() → read_metadata(slug) → ActiveAgent.label / .color │ ▼ -agent row → label (colored) in the row string +cmd_list → label (with ANSI color) in the row string ``` ### BottleSpec changes @@ -162,42 +157,48 @@ class ActiveAgent: constructing each `ActiveAgent`. The smolmachines backend gets the same additions for symmetry. -### Agent row rendering +### `cli list active` rendering -The display name logic becomes: +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 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). Color -pairs are allocated lazily and cached in a `dict[str, int]` that lives for -the duration of the supervisor session. +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 → curses constant mapping: +The 16 ANSI color name → escape 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` | +| 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` | -Terminals that don't support color fall back to plain text (the helper returns -0, which ORed in is a no-op). +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. It is -called identically from the supervisor flow and from `cmd_start`. +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}") @@ -237,9 +238,9 @@ the field was never edited, or the typed text otherwise. ``` The list starts with `(none)` selected. Arrow keys move the cursor; Enter -confirms the highlighted choice; Esc or `q` skips color (equivalent to -selecting `(none)`). Each color name in the list is rendered in its own color -so the operator can preview the palette. +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. @@ -247,16 +248,15 @@ The function returns `(label, color)` — both strings, `color` is `""` when ### 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 the agents -pane). Both the supervisor and `cmd_start` have access to the slug after -`bottle_identity()` is called; the default label is computed there and passed -to `name_color_modal`. +last four characters of the slug (the same short hash shown in `list active`). -In `cmd_start` the slug is not yet known at the time the modal appears (it is -minted inside `prepare`). The modal is therefore called with -`default_label=args.name` (the manifest agent key) as a simpler fallback; the -supervisor path, which has the slug available from `_new_agent_flow`, uses the -full `-` form. +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 @@ -266,8 +266,8 @@ Per PRD 0050, the `claude.json` trust-marker file is written by `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`; other providers accept the params -and ignore them. +modules pass `spec.label` / `spec.color`; `CodexAgentProvider` accepts the +params and ignores them. In `ClaudeAgentProvider.provision_plan()`: @@ -301,18 +301,17 @@ Two PRs, each independently mergeable. `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 + display +### Chunk 2 — modal - `bot_bottle/cli/tui.py`: add `name_color_modal(default_label)` implementing the two-step curses window described above. -- Supervisor `_new_agent_flow`: call `name_color_modal` after agent selection, - pass `label` / `color` into `BottleSpec`. -- `cmd_start`: call `name_color_modal(default_label=args.name)` before - `_launch_bottle`; pass `label` / `color` into `BottleSpec`. -- Supervisor agent row: add `_color_pair_for` helper; update row rendering to - use `a.label` with color. +- `cmd_start`: call `name_color_modal(default_label=agent_name)` after backend + selection and before `_launch_bottle`; pass `label` / `color` into + `BottleSpec`. ## Open questions -- 2.52.0 From 04d7ca2e6a954fd0fde031339ba8d830f24f08d7 Mon Sep 17 00:00:00 2001 From: didericis Date: Sun, 7 Jun 2026 12:12:32 -0400 Subject: [PATCH 6/6] feat(agents): named and labelled agents with optional ANSI color MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chunk 1 (schema + storage): BottleSpec, ActiveAgent, and BottleMetadata gain label and color fields. Both docker and smolmachines backends persist them to metadata.json on prepare and surface them in enumerate_active_agents(). AgentProvider.provision_plan() passes label/color through to the Claude provider, which injects them into claude.json so claude-code displays the session name and color in its header. Codex provider accepts and ignores the knobs. Chunk 2 (curses modal + display): cmd_start presents a two-step curses modal — first edit the label (first keystroke replaces the pre-fill), then optionally pick a color. cli list active renders label with ANSI escape codes when the terminal supports it, falling back to agent_name when no label is set. Co-Authored-By: Claude Sonnet 4.6 --- bot_bottle/agent_provider.py | 6 + bot_bottle/backend/__init__.py | 4 + bot_bottle/backend/docker/bottle_state.py | 4 + bot_bottle/backend/docker/enumerate.py | 2 + bot_bottle/backend/docker/prepare.py | 4 + bot_bottle/backend/smolmachines/enumerate.py | 2 + bot_bottle/backend/smolmachines/prepare.py | 4 + bot_bottle/cli/list.py | 45 +++- bot_bottle/cli/start.py | 4 + bot_bottle/cli/tui.py | 217 +++++++++++++++++++ bot_bottle/contrib/claude/agent_provider.py | 11 +- bot_bottle/contrib/codex/agent_provider.py | 4 +- 12 files changed, 299 insertions(+), 8 deletions(-) diff --git a/bot_bottle/agent_provider.py b/bot_bottle/agent_provider.py index dcc79b2..4384d98 100644 --- a/bot_bottle/agent_provider.py +++ b/bot_bottle/agent_provider.py @@ -139,6 +139,8 @@ class AgentProvider(ABC): forward_host_credentials: bool = False, host_env: dict[str, str] | None = None, trusted_project_path: str = "", + label: str = "", + color: str = "", ) -> AgentProvisionPlan: """Build the declarative AgentProvisionPlan for one launch. Backends call this during `prepare` and consume the result as @@ -326,6 +328,8 @@ def agent_provision_plan( forward_host_credentials: bool = False, host_env: dict[str, str] | None = None, trusted_project_path: str = "", + label: str = "", + color: str = "", ) -> AgentProvisionPlan: """Back-compat shim — `prepare` callers stay the same; the work now lives on the provider plugin.""" @@ -338,6 +342,8 @@ def agent_provision_plan( forward_host_credentials=forward_host_credentials, host_env=host_env, trusted_project_path=trusted_project_path, + label=label, + color=color, ) diff --git a/bot_bottle/backend/__init__.py b/bot_bottle/backend/__init__.py index 52b5f56..1912809 100644 --- a/bot_bottle/backend/__init__.py +++ b/bot_bottle/backend/__init__.py @@ -67,6 +67,8 @@ class BottleSpec: # (`cli.py resume `) sets this to continue an existing # bottle's state. Empty string for a fresh `start`. identity: str = "" + label: str = "" + color: str = "" @dataclass(frozen=True) @@ -189,6 +191,8 @@ class ActiveAgent: agent_name: str # from metadata.json; "?" if missing started_at: str # ISO 8601 from metadata.json; "" if missing services: tuple[str, ...] # alphabetical + label: str = "" + color: str = "" class Bottle(ABC): diff --git a/bot_bottle/backend/docker/bottle_state.py b/bot_bottle/backend/docker/bottle_state.py index 673a278..80c1eea 100644 --- a/bot_bottle/backend/docker/bottle_state.py +++ b/bot_bottle/backend/docker/bottle_state.py @@ -109,6 +109,8 @@ class BottleMetadata: # for state dirs written before PRD 0040; callers default to "docker" # for backward compatibility. backend: str = "" + label: str = "" + color: str = "" def metadata_path(identity: str) -> Path: @@ -144,6 +146,8 @@ def read_metadata(identity: str) -> BottleMetadata | None: started_at=str(raw_typed.get("started_at", "")), compose_project=str(raw_typed.get("compose_project", "")), backend=str(raw_typed.get("backend", "")), + label=str(raw_typed.get("label", "")), + color=str(raw_typed.get("color", "")), ) diff --git a/bot_bottle/backend/docker/enumerate.py b/bot_bottle/backend/docker/enumerate.py index b57fc83..9348d82 100644 --- a/bot_bottle/backend/docker/enumerate.py +++ b/bot_bottle/backend/docker/enumerate.py @@ -39,6 +39,8 @@ def enumerate_active() -> list[ActiveAgent]: agent_name=metadata.agent_name if metadata else "?", started_at=metadata.started_at if metadata else "", services=tuple(sorted(services)), + label=metadata.label if metadata else "", + color=metadata.color if metadata else "", )) return out diff --git a/bot_bottle/backend/docker/prepare.py b/bot_bottle/backend/docker/prepare.py index 19a130e..73ce414 100644 --- a/bot_bottle/backend/docker/prepare.py +++ b/bot_bottle/backend/docker/prepare.py @@ -80,6 +80,8 @@ def resolve_plan( started_at=datetime.now(timezone.utc).isoformat(), compose_project=f"bot-bottle-{slug}", backend="docker", + label=spec.label, + color=spec.color, )) # Clear any leftover preserve marker from a prior capability-block # so this fresh launch can be cleaned up at session-end unless @@ -191,6 +193,8 @@ def resolve_plan( auth_token=provider.auth_token, host_env=dict(os.environ), trusted_project_path=workspace_plan.workdir, + label=spec.label, + color=spec.color, ) guest_env = dict(agent_provision.guest_env) for key, val in agent_provision.env_vars.items(): diff --git a/bot_bottle/backend/smolmachines/enumerate.py b/bot_bottle/backend/smolmachines/enumerate.py index f1a81ff..c01e4ac 100644 --- a/bot_bottle/backend/smolmachines/enumerate.py +++ b/bot_bottle/backend/smolmachines/enumerate.py @@ -64,6 +64,8 @@ def enumerate_active() -> list[ActiveAgent]: agent_name=metadata.agent_name if metadata else "?", started_at=metadata.started_at if metadata else "", services=services_by_slug.get(slug, ()), + label=metadata.label if metadata else "", + color=metadata.color if metadata else "", )) return out diff --git a/bot_bottle/backend/smolmachines/prepare.py b/bot_bottle/backend/smolmachines/prepare.py index 87c13f6..d69fb85 100644 --- a/bot_bottle/backend/smolmachines/prepare.py +++ b/bot_bottle/backend/smolmachines/prepare.py @@ -73,6 +73,8 @@ def resolve_plan( started_at=datetime.now(timezone.utc).isoformat(), compose_project="", backend="smolmachines", + label=spec.label, + color=spec.color, )) subnet, gateway, bundle_ip = smolmachines_bundle_subnet(slug) @@ -136,6 +138,8 @@ def resolve_plan( auth_token=provider.auth_token, host_env=dict(os.environ), trusted_project_path=workspace_plan.workdir, + label=spec.label, + color=spec.color, ) merged_guest_env = dict(agent_provision.guest_env) for key, val in agent_provision.env_vars.items(): diff --git a/bot_bottle/cli/list.py b/bot_bottle/cli/list.py index 715b983..e895b72 100644 --- a/bot_bottle/cli/list.py +++ b/bot_bottle/cli/list.py @@ -3,12 +3,47 @@ from __future__ import annotations import argparse +import os import sys from ..backend import enumerate_active_agents from ..manifest import Manifest from ._common import PROG, USER_CWD +_ANSI_COLOR_CODES: dict[str, str] = { + "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", +} +_ANSI_RESET = "\033[0m" + + +def _ansi_label(text: str, color: str) -> str: + if not color: + return text + if not sys.stdout.isatty(): + return text + term = os.environ.get("TERM", "") + if term in ("dumb", ""): + return text + code = _ANSI_COLOR_CODES.get(color) + if not code: + return text + return f"{code}{text}{_ANSI_RESET}" + def cmd_list(argv: list[str]) -> int: parser = argparse.ArgumentParser(prog=f"{PROG} list", add_help=True) @@ -27,11 +62,11 @@ def cmd_list(argv: list[str]) -> int: if not active: print("no active bot-bottle bottles", file=sys.stderr) return 0 - # One line per bottle: `\t\t\t`. - # Tab-separated keeps the format stable for shell pipelines; - # the dashboard renders the same data through its own - # formatter. + # One line per bottle: `\t\t