Merge main into fix/db-off-data-plane-469
tracker-policy-pr / check-pr (pull_request) Successful in 13s
test / integration-docker (pull_request) Successful in 34s
test / unit (pull_request) Successful in 49s
lint / lint (push) Successful in 57s
test / integration-firecracker (pull_request) Successful in 3m29s
test / coverage (pull_request) Successful in 20s
test / publish-infra (pull_request) Has been skipped

Brings in the two commits main gained: #472 (CLI cleanup — remove `info`,
split `list active` into a top-level `active`, simplify `list`) and the
terminology glossary. #472 was written against the old flat cli/ layout,
so its semantics are reconciled into the reorg'd cli/commands/ structure:

  * new `active` command lives at cli/commands/active.py, registered in
    the lazy registry and listed in `help` (not left at the flat path).
  * `info` removed: deleted cli/commands/info.py, dropped from the
    registry and `help`.
  * `list` simplified to list-available-only, using the reorg's
    os.getcwd()/constants.PROG conventions.
  * the `list active` -> `active` doc wording applied to the backend
    docstrings that our split moved into base.py / selection.py.

Our reorg wins for the fully-rewritten dispatcher (cli/__init__ shim,
backend facade). pyright: 0 errors. Full unit suite green (2243).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 19:35:49 -04:00
12 changed files with 128 additions and 107 deletions
+1
View File
@@ -35,6 +35,7 @@ backend remains available with `BOT_BOTTLE_BACKEND=docker` or
- `.bot-bottle/` — per-repo agent and bottle manifests (YAML markdown format). - `.bot-bottle/` — per-repo agent and bottle manifests (YAML markdown format).
- `examples/` — example bottles and agents showing the manifest format. - `examples/` — example bottles and agents showing the manifest format.
- `docs/README.md` — docs overview; when to write which document. - `docs/README.md` — docs overview; when to write which document.
- `docs/glossary.md` — canonical term definitions (Agent Provider, Bottle, Sealed Bottle, etc.).
- `docs/prds/` — product requirement docs (see `docs/prds/README.md` for format). - `docs/prds/` — product requirement docs (see `docs/prds/README.md` for format).
- `docs/research/` — research notes (see `docs/research/README.md`). - `docs/research/` — research notes (see `docs/research/README.md`).
- `docs/decisions/` — decision records (ADR-lite). - `docs/decisions/` — decision records (ADR-lite).
+2 -2
View File
@@ -177,7 +177,7 @@ class ExecResult:
@dataclass(frozen=True) @dataclass(frozen=True)
class ActiveAgent: class ActiveAgent:
"""One currently-running agent, as the CLI `list active` and """One currently-running agent, as the CLI `active` and
dashboard agents pane render it. ("Agent" is the project's dashboard agents pane render it. ("Agent" is the project's
consistent name for the thing running inside a bottle — the consistent name for the thing running inside a bottle — the
bottle is the container, the agent is what runs in it.) bottle is the container, the agent is what runs in it.)
@@ -570,7 +570,7 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]):
Linux + KVM. Used by the cross-backend Linux + KVM. Used by the cross-backend
`enumerate_active_agents` / `cmd_cleanup` to skip backends `enumerate_active_agents` / `cmd_cleanup` to skip backends
the operator hasn't installed, so a docker-only host the operator hasn't installed, so a docker-only host
doesn't fail when `cli.py list active` walks past doesn't fail when `cli.py active` walks past
firecracker.""" firecracker."""
@classmethod @classmethod
+2 -2
View File
@@ -1,6 +1,6 @@
"""Active-agent enumeration for the docker backend. """Active-agent enumeration for the docker backend.
Returns `ActiveAgent` records the CLI `list active` command and the Returns `ActiveAgent` records the CLI `active` command and the
dashboard agents pane consume. Empty when docker isn't reachable dashboard agents pane consume. Empty when docker isn't reachable
— gated by `has_backend('docker')` at the cross-backend caller — gated by `has_backend('docker')` at the cross-backend caller
so this module trusts that docker is available when called. so this module trusts that docker is available when called.
@@ -60,7 +60,7 @@ def _parse_services_by_project(stdout: str) -> dict[str, set[str]]:
def _query_services_by_project() -> dict[str, set[str]]: def _query_services_by_project() -> dict[str, set[str]]:
"""One `docker ps` call → `{project: {service, ...}}`. Used """One `docker ps` call → `{project: {service, ...}}`. Used
by the CLI's `list active` and the dashboard's agents pane — by the CLI's `active` and the dashboard's agents pane —
one subprocess per refresh tick, not one per bottle.""" one subprocess per refresh tick, not one per bottle."""
try: try:
r = subprocess.run( r = subprocess.run(
+1 -1
View File
@@ -168,7 +168,7 @@ def has_backend(name: str) -> bool:
def enumerate_active_agents() -> list[ActiveAgent]: def enumerate_active_agents() -> list[ActiveAgent]:
"""All currently-running agents, across every available """All currently-running agents, across every available
backend. Used by CLI `list active` and the dashboard's agents backend. Used by CLI `active` and the dashboard's agents
pane so neither has to know which backends exist. Skips pane so neither has to know which backends exist. Skips
backends whose `is_available()` reports False. backends whose `is_available()` reports False.
+1 -1
View File
@@ -17,12 +17,12 @@ from typing import Callable
# command name -> "<submodule>:<handler attr>". Kept as strings so building # command name -> "<submodule>:<handler attr>". Kept as strings so building
# the registry imports nothing; the module loads only when dispatched. # the registry imports nothing; the module loads only when dispatched.
_HANDLERS: dict[str, str] = { _HANDLERS: dict[str, str] = {
"active": "active:cmd_active",
"backend": "backend:cmd_backend", "backend": "backend:cmd_backend",
"cleanup": "cleanup:cmd_cleanup", "cleanup": "cleanup:cmd_cleanup",
"commit": "commit:cmd_commit", "commit": "commit:cmd_commit",
"edit": "edit:cmd_edit", "edit": "edit:cmd_edit",
"help": "help:cmd_help", "help": "help:cmd_help",
"info": "info:cmd_info",
"init": "init:cmd_init", "init": "init:cmd_init",
"list": "list:cmd_list", "list": "list:cmd_list",
"login": "login:cmd_login", "login": "login:cmd_login",
+53
View File
@@ -0,0 +1,53 @@
"""active: list currently-running bot-bottle bottles."""
from __future__ import annotations
import os
import sys
from ...backend import enumerate_active_agents
from ..constants import PROG
_ANSI_COLOR_CODES: dict[str, str] = {
"red": "\033[91m",
"green": "\033[92m",
"yellow": "\033[93m",
"blue": "\033[94m",
"magenta": "\033[95m",
}
_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_active(argv: list[str]) -> int:
if argv and argv[0] in ("-h", "--help"):
sys.stderr.write(f"usage: {PROG} active\n")
sys.stderr.write("\nList all currently-running bot-bottle bottles.\n")
sys.stderr.write("Output: <backend>\\t<slug>\\t<label>\\t<services>\n")
return 0
active = enumerate_active_agents()
if not active:
print("no active bot-bottle bottles", file=sys.stderr)
return 0
# One line per bottle: `<backend>\t<slug>\t<label>\t<services>`.
# Tab-separated keeps the format stable for shell pipelines.
for b in active:
services = ",".join(b.services) if b.services else "-"
display_name = f"{b.label} ({b.agent_name})" if b.label else b.agent_name
colored_name = _ansi_label(display_name, b.color)
print(f"{b.backend_name}\t{b.slug}\t{colored_name}\t{services}")
return 0
+1 -1
View File
@@ -27,7 +27,7 @@ def cmd_commit(argv: list[str]) -> int:
nargs="?", nargs="?",
default=None, default=None,
help=( help=(
"bottle slug from `cli.py list active` " "bottle slug from `cli.py active` "
"(omit to pick interactively)" "(omit to pick interactively)"
), ),
) )
+2 -2
View File
@@ -21,14 +21,14 @@ def cmd_help(argv: list[str] | None = None) -> int:
w = sys.stderr.write w = sys.stderr.write
w(f"usage: {PROG} <command> [args...]\n\n") w(f"usage: {PROG} <command> [args...]\n\n")
w("Commands:\n") w("Commands:\n")
w(" active list currently-running bot-bottle bottles\n")
w(" backend set up / check / undo a backend's host prerequisites (setup|status|teardown)\n") w(" backend set up / check / undo a backend's host prerequisites (setup|status|teardown)\n")
w(" cleanup stop and remove all active bot-bottle containers\n") w(" cleanup stop and remove all active bot-bottle containers\n")
w(" commit snapshot a running bottle's container state to a Docker image\n") w(" commit snapshot a running bottle's container state to a Docker image\n")
w(" edit open an agent in vim for editing\n") w(" edit open an agent in vim for editing\n")
w(" help show this command list\n") w(" help show this command list\n")
w(" info print env, skills, and prompt details for a named agent\n")
w(" init interactively create a new agent and add it to bot-bottle.json\n") w(" init interactively create a new agent and add it to bot-bottle.json\n")
w(" list list available agents or active containers\n") w(" list list available agents from bot-bottle.json\n")
w(" login register this host with a bot-bottle console\n") w(" login register this host with a bot-bottle console\n")
w(" resume re-launch a bottle by its identity (continues state from PRD 0016)\n") w(" resume re-launch a bottle by its identity (continues state from PRD 0016)\n")
w(" start boot a container for a named agent and attach an interactive session\n") w(" start boot a container for a named agent and attach an interactive session\n")
-50
View File
@@ -1,50 +0,0 @@
"""info: print env, skills, and prompt details for a named agent."""
from __future__ import annotations
import argparse
import os
from ...log import info
from ...manifest import ManifestIndex
from ..constants import PROG
def cmd_info(argv: list[str]) -> int:
parser = argparse.ArgumentParser(prog=f"{PROG} info", add_help=True)
parser.add_argument("name", help="agent name defined in bot-bottle.json")
args = parser.parse_args(argv)
names = ManifestIndex.resolve(os.getcwd())
names.require_agent(args.name)
manifest = names.load_for_agent(args.name)
agent = manifest.agent
bottle = manifest.bottle
env_names = list(bottle.env.keys())
prompt_first_line = agent.prompt.splitlines()[0] if agent.prompt else ""
print()
info(f"agent : {args.name}")
info(f"env (names only): {', '.join(env_names) if env_names else '(none)'}")
info(f"skills : {' '.join(agent.skills) if agent.skills else '(none)'}")
info(
f"prompt : {len(agent.prompt)} chars; "
f"first line: {prompt_first_line or '(empty)'}"
)
info(f"bottle : {agent.bottle}")
identity = manifest.git_identity_summary()
if identity:
info(f" git identity : {identity}")
if bottle.git:
for e in bottle.git:
info(
f" git remote : {e.Name} -> {e.Upstream} "
f"(IdentityFile={e.IdentityFile})"
)
if e.KnownHostKey:
info(f" KnownHostKey: {e.KnownHostKey}")
else:
info(" git remotes : (none)")
print()
return 0
+7 -48
View File
@@ -1,62 +1,21 @@
"""list: list available agents or active bottles.""" """list: list available agents."""
from __future__ import annotations from __future__ import annotations
import argparse
import os import os
import sys import sys
from ...backend import enumerate_active_agents
from ...manifest import ManifestIndex from ...manifest import ManifestIndex
from ..constants import PROG from ..constants import PROG
_ANSI_COLOR_CODES: dict[str, str] = {
"red": "\033[91m",
"green": "\033[92m",
"yellow": "\033[93m",
"blue": "\033[94m",
"magenta": "\033[95m",
}
_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: def cmd_list(argv: list[str]) -> int:
parser = argparse.ArgumentParser(prog=f"{PROG} list", add_help=True) if argv and argv[0] in ("-h", "--help"):
parser.add_argument("scope", choices=["available", "active"]) sys.stderr.write(f"usage: {PROG} list\n")
args = parser.parse_args(argv) sys.stderr.write("\nList all available agents from bot-bottle.json.\n")
if args.scope == "available":
manifest = ManifestIndex.resolve(os.getcwd())
for name in manifest.all_agent_names:
print(name)
return 0 return 0
# `active` enumerates every backend (docker, firecracker, manifest = ManifestIndex.resolve(os.getcwd())
# macos-container) so non-docker bottles aren't hidden behind for name in manifest.all_agent_names:
# the env var. print(name)
active = enumerate_active_agents()
if not active:
print("no active bot-bottle bottles", file=sys.stderr)
return 0
# One line per bottle: `<backend>\t<slug>\t<label>\t<services>`.
# Tab-separated keeps the format stable for shell pipelines.
for b in active:
services = ",".join(b.services) if b.services else "-"
display_name = f"{b.label} ({b.agent_name})" if b.label else b.agent_name
colored_name = _ansi_label(display_name, b.color)
print(f"{b.backend_name}\t{b.slug}\t{colored_name}\t{services}")
return 0 return 0
+1
View File
@@ -7,6 +7,7 @@ picking the right document for what you're capturing.
| Artifact | For | | Artifact | For |
|---|---| |---|---|
| **Glossary** (`docs/glossary.md`) | Canonical term definitions — what words mean in this project. |
| **PRD** (`docs/prds/`) | A feature: what to build, scope, success criteria. | | **PRD** (`docs/prds/`) | A feature: what to build, scope, success criteria. |
| **Research note** (`docs/research/`) | A landscape/tradeoff investigation. | | **Research note** (`docs/research/`) | A landscape/tradeoff investigation. |
| **Decision record** (`docs/decisions/`) | A decision that isn't itself a feature — a policy, a convention, a "we will / won't do this," or a load-bearing choice made inside a larger PRD that deserves to be discoverable on its own. | | **Decision record** (`docs/decisions/`) | A decision that isn't itself a feature — a policy, a convention, a "we will / won't do this," or a load-bearing choice made inside a larger PRD that deserves to be discoverable on its own. |
+57
View File
@@ -0,0 +1,57 @@
# Glossary
Canonical terminology for bot-bottle. Prefer these names in docs, comments, and UI.
---
## Agent Provider
The component that connects to an external model provider and sets up the model
harness inside the agent runtime. Configured in a bottle manifest under
`agent_provider:`; built-in templates are `claude` and `codex`. Responsible for
provider-specific auth, startup args, and egress routes.
## Agent Runtime
The OCI image (and the container or VM it runs in) that houses the model
harness. On the macOS-container backend this is an Apple Container; on
Firecracker it is a microVM; on the legacy Docker backend it is a Docker
container. The agent runtime is built from the agent provider's Dockerfile
(built-in or custom).
## Agent / Agent Definition
A Markdown file with YAML frontmatter that declares the system prompt and
identity of the model harness. Lives under `~/.bot-bottle/agents/` or a repo's
`.bot-bottle/agents/`. Specifies which bottle to run under (`bottle:`) and
which skills to load. Agent definitions are safe to commit; they contain no
secrets or egress policy.
## Bottle / Bottle Definition
A Markdown file with YAML frontmatter that declares the security and runtime
boundaries for one agent runtime: egress allowlist, git remotes, env vars,
nested-container flag, and agent provider config. Lives under
`~/.bot-bottle/bottles/`. Bottles are scoped to `$HOME` so a cloned repo
cannot override host egress policy.
## Sealed Bottle
The fully-resolved bottle after all `extends:` inheritance is applied and every
field has been validated. The sealed bottle is the immutable boundary spec that
the launcher enforces — no further overrides are possible once it is sealed.
## Bottled Agent
The combination of an Agent Definition and a Sealed Bottle, representing a
single deployable agent with a fixed identity and fixed boundaries. A bottled
agent has two observable states:
- **Active** — the agent runtime is running and the agent is executing.
- **Frozen** — the agent runtime has been snapshotted (Firecracker committed
image); the agent is not running but can be resumed from the snapshot.
## Active Bottle
Shorthand for an active (running) Bottled Agent. Used in the supervisor TUI
and discovery layer to mean "a bottle whose agent runtime is currently up."