prd-new-user-provider-plugins → 0053-user-provider-plugins prd-new-named-labelled-agents → 0054-named-labelled-agents Both PRDs ship with their implementations so Status flips Draft → Active. Manual fix: the prd-number workflow did not fire on these merges. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
11 KiB
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
- 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
<agent_name>-<slug_suffix>(the same pattern currently shown inlist active). No color is pre-selected. - 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.
- 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.
labelandcolorare stored inBottleMetadataand written to the bottle'smetadata.json. Both fields default to""(empty / unset).ActiveAgentcarrieslabelandcolor;enumerate_active()reads them frommetadata.json.cli list activeshows the label when non-empty (falling back toagent_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.BottleSpeccarrieslabelandcolor; both backends'preparesteps copy them intoBottleMetadata.ClaudeAgentProvider.provision_plan()writeslabel→"name"andcolor→"color"into the generatedclaude.json. Fields are omitted when empty.cmd_startcallsname_color_modalafter backend selection and before_launch_bottle; passeslabel/colorintoBottleSpec.- 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.jsonis 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
@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:
@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
@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:
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.
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 <agent_name>-<slug_suffix>, 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 <agent_name>-<slug_suffix> 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():
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 = ""andcolor: str = ""toBottleSpec,BottleMetadata, andActiveAgent. docker/prepare.pyandsmolmachines/prepare.py: copyspec.label/spec.colorintoBottleMetadata; pass them toagent_provision_plan().docker/enumerate.pyand smolmachines equivalent: copymetadata.label/metadata.colorintoActiveAgent.- Add
label: str = ""andcolor: str = ""keyword params toAgentProvider.provision_plan()(ABC),ClaudeAgentProvider.provision_plan()(uses them in theclaude.jsonwrite), and theagent_provision_plan()shim.CodexAgentProvideraccepts the params and ignores them. cmd_list: updatelist activerow to uselabelwhen 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: addname_color_modal(default_label)implementing the two-step curses window described above.cmd_start: callname_color_modal(default_label=agent_name)after backend selection and before_launch_bottle; passlabel/colorintoBottleSpec.
Open questions
None.