Files
bot-bottle/docs/prds/0054-named-labelled-agents.md
T
didericis 7ebddf7792
prd-number / assign-numbers (push) Successful in 20s
ci(prd): assign sequential numbers to new PRDs
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>
2026-06-07 14:23:56 -04:00

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

  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 <agent_name>-<slug_suffix> (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

@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 = "" 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.