From cd9f023f3da84037bcaa5449aae55e49f7de8bdb Mon Sep 17 00:00:00 2001 From: didericis Date: Mon, 27 Jul 2026 10:20:17 -0400 Subject: [PATCH] docs: name bot-bottle, not ./cli.py, in every instruction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `doctor` told users to run `./cli.py backend setup`. cli.py is a four-line wrapper at the repo root that calls bot_bottle.cli:main, and it does not ship — [tool.setuptools.packages.find] includes only bot_bottle*, so anyone who installed rather than cloned has no such file. Confirmed against a real install: the user's tree contains exactly one executable, `bot-bottle`, and no cli.py anywhere, while doctor recommended `./cli.py` three times. Invisible in development, where ./cli.py works fine from a checkout, which is why only a clean-install test surfaced it. The two entry points are the same code, so the fix is to name the one that always exists. 141 replacements across 45 files: the runtime messages that caused this, plus README, docs, PRDs, research notes and test prose, so nothing teaches the invocation a user cannot run. scripts/demo.sh is deliberately untouched — it *executes* ./cli.py from a checkout, where that is the correct and available path. Verified end to end: a sandbox install now reports "Run: bot-bottle backend setup --backend=firecracker", and bot-bottle is on that user's PATH. Unit suite unchanged against the pre-existing baseline. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WEfZZhakx13bxTfXcZCoS5 --- README.md | 14 +++---- bot_bottle/agent_provider.py | 2 +- bot_bottle/backend/base.py | 6 +-- bot_bottle/backend/docker/setup.py | 6 +-- bot_bottle/backend/firecracker/infra_vm.py | 2 +- .../backend/firecracker/isolation_probe.py | 2 +- bot_bottle/backend/firecracker/netpool.py | 6 +-- bot_bottle/backend/firecracker/setup.py | 10 ++--- bot_bottle/backend/firecracker/util.py | 8 ++-- bot_bottle/backend/freeze.py | 4 +- bot_bottle/backend/macos_container/setup.py | 4 +- bot_bottle/backend/selection.py | 2 +- bot_bottle/cli/commands/backend.py | 6 +-- bot_bottle/cli/commands/cleanup.py | 2 +- bot_bottle/cli/commands/commit.py | 4 +- bot_bottle/cli/commands/resume.py | 2 +- bot_bottle/cli/commands/start.py | 12 +++--- bot_bottle/cli/commands/supervise.py | 2 +- bot_bottle/gateway/git_gate/render.py | 2 +- docs/demo.tape | 4 +- docs/prds/0011-per-file-md-manifest.md | 2 +- docs/prds/0018-compose-per-instance.md | 4 +- docs/prds/0019-active-agents-in-dashboard.md | 8 ++-- .../0020-start-and-attach-from-dashboard.md | 42 +++++++++---------- docs/prds/0021-dashboard-tmux-split-pane.md | 6 +-- docs/prds/0026-agent-provider-templates.md | 2 +- docs/prds/0048-ssh-deploy-key-provisioning.md | 2 +- .../0049-strip-dashboard-to-supervisor-tui.md | 26 ++++++------ docs/prds/0051-launch-selector.md | 18 ++++---- docs/prds/0060-commit-bottle-state.md | 6 +-- .../0066-separate-agent-bottle-selection.md | 4 +- docs/prds/0068-smolmachines-linux.md | 2 +- docs/prds/0077-macos-container-ci-runner.md | 4 +- docs/prds/0078-install-script.md | 2 +- docs/research/built-in-supervisor-design.md | 12 +++--- .../research/claude-code-pane-in-dashboard.md | 6 +-- docs/research/malicious-commit-scanning.md | 4 +- docs/research/manifest-format-and-grouping.md | 4 +- docs/research/polish-priorities.md | 2 +- docs/research/remote-docker-vm-isolation.md | 2 +- scripts/firecracker-netpool.sh | 2 +- tests/README.md | 4 +- tests/_backend.py | 2 +- tests/unit/test_backend_skip_guards.py | 2 +- tests/unit/test_cli_backend.py | 2 +- 45 files changed, 135 insertions(+), 135 deletions(-) diff --git a/README.md b/README.md index 6f54fd4f..ea2f6c62 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ ## Features - **Per-bottle egress allowlist** — TLS-bumped HTTP/HTTPS chokepoint with a per-manifest host allowlist; per-route path/method/header `matches` filtering; outbound DLP scanning for known tokens and secrets, inbound DLP scanning for prompt-injection attempts; DoH and arbitrary hosts blocked by default. -- **Per-route token-match policy** — each egress route picks what happens when the outbound DLP catches a token via `dlp.outbound_on_match`: `supervise` (default) holds the request and surfaces it in `./cli.py supervise` for approval (an approved value is remembered for the life of the proxy); `redact` scrubs the value and forwards; `block` is a hard `403`. Cuts false-positive friction without weakening default-deny. +- **Per-route token-match policy** — each egress route picks what happens when the outbound DLP catches a token via `dlp.outbound_on_match`: `supervise` (default) holds the request and surfaces it in `bot-bottle supervise` for approval (an approved value is remembered for the life of the proxy); `redact` scrubs the value and forwards; `block` is a hard `403`. Cuts false-positive friction without weakening default-deny. - **Tokens the agent never sees** — host secrets live in a gateway; the agent dials `http://gateway:9099/` and the proxy strips inbound `Authorization` and injects the real token before forwarding. `printenv` in the agent shows proxy URLs only. - **Gitleaks-scanned push (git-gate)** — `bottle.git` remotes route through a per-bottle `git daemon` that gitleaks-scans incoming refs pre-receive and forwards clean refs upstream over SSH. The agent never holds the upstream credential. - **Manifest-scoped skills + secrets** — each bottle declares its skills, env, git identity, remotes, and egress routes; unknown keys die at load. @@ -39,7 +39,7 @@ On the legacy Docker backend, the same logical bottle is two containers per agen The Docker topology looks like this: ``` - host ( ./cli.py ) + host ( bot-bottle ) │ starts │ stops ▼ @@ -87,7 +87,7 @@ The installer is a bootstrapper: it finds a suitable Python, installs bot-bottle **A backend**, which the installer deliberately does *not* install for you — `doctor` reports what's missing afterwards. On compatible macOS hosts, the default backend requires Apple's `container` CLI and does not require Docker. The Firecracker backend (Linux) requires Docker on the host for the gateway plus the `firecracker` binary and KVM. The legacy Docker backend requires Docker. Claude bottles also need a long-lived Claude Code OAuth token (`claude setup-token`) exported as `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN`. -Use `BOT_BOTTLE_BACKEND=docker ./cli.py start ` on hosts where neither Apple Container nor KVM is available and Docker is the desired backend. +Use `BOT_BOTTLE_BACKEND=docker bot-bottle start ` on hosts where neither Apple Container nor KVM is available and Docker is the desired backend. > **CI (macOS Apple Container):** the advisory `integration-macos` job in `.gitea/workflows/pre-release-test.yml` runs only on manual dispatch. It targets a self-hosted host-mode runner labelled `macos`; Apple Container cannot run inside the Linux pull-request runner. Provision an Apple Silicon host with the `container` CLI running and Python ≥ 3.11 plus `coverage` on the launchd service's explicit `PATH`. The infra container is a singleton (`bot-bottle-mac-infra`), so keep runner concurrency at 1. Its coverage is reported separately and never feeds the required pull-request gate. @@ -180,10 +180,10 @@ On Linux, a KVM-capable host defaults to the Firecracker backend. It needs: - **`/dev/kvm`** present and accessible. Load `kvm-intel` or `kvm-amd` (and enable virtualization in BIOS/firmware). The invoking user must be in the `kvm` group: `sudo usermod -aG kvm "$USER"` then re-login. bot-bottle preflights this and reports exactly what's missing. - **`firecracker`** on `PATH`: grab a release from . Start flows print this pointer when the binary is missing. - **Docker** for the gateway and image build. -- **A one-time privileged network setup** — the per-bottle TAP pool plus the fail-closed `nftables` isolation table. Run `./cli.py backend setup --backend=firecracker` for the host-appropriate config (a NixOS module, a `sudo` script elsewhere); `./cli.py backend status --backend=firecracker` reports what's present, including whether the pool range collides with an existing route. The pool defaults to `10.243.0.0/16` (an obscure RFC-1918 block that dodges docker/libvirt/LAN and, deliberately, Tailscale's `100.64.0.0/10` CGNAT range); override with `BOT_BOTTLE_FC_IP_BASE` if it clashes on your host. +- **A one-time privileged network setup** — the per-bottle TAP pool plus the fail-closed `nftables` isolation table. Run `bot-bottle backend setup --backend=firecracker` for the host-appropriate config (a NixOS module, a `sudo` script elsewhere); `bot-bottle backend status --backend=firecracker` reports what's present, including whether the pool range collides with an existing route. The pool defaults to `10.243.0.0/16` (an obscure RFC-1918 block that dodges docker/libvirt/LAN and, deliberately, Tailscale's `100.64.0.0/10` CGNAT range); override with `BOT_BOTTLE_FC_IP_BASE` if it clashes on your host. ```sh -BOT_BOTTLE_BACKEND=firecracker ./cli.py start +BOT_BOTTLE_BACKEND=firecracker bot-bottle start ``` > **NixOS:** enable `virtualisation.docker`, ensure the KVM module is loaded (`boot.kernelModules = [ "kvm-intel" ];` or `kvm-amd`), and add your user to the `kvm` and `docker` groups. For the network pool, consume the flake module — `imports = [ inputs.bot-bottle.nixosModules.firecracker-netpool ]; services.bot-bottle-firecracker = { enable = true; owner = "you"; };` — then `nixos-rebuild switch` (imperative nft/TAP rules don't survive a rebuild; channel users can `imports = [ /nix/firecracker-netpool.nix ]`). `firecracker` isn't in nixpkgs by default as a user binary — install the release binary (pin the version) and put it on `PATH`. @@ -191,7 +191,7 @@ BOT_BOTTLE_BACKEND=firecracker ./cli.py start > **CI:** Firecracker integration runs in the manually dispatched `.gitea/workflows/pre-release-test.yml` on a self-hosted runner labelled `kvm`; privileged KVM hosts never execute unreviewed PR code automatically. Provision it like a normal Firecracker host: `firecracker` on `PATH`, `/dev/kvm`, the cached guest kernel and static dropbear, and the persistent TAP/nft pool. The required pull-request workflow runs unit plus the complete Docker integration suite on `ubuntu-latest`; see `docs/ci.md`. ```sh -./cli.py start # builds the image on first run, drops you into claude +bot-bottle start # builds the image on first run, drops you into claude ``` ## Manifest @@ -267,7 +267,7 @@ You help maintain Gitea-hosted projects. | `dlp.outbound_on_match` | no | What to do when an outbound token is detected: `supervise` (default for manifest routes — hold for operator approval), `redact` (scrub the value and forward), or `block` (hard 403). Agent-provider routes (e.g. `api.anthropic.com`) default to `redact`. | | `git.fetch` | no | `true` permits smart HTTP clone/fetch (`git-upload-pack`) for this host. Push (`git-receive-pack`) remains blocked. | -When an outbound DLP detector matches a token, the route's `dlp.outbound_on_match` policy decides what happens. Under the default `supervise`, the proxy queues an `egress-token-allow` proposal for the operator's `./cli.py supervise` TUI and holds the request open until it is answered (or `EGRESS_TOKEN_ALLOW_TIMEOUT_SECONDS`, default 300s, elapses — after which it fails closed). The operator never sees the raw token, only the host, method, path, and a redacted snippet; approving adds the value to an in-memory safelist for the life of the egress proxy. Under `redact`, the matched value is scrubbed from the body, headers, and path and the request is forwarded (failing closed if a match lands somewhere unredactable, like the hostname). Under `block` it stays a hard `403`. Structural blocks (CRLF injection) and not-in-allowlist host blocks are always hard `403`s regardless of policy. +When an outbound DLP detector matches a token, the route's `dlp.outbound_on_match` policy decides what happens. Under the default `supervise`, the proxy queues an `egress-token-allow` proposal for the operator's `bot-bottle supervise` TUI and holds the request open until it is answered (or `EGRESS_TOKEN_ALLOW_TIMEOUT_SECONDS`, default 300s, elapses — after which it fails closed). The operator never sees the raw token, only the host, method, path, and a redacted snippet; approving adds the value to an in-memory safelist for the life of the egress proxy. Under `redact`, the matched value is scrubbed from the body, headers, and path and the request is forwarded (failing closed if a match lands somewhere unredactable, like the hostname). Under `block` it stays a hard `403`. Structural blocks (CRLF injection) and not-in-allowlist host blocks are always hard `403`s regardless of policy. More examples in `examples/`. Full design lives under `docs/prds/`; the trust-boundary rationale is in `docs/prds/0011-per-file-md-manifest.md`. diff --git a/bot_bottle/agent_provider.py b/bot_bottle/agent_provider.py index 84524a3b..38299766 100644 --- a/bot_bottle/agent_provider.py +++ b/bot_bottle/agent_provider.py @@ -226,7 +226,7 @@ class AgentProvider(ABC): initial task in a non-interactive (headless) session. Called only when ``--prompt`` is passed to - ``./cli.py start --headless``; the returned args are appended + ``bot-bottle start --headless``; the returned args are appended after the provider's ``bypass_args`` and ``startup_args``.""" def provision_ca(self, bottle: "Bottle", plan: "BottlePlan") -> None: diff --git a/bot_bottle/backend/base.py b/bot_bottle/backend/base.py index 1216a1a7..a615203d 100644 --- a/bot_bottle/backend/base.py +++ b/bot_bottle/backend/base.py @@ -531,7 +531,7 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]): host-appropriate config or commands. Prints to stdout/stderr and returns a shell exit code (0 = nothing to report / success). A backend that needs no host setup prints a short note and returns - 0. Invoked generically by `./cli.py backend setup [--backend=…]` + 0. Invoked generically by `bot-bottle backend setup [--backend=…]` so operators can provision any backend without a backend-specific command. Classmethod (like `is_available`) — it's a host query, not per-bottle state.""" @@ -548,14 +548,14 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]): stderr. When quiet=True returns the status code silently — useful for cheap programmatic checks. - Invoked by `./cli.py backend status [--backend=…]` (quiet=False) + Invoked by `bot-bottle backend status [--backend=…]` (quiet=False) and by is_backend_ready() (caller-controlled).""" @classmethod @abstractmethod def teardown(cls) -> int: """Undo `setup()` — the inverse operation, surfaced as - `./cli.py backend teardown [--backend=…]` (uninstall). Symmetric + `bot-bottle backend teardown [--backend=…]` (uninstall). Symmetric with setup: where setup is advisory (prints the privileged commands / declarative config to apply), teardown prints the commands / config change to remove the host prerequisites. A diff --git a/bot_bottle/backend/docker/setup.py b/bot_bottle/backend/docker/setup.py index 90e52d2b..190ad896 100644 --- a/bot_bottle/backend/docker/setup.py +++ b/bot_bottle/backend/docker/setup.py @@ -8,7 +8,7 @@ pointer, and `status()` reports whether docker is usable. This is intentionally minimal; a richer version (daemon config checks, gVisor/runsc install guidance, rootless-docker hints) is tracked separately. Reached via `DockerBottleBackend.setup` / `.status`, which -the generic `./cli.py backend {setup,status}` dispatches to. +the generic `bot-bottle backend {setup,status}` dispatches to. """ from __future__ import annotations @@ -69,7 +69,7 @@ def teardown() -> int: sys.stderr.write( "Docker backend: nothing to undo — it provisions no privileged host " "state (networks and the gateway are per-launch and are " - "removed by `./cli.py cleanup`). Docker itself is left installed.\n" + "removed by `bot-bottle cleanup`). Docker itself is left installed.\n" ) return 0 @@ -89,5 +89,5 @@ def status() -> int: runsc = _docker_on_path() and _util.runsc_available() sys.stderr.write(f"gVisor runsc runtime: {'registered' if runsc else 'not registered (optional)'}\n") if not ok: - sys.stderr.write("\nRun: ./cli.py backend setup --backend=docker\n") + sys.stderr.write("\nRun: bot-bottle backend setup --backend=docker\n") return 0 if ok else 1 diff --git a/bot_bottle/backend/firecracker/infra_vm.py b/bot_bottle/backend/firecracker/infra_vm.py index 0595df1a..04c4b66e 100644 --- a/bot_bottle/backend/firecracker/infra_vm.py +++ b/bot_bottle/backend/firecracker/infra_vm.py @@ -204,7 +204,7 @@ def boot_vm( Records the PID.""" if not netpool.tap_present(slot.iface): die(f"infra link {slot.iface} not present.\n" - f" ./cli.py backend setup --backend=firecracker") + f" bot-bottle backend setup --backend=firecracker") run_dir.mkdir(parents=True, exist_ok=True) rootfs = run_dir / "rootfs.ext4" diff --git a/bot_bottle/backend/firecracker/isolation_probe.py b/bot_bottle/backend/firecracker/isolation_probe.py index 949199f0..0915e1c3 100644 --- a/bot_bottle/backend/firecracker/isolation_probe.py +++ b/bot_bottle/backend/firecracker/isolation_probe.py @@ -108,7 +108,7 @@ def verify_isolation(private_key: Path, guest_ip: str) -> None: die(f"ISOLATION FAILURE: the VM reached the host canary " f"{canary_ip}:{canary_port}. The egress boundary is not in " f"force — refusing to run the agent (fail-closed). Verify the " - f"nft table with: ./cli.py backend setup --backend=firecracker") + f"nft table with: bot-bottle backend setup --backend=firecracker") if result.returncode == 2: die("isolation probe inconclusive: the guest has no python3/bash/nc " "to run the connectivity test. Refusing to continue " diff --git a/bot_bottle/backend/firecracker/netpool.py b/bot_bottle/backend/firecracker/netpool.py index d7496d12..a279f454 100644 --- a/bot_bottle/backend/firecracker/netpool.py +++ b/bot_bottle/backend/firecracker/netpool.py @@ -3,7 +3,7 @@ config renderers (shell command + NixOS module) shown to operators. The Firecracker backend needs a privileged one-time network setup: a pool of point-to-point TAP devices (owned by the invoking user, so -`./cli.py start` never needs root) and a dedicated nftables table that +`bot-bottle start` never needs root) and a dedicated nftables table that isolates every VM. The pool parameters live in exactly one place — `netpool.defaults.env`, a plain KEY=VALUE file next to this module — and every consumer reads *that*: this module (below), the shell script @@ -315,11 +315,11 @@ def allocate(slug: str) -> tuple[Slot, IO[str]]: return s, handle die(f"Firecracker TAP pool exhausted ({pool_size()} slots, all in " f"use). Stop a running bottle or raise BOT_BOTTLE_FC_POOL_SIZE " - f"and re-run `./cli.py backend setup --backend=firecracker`.") + f"and re-run `bot-bottle backend setup --backend=firecracker`.") raise AssertionError("unreachable") -# --- config renderers (shown by `./cli.py backend setup`) ----------- +# --- config renderers (shown by `bot-bottle backend setup`) ----------- # The persistent unit is the portable install: the same systemd oneshot # on every systemd distro (Debian/Ubuntu/Fedora/RHEL/Arch/…). diff --git a/bot_bottle/backend/firecracker/setup.py b/bot_bottle/backend/firecracker/setup.py index b7c93ed2..f7245783 100644 --- a/bot_bottle/backend/firecracker/setup.py +++ b/bot_bottle/backend/firecracker/setup.py @@ -8,7 +8,7 @@ bundled setup script. `status()` reports what's present, including whether the pool range collides with an existing route. Called through `FirecrackerBottleBackend.setup` / `.status`, which the -generic `./cli.py backend {setup,status}` command dispatches to. +generic `bot-bottle backend {setup,status}` command dispatches to. """ from __future__ import annotations @@ -34,7 +34,7 @@ _UNIT_PATH = Path("/etc/systemd/system") / netpool.SYSTEMD_UNIT def _owner() -> str: # Under `sudo`, USER is root but SUDO_USER is the real invoker — the - # TAPs must be owned by them so `./cli.py start` stays rootless. + # TAPs must be owned by them so `bot-bottle start` stays rootless. return os.environ.get("SUDO_USER") or os.environ.get("USER") or "youruser" @@ -161,7 +161,7 @@ def _setup_systemd() -> None: if rc == 0: sys.stderr.write( f"Installed and started {netpool.SYSTEMD_UNIT}. Verify with " - f"`./cli.py backend status --backend=firecracker`.\n" + f"`bot-bottle backend status --backend=firecracker`.\n" ) else: sys.stderr.write( @@ -181,7 +181,7 @@ def _setup_systemd() -> None: ) sys.stderr.write( f"\n(Or re-run this as root to install it directly: " - f"sudo ./cli.py backend setup --backend=firecracker)\n" + f"sudo bot-bottle backend setup --backend=firecracker)\n" ) @@ -334,7 +334,7 @@ def status() -> int: sys.stderr.write(f"range overlap: none (base {netpool.ip_base()})\n") _report_persistence() if not ok: - sys.stderr.write("\nRun: ./cli.py backend setup --backend=firecracker\n") + sys.stderr.write("\nRun: bot-bottle backend setup --backend=firecracker\n") return 0 if ok else 1 diff --git a/bot_bottle/backend/firecracker/util.py b/bot_bottle/backend/firecracker/util.py index f7cd8e67..9ce31212 100644 --- a/bot_bottle/backend/firecracker/util.py +++ b/bot_bottle/backend/firecracker/util.py @@ -9,7 +9,7 @@ generation. The privileged network setup (TAP pool + nft table) is a one-time operator step — see `netpool.py`, `scripts/firecracker-netpool.sh`, -and `./cli.py backend setup --backend=firecracker`. +and `bot-bottle backend setup --backend=firecracker`. """ from __future__ import annotations @@ -132,18 +132,18 @@ def _require_network_pool() -> None: f"{netpool.pool_size()} slots) overlaps existing routes: " f"{detail}. This can shadow or be shadowed by that route; " f"set BOT_BOTTLE_FC_IP_BASE to a free range and re-run " - f"./cli.py backend setup --backend=firecracker.") + f"bot-bottle backend setup --backend=firecracker.") missing = netpool.missing_taps() if missing: die(f"network pool incomplete — missing TAP devices: " - f"{', '.join(missing)}.\n ./cli.py backend setup --backend=firecracker") + f"{', '.join(missing)}.\n bot-bottle backend setup --backend=firecracker") if shutil.which("nft") is not None and not netpool.nft_table_present(): # nft is queryable and says the table is absent — that's a # definite, catchable misconfiguration; fail early. warn(f"isolation table `inet {netpool.NFT_TABLE}` not found via nft. " "If this is a permissions issue it will be re-checked " "empirically after boot; otherwise run: " - "./cli.py backend setup --backend=firecracker") + "bot-bottle backend setup --backend=firecracker") # --- rootfs pipeline (rootless) ------------------------------------- diff --git a/bot_bottle/backend/freeze.py b/bot_bottle/backend/freeze.py index ec4f4244..220067a8 100644 --- a/bot_bottle/backend/freeze.py +++ b/bot_bottle/backend/freeze.py @@ -43,7 +43,7 @@ class Freezer(ABC): Calls _freeze for the backend-specific snapshot, then writes the committed image reference to per-bottle state and marks the bottle - preserved so the next `./cli.py resume` boots from the snapshot. + preserved so the next `bot-bottle resume` boots from the snapshot. Raises CommitCancelled if the user declines an interactive confirmation prompt (e.g. the macos-container stop prompt). @@ -51,7 +51,7 @@ class Freezer(ABC): image_ref = self._freeze(agent) write_committed_image(agent.slug, image_ref) mark_preserved(agent.slug) - info(f"to resume from this snapshot: ./cli.py resume {agent.slug}") + info(f"to resume from this snapshot: bot-bottle resume {agent.slug}") self._export_hint(agent.slug, image_ref) @abstractmethod diff --git a/bot_bottle/backend/macos_container/setup.py b/bot_bottle/backend/macos_container/setup.py index 2125f3d1..070ee2df 100644 --- a/bot_bottle/backend/macos_container/setup.py +++ b/bot_bottle/backend/macos_container/setup.py @@ -5,7 +5,7 @@ Like Docker, this backend needs no privileged network-pool provisioning running. `setup()` points at the install/`container system start` steps; `status()` reports readiness. Reached via `MacosContainerBottleBackend.setup` / `.status`, dispatched from the -generic `./cli.py backend {setup,status}`. +generic `bot-bottle backend {setup,status}`. """ from __future__ import annotations @@ -75,5 +75,5 @@ def status() -> int: if not _service_running(): ok = False if not ok: - sys.stderr.write("\nRun: ./cli.py backend setup --backend=macos-container\n") + sys.stderr.write("\nRun: bot-bottle backend setup --backend=macos-container\n") return 0 if ok else 1 diff --git a/bot_bottle/backend/selection.py b/bot_bottle/backend/selection.py index 61ad2c52..3b50d982 100644 --- a/bot_bottle/backend/selection.py +++ b/bot_bottle/backend/selection.py @@ -86,7 +86,7 @@ def _print_vm_install_instructions() -> None: info("Then start the service: container system start") else: info("Install Firecracker: https://github.com/firecracker-microvm/firecracker/releases") - info("Configure the host: ./cli.py backend setup") + info("Configure the host: bot-bottle backend setup") def _auto_select_backend(prompt: bool = True) -> str: diff --git a/bot_bottle/cli/commands/backend.py b/bot_bottle/cli/commands/backend.py index a06e44f5..1fb6edcf 100644 --- a/bot_bottle/cli/commands/backend.py +++ b/bot_bottle/cli/commands/backend.py @@ -1,9 +1,9 @@ """`backend` CLI command — generic host setup/status across backends. -`./cli.py backend setup [--backend=NAME]` provisions (or points at how +`bot-bottle backend setup [--backend=NAME]` provisions (or points at how to provision) the chosen backend's one-time host prerequisites. -`./cli.py backend status [--backend=NAME]` reports readiness. -`./cli.py backend teardown [--backend=NAME]` undoes setup (uninstall). +`bot-bottle backend status [--backend=NAME]` reports readiness. +`bot-bottle backend teardown [--backend=NAME]` undoes setup (uninstall). All dispatch to the backend's `setup()` / `status()` / `teardown()` classmethods, so there are no backend-specific commands — swapping diff --git a/bot_bottle/cli/commands/cleanup.py b/bot_bottle/cli/commands/cleanup.py index 24d7c297..a33fd4b3 100644 --- a/bot_bottle/cli/commands/cleanup.py +++ b/bot_bottle/cli/commands/cleanup.py @@ -1,7 +1,7 @@ """cleanup: stop and remove all orphaned bot-bottle resources. Walks every registered backend (docker, firecracker, macos-container) -so a single `./cli.py cleanup` reaps every backend's leftovers — a +so a single `bot-bottle cleanup` reaps every backend's leftovers — a firecracker bottle's VM processes and run dirs won't survive a docker-only cleanup pass (issue addressed alongside #77). diff --git a/bot_bottle/cli/commands/commit.py b/bot_bottle/cli/commands/commit.py index 2e653b62..5258b275 100644 --- a/bot_bottle/cli/commands/commit.py +++ b/bot_bottle/cli/commands/commit.py @@ -4,7 +4,7 @@ Docker bottles are committed to a local Docker image. Macos-container bottles are exported and rebuilt as a local Apple Container image. Firecracker bottles stream the guest rootfs out over SSH and rebuild a local Docker image. The resulting reference is stored in per-bottle -state so the next `./cli.py resume ` boots from the snapshot +state so the next `bot-bottle resume ` boots from the snapshot instead of rebuilding from the Dockerfile. """ @@ -37,7 +37,7 @@ def cmd_commit(argv: list[str]) -> int: if slug is None: active = enumerate_active_agents() if not active: - die("no active bottles; start one with `./cli.py start`") + die("no active bottles; start one with `bot-bottle start`") choices = [a.slug for a in active] slug = tui.filter_select(choices, title="Select bottle to commit") if slug is None: diff --git a/bot_bottle/cli/commands/resume.py b/bot_bottle/cli/commands/resume.py index e3b6bc28..c669fb25 100644 --- a/bot_bottle/cli/commands/resume.py +++ b/bot_bottle/cli/commands/resume.py @@ -8,7 +8,7 @@ override and transcript snapshot under the same state dir. Use case: an interrupted or preserved bottle needs to be relaunched; the operator runs - ./cli.py resume + bot-bottle resume to bring up the replacement from the recorded state. """ diff --git a/bot_bottle/cli/commands/start.py b/bot_bottle/cli/commands/start.py index 71f19776..833cb6a9 100644 --- a/bot_bottle/cli/commands/start.py +++ b/bot_bottle/cli/commands/start.py @@ -203,19 +203,19 @@ def _start_headless( if not os.isatty(stdin_fd): die( "--headless requires a PTY on stdin; run via:\n" - " script -q /dev/null ./cli.py start ..." + " script -q /dev/null bot-bottle start ..." ) agent_name = args.name if not agent_name: - die("--headless requires an agent name: ./cli.py start --headless") + die("--headless requires an agent name: bot-bottle start --headless") manifest.require_agent(agent_name) # raises ManifestError if unknown prompt = args.prompt if not prompt: die( "--headless requires --prompt: " - "./cli.py start --headless --prompt 'Do the thing'" + "bot-bottle start --headless --prompt 'Do the thing'" ) if args.bottle: @@ -319,9 +319,9 @@ def attach_agent( `resume=True` adds `--continue` so claude picks up its most recent session non-interactively (no session-picker prompt). - First-attach paths (`./cli.py start`) leave it False. + First-attach paths (`bot-bottle start`) leave it False. - Used as the inner step of `./cli.py start`.""" + Used as the inner step of `bot-bottle start`.""" runtime = runtime_for(agent_provider_template) info( f"attaching interactive {agent_provider_template} session " @@ -354,7 +354,7 @@ def settle_state(identity: str) -> None: if not identity: return if is_preserved(identity): - info(f"to resume this bottle: ./cli.py resume {identity}") + info(f"to resume this bottle: bot-bottle resume {identity}") return cleanup_state(identity) diff --git a/bot_bottle/cli/commands/supervise.py b/bot_bottle/cli/commands/supervise.py index 05ce18c8..1c7eb942 100644 --- a/bot_bottle/cli/commands/supervise.py +++ b/bot_bottle/cli/commands/supervise.py @@ -110,7 +110,7 @@ def discover_pending() -> list[QueuedProposal]: def _approval_status(qp: QueuedProposal, verb: str) -> str: """Status-line text after a successful approval.""" base = f"{verb} {qp.proposal.tool} for [{qp.label}]" - return f"{base}; resume: ./cli.py resume {qp.label}" + return f"{base}; resume: bot-bottle resume {qp.label}" def _detail_lines( diff --git a/bot_bottle/gateway/git_gate/render.py b/bot_bottle/gateway/git_gate/render.py index 515db6fc..b142ea73 100644 --- a/bot_bottle/gateway/git_gate/render.py +++ b/bot_bottle/gateway/git_gate/render.py @@ -385,7 +385,7 @@ PY ;; esac echo "git-gate: queued # gitleaks:allow supervisor approval $proposal_id" >&2 - echo "git-gate: approve with './cli.py supervise' to continue this push" >&2 + echo "git-gate: approve with 'bot-bottle supervise' to continue this push" >&2 waited=0 while [ "$waited" -lt "$timeout" ]; do status=$(PYTHONPATH="/app${PYTHONPATH:+:$PYTHONPATH}" python3 - "$proposal_id" <<'PY' diff --git a/docs/demo.tape b/docs/demo.tape index e1fe0e67..7a94cda4 100644 --- a/docs/demo.tape +++ b/docs/demo.tape @@ -1,4 +1,4 @@ -# VHS tape — drives `./cli.py start demo` interactively and asks +# VHS tape — drives `bot-bottle start demo` interactively and asks # claude (the AI) to run four probes via natural-language prompts. # Setup (manifest + dummy SSH key + image pre-warm) and teardown # happen outside the tape; record via `bash scripts/demo-record.sh`, @@ -29,7 +29,7 @@ Show # defaults), one git upstream (unreachable on purpose so gitleaks runs # before the gate would forward), and a FAKE_TOKEN env var shaped like # a GitHub PAT. -Type "./cli.py start demo" +Type "bot-bottle start demo" Enter Sleep 8s diff --git a/docs/prds/0011-per-file-md-manifest.md b/docs/prds/0011-per-file-md-manifest.md index ae1c3722..b589e237 100644 --- a/docs/prds/0011-per-file-md-manifest.md +++ b/docs/prds/0011-per-file-md-manifest.md @@ -132,7 +132,7 @@ Each test runs against a temporary `$HOME` and a temporary `$CWD`: can revisit, but the v1 of this PRD is one file = one bottle. - **Hot-reload.** Changes to manifest files take effect at next - `./cli.py start`; we do not watch the directory. + `bot-bottle start`; we do not watch the directory. ## Scope diff --git a/docs/prds/0018-compose-per-instance.md b/docs/prds/0018-compose-per-instance.md index 49422e07..b5e87515 100644 --- a/docs/prds/0018-compose-per-instance.md +++ b/docs/prds/0018-compose-per-instance.md @@ -290,7 +290,7 @@ After this PRD: ### Cleanup CLI -`./cli.py cleanup` switches from "list every container with prefix +`bot-bottle cleanup` switches from "list every container with prefix `bot-bottle-` and every network with prefix `bot-bottle-net-` or `bot-bottle-egress-`" to: @@ -369,7 +369,7 @@ Sized for one PR each, in order. `docker compose up -d` + attach + teardown. Per-sidecar `start()`/ `stop()` lifecycle methods deleted in the same chunk. Compose- log dump on teardown added. -4. **Cleanup CLI on compose.** Switch `./cli.py cleanup` to +4. **Cleanup CLI on compose.** Switch `bot-bottle cleanup` to `docker compose ls`-based discovery; keep prefix-scan as fallback for one release. 5. **Dashboard.** Decide on the discovery question (open question diff --git a/docs/prds/0019-active-agents-in-dashboard.md b/docs/prds/0019-active-agents-in-dashboard.md index 44f558ad..9b69da17 100644 --- a/docs/prds/0019-active-agents-in-dashboard.md +++ b/docs/prds/0019-active-agents-in-dashboard.md @@ -37,7 +37,7 @@ Two rough edges in the current dashboard: shows only pending proposals. If no agent has called a tool, the screen reads "no pending proposals" — even when five bottles are quietly working. The operator has to `docker - compose ls` (or `./cli.py cleanup -n` to see the y/N preview) + compose ls` (or `bot-bottle cleanup -n` to see the y/N preview) to find out what's actually live. 2. **`e` / `p` re-discover-and-disambiguate every invocation.** @@ -82,12 +82,12 @@ the "operator wants to make an unprompted change" case. global across bottles. Filtering ("show me only this agent's proposals") might be a follow-up but isn't this PRD. - **Agent lifecycle from the dashboard.** Starting / stopping - agents stays in `./cli.py start` / `./cli.py cleanup`. The + agents stays in `bot-bottle start` / `bot-bottle cleanup`. The dashboard reads state; it doesn't change it. - **Preserved-but-not-running bottles.** The active-agents list is strictly "what's running now" (cross-referenced from `docker compose ls`). Preserved state dirs without a live - project don't appear — `./cli.py resume ` is the + project don't appear — `bot-bottle resume ` is the path for those. - **A separate per-agent detail view.** The agent rows are one-line summaries. Pressing Enter on a proposal still drops @@ -125,7 +125,7 @@ the "operator wants to make an unprompted change" case. - Changes to proposal handling (`a` / `m` / `r` / Enter all unchanged). - Changes to the queue-dir / supervise sidecar protocol. -- New CLI surface beyond what's in `./cli.py dashboard`. +- New CLI surface beyond what's in `bot-bottle dashboard`. - Touching the manifest, compose renderer, launch lifecycle. ## Proposed design diff --git a/docs/prds/0020-start-and-attach-from-dashboard.md b/docs/prds/0020-start-and-attach-from-dashboard.md index a88d349e..3c65bfbc 100644 --- a/docs/prds/0020-start-and-attach-from-dashboard.md +++ b/docs/prds/0020-start-and-attach-from-dashboard.md @@ -14,8 +14,8 @@ Today the dashboard is read-only: it surfaces pending proposals and active agents (PRD 0019) but can't *start* an agent or *re-enter* one. The operator's path is split — they launch -agents from one terminal (`./cli.py start `), and watch -them from another (`./cli.py dashboard`). +agents from one terminal (`bot-bottle start `), and watch +them from another (`bot-bottle dashboard`). This PRD collapses that split. The dashboard becomes the operator's single surface: pressing a key opens an agent picker, @@ -31,7 +31,7 @@ claude session AND the dashboard process. Exit claude → back to dashboard, bottle still running. Start another agent → two bottles up at once. Quit the dashboard → bottles continue running. Teardown is **always explicit**: the operator presses -`x` on an agent, or runs `./cli.py cleanup` later. +`x` on an agent, or runs `bot-bottle cleanup` later. ## Problem @@ -45,7 +45,7 @@ Two real frictions today: open and the dashboard's "active agents" pane is hopelessly behind reality because they just spawned three in a row. -2. **`./cli.py start` ties the bottle to a single claude +2. **`bot-bottle start` ties the bottle to a single claude session.** The start command's `ExitStack` brings the bottle up, runs claude, and tears down on Ctrl-D — fine for a one- shot session, wrong for "let me bounce in and out of this @@ -60,7 +60,7 @@ captures full-merged logs per bottle (PRD 0018). It already ## Goals / Success Criteria -1. From inside `./cli.py dashboard`, pressing `n` (new) opens +1. From inside `bot-bottle dashboard`, pressing `n` (new) opens an agent picker listing every agent defined in the manifest. Selecting one runs `prepare → preflight → launch`. 2. The preflight Y/N summary renders cleanly — either as a @@ -83,7 +83,7 @@ captures full-merged logs per bottle (PRD 0018). It already state cleanup) without quitting the dashboard. 7. Quitting the dashboard (`q`) leaves every running bottle running. Bottle teardown is always explicit (per-bottle `x` - or `./cli.py cleanup`). The next `./cli.py dashboard` + or `bot-bottle cleanup`). The next `bot-bottle dashboard` invocation re-discovers them via `list_active_slugs()` and surfaces re-attach for any it can reconstruct context for (see "Cross-dashboard re-attach" below). @@ -94,13 +94,13 @@ captures full-merged logs per bottle (PRD 0018). It already embedded-emulator option from the research doc is out of scope. The handoff (option 1) is the v1; option 2 is a separate PRD if and when handoff is observably insufficient. -- **Adopting bottles started by an out-of-dashboard `./cli.py +- **Adopting bottles started by an out-of-dashboard `bot-bottle start` invocation.** Those have their own ExitStack-owner and the dashboard treats them as read-only-watch (already does today). Re-attach only applies to bottles the *current dashboard process* started. - **Resurrecting an out-of-process bottle into a new dashboard - with full re-attach.** A bottle started by `./cli.py start` + with full re-attach.** A bottle started by `bot-bottle start` in another terminal — or by a previous dashboard run, now exited — appears in the agents pane (already does, PRD 0019) and can be re-attached via `docker exec -it claude` because @@ -109,12 +109,12 @@ captures full-merged logs per bottle (PRD 0018). It already context object to drive teardown — e.g., the ExitStack-tracked CA + state cleanup `_settle_state` performs today. Cross-dashboard re-attach uses the existing - `./cli.py cleanup` for teardown, not an `x` keypress (see + `bot-bottle cleanup` for teardown, not an `x` keypress (see open questions). - **Multi-window UI.** Single curses window, two existing panes (proposals + agents); the agent picker is a modal, not a third pane. -- **Removing `./cli.py start`.** Stays as the script-friendly / +- **Removing `bot-bottle start`.** Stays as the script-friendly / legacy entry point. The dashboard is the new default. ## Scope @@ -140,7 +140,7 @@ captures full-merged logs per bottle (PRD 0018). It already ### Out of scope -- Changes to `./cli.py start` itself. It keeps its current +- Changes to `bot-bottle start` itself. It keeps its current shape; the dashboard reuses its internal pieces (backend. prepare / backend.launch) without reaching through the CLI layer. @@ -157,7 +157,7 @@ captures full-merged logs per bottle (PRD 0018). It already Today's flow: ``` -./cli.py start agent +bot-bottle start agent └─ with backend.launch(plan) as bottle: ← bottle alive while inside `with` bottle.exec_agent([...], tty=True) ← blocks until claude exits # context exits → compose down → state cleanup @@ -166,7 +166,7 @@ Today's flow: The proposed dashboard-driven flow: ``` -./cli.py dashboard +bot-bottle dashboard └─ bottles: dict[str, tuple[ContextManager, DockerBottle]] = {} # operator presses `n`, picks agent @@ -205,7 +205,7 @@ Two shifts: evaluation, state-dir reap) doesn't fire on a quit-while- running bottle. It DOES fire when the operator explicitly stops via `x`, because that calls `cm.__exit__`. For - bottles a previous dashboard quit on, `./cli.py cleanup` + bottles a previous dashboard quit on, `bot-bottle cleanup` is the path — its compose-down + state-reap logic already covers the case. @@ -213,7 +213,7 @@ Two shifts: When the dashboard discovers a bottle in `discover_active_agents` that it didn't itself start (a previous-dashboard or external -`./cli.py start` bottle), Enter still attaches via `docker exec +`bot-bottle start` bottle), Enter still attaches via `docker exec -it … claude` — the agent container is running `sleep infinity` exactly the same way regardless of who started it. The only thing the current dashboard lacks for those bottles is the @@ -221,8 +221,8 @@ launch-context object needed to drive a clean teardown via `x`. For v1 we surface this honestly: pressing `x` on a non-owned -agent shows a status hint pointing at `./cli.py cleanup` (or -`./cli.py cleanup` targeted at the slug if we add that flag +agent shows a status hint pointing at `bot-bottle cleanup` (or +`bot-bottle cleanup` targeted at the slug if we add that flag later). The agent stays alive; the operator handles teardown out-of-band. Enter (re-attach) works for both owned and non-owned bottles. @@ -288,7 +288,7 @@ agents pane. `x` on a non-owned agent (discovered via `list_active_slugs` but not in `bottles` dict): no-op with status hint pointing -at `./cli.py cleanup` (the existing path that tears down +at `bot-bottle cleanup` (the existing path that tears down ANY bot-bottle compose project plus reaps state dirs). ### Dashboard quit @@ -300,7 +300,7 @@ the `docker compose` project keeps running. The next dashboard invocation discovers the bottles via `list_active_slugs` and surfaces re-attach. -This is a real departure from today's `./cli.py start` +This is a real departure from today's `bot-bottle start` semantics (which couples bottle lifetime to the process via ExitStack). It's intentional: the dashboard is a watching + acting surface, not a lifetime owner. @@ -322,7 +322,7 @@ Sized for one PR each. dashboard's ExitStack; handoff invokes `attach_agent`. 3. **Re-attach via Enter on owned agents-pane row.** Looks up the slug in the dashboard's `bottles` map; if present → - handoff; else → status-line hint pointing at `./cli.py + handoff; else → status-line hint pointing at `bot-bottle resume`. 4. **Explicit per-bottle stop (`x` keybinding).** Pop the bottle's `close` callback off the stack, call it, refresh. @@ -369,7 +369,7 @@ Sized for one PR each. bottles dict goes out of scope without invoking `__exit__`, so the `docker compose` projects keep running. Bottle teardown is always explicit: per-bottle `x` (for - dashboard-owned), or `./cli.py cleanup` (for everything). + dashboard-owned), or `bot-bottle cleanup` (for everything). ## Open questions diff --git a/docs/prds/0021-dashboard-tmux-split-pane.md b/docs/prds/0021-dashboard-tmux-split-pane.md index abe8e7b1..47573ca4 100644 --- a/docs/prds/0021-dashboard-tmux-split-pane.md +++ b/docs/prds/0021-dashboard-tmux-split-pane.md @@ -46,7 +46,7 @@ window, two panes, no terminal handoff. ## Goals / Success Criteria -1. When the operator runs `./cli.py dashboard` from inside a +1. When the operator runs `bot-bottle dashboard` from inside a tmux session (`$TMUX` set), the dashboard establishes a two-pane layout: dashboard in the left pane, an initially- empty right pane reserved for claude sessions. @@ -313,7 +313,7 @@ Sized small. 3. **Dashboard launched OUTSIDE tmux but tmux is installed.** Should the dashboard auto-exec itself inside a fresh tmux session to get the split-pane experience? Convenient but - surprising (`./cli.py dashboard` shouldn't silently + surprising (`bot-bottle dashboard` shouldn't silently change what session you're in). v1 leaves this off — operators who want split-pane mode start tmux themselves and then run the dashboard. @@ -332,7 +332,7 @@ Sized small. PRD-0019 focus indicator? 6. **Concurrent dashboards in different tmux windows.** - Multiple `./cli.py dashboard` invocations in different + Multiple `bot-bottle dashboard` invocations in different tmux windows would each create their own right pane — probably fine, each has its own state, but worth verifying that `tmux list-panes` is scoped to the right diff --git a/docs/prds/0026-agent-provider-templates.md b/docs/prds/0026-agent-provider-templates.md index a82d3286..2fbb49e9 100644 --- a/docs/prds/0026-agent-provider-templates.md +++ b/docs/prds/0026-agent-provider-templates.md @@ -14,7 +14,7 @@ Today bot-bottle is hard-wired around Claude Code assumptions. When Claude runs ## Goals / Success Criteria -- A Codex agent can be started from the dashboard and via `./cli.py start` alongside a Claude agent. +- A Codex agent can be started from the dashboard and via `bot-bottle start` alongside a Claude agent. - The manifest can express the agent provider/template and, where needed, a custom agent Dockerfile. - Claude-specific default egress/auth behavior is no longer implicit; provider-specific auth is expressed through explicit bottle egress routes and roles. - The launcher preserves required infrastructure behavior for sidecars, egress, pipelock, supervisor MCP, CA handling, git, and shell basics. diff --git a/docs/prds/0048-ssh-deploy-key-provisioning.md b/docs/prds/0048-ssh-deploy-key-provisioning.md index f2e9cf25..b37ff1b3 100644 --- a/docs/prds/0048-ssh-deploy-key-provisioning.md +++ b/docs/prds/0048-ssh-deploy-key-provisioning.md @@ -30,7 +30,7 @@ across every bottle spin-up. This has several consequences: to grant that access. - **Manual rotation burden.** Operators must manage key files on disk, keeping them secure, rotating them on a schedule, and distributing them across hosts - that run `./cli.py start`. + that run `bot-bottle start`. ## Goals / Success Criteria diff --git a/docs/prds/0049-strip-dashboard-to-supervisor-tui.md b/docs/prds/0049-strip-dashboard-to-supervisor-tui.md index 4c1257a9..0225b779 100644 --- a/docs/prds/0049-strip-dashboard-to-supervisor-tui.md +++ b/docs/prds/0049-strip-dashboard-to-supervisor-tui.md @@ -6,7 +6,7 @@ ## Summary -The `./cli.py dashboard` command has grown from its PRD 0013 roots +The `bot-bottle dashboard` command has grown from its PRD 0013 roots (triage supervise proposals) into a parallel-agent control surface (PRDs 0019/0020/0021): an active-agents pane, agent picker + start, re-attach, per-bottle stop, tmux split-pane handoff, operator- @@ -21,7 +21,7 @@ proposals, approve / modify / reject each one, write audit entries, deliver the response that unblocks the agent's tool call. Everything that's about *starting / re-entering / stopping* bottles, or about *operator-initiated* config edits, comes out. The command is renamed -`./cli.py supervise` so the name matches what it does after the cut. +`bot-bottle supervise` so the name matches what it does after the cut. Future agent-management UX is explicitly punted: if and when a control surface for parallel agents resurfaces, the working @@ -41,9 +41,9 @@ Three concrete pains, all downstream of the dashboard's growth: ExitStack-free bottle ownership are intricate enough that shipping the next polish increment costs more than it returns. 2. **No clear ownership of "starts and stops bottles".** Today - that responsibility is split: `./cli.py start` owns one-shot + that responsibility is split: `bot-bottle start` owns one-shot sessions; the dashboard owns multi-session bottles it started - itself; `./cli.py cleanup` owns everything else. The dashboard + itself; `bot-bottle cleanup` owns everything else. The dashboard tracking its own `bottles: dict[str, (cm, bottle, identity)]` that doesn't survive a quit is a confusing third lane. 3. **Wrong target shape for a "manage many agents" UI.** The @@ -97,12 +97,12 @@ problem is everything that got bolted onto that core after. dashboard. After this PRD they don't exist anywhere — operators who need ad-hoc edits use the same path the agents do (call the supervise tool from inside the bottle) or hand-edit the host- - side files and restart the sidecar. Adding a `./cli.py routes + side files and restart the sidecar. Adding a `bot-bottle routes edit ` verb is a follow-up if the loss bites. -- **Removing `./cli.py start` or changing its semantics.** Start +- **Removing `bot-bottle start` or changing its semantics.** Start remains the one-shot launch path. PRD 0020's bottle-outlives- process model is removed; the only path to a long-running - bottle is `./cli.py start` (foreground) plus `cli.py cleanup` + bottle is `bot-bottle start` (foreground) plus `cli.py cleanup` for teardown. - **Removing the supervise-sidecar protocol or any of the three block-remediation engines.** PRDs 0013–0016 stay Active. The @@ -122,8 +122,8 @@ problem is everything that got bolted onto that core after. ### In scope -- **Rename the subcommand.** `./cli.py dashboard` becomes - `./cli.py supervise`. The module moves from `bot_bottle/cli/ +- **Rename the subcommand.** `bot-bottle dashboard` becomes + `bot-bottle supervise`. The module moves from `bot_bottle/cli/ dashboard.py` to `bot_bottle/cli/supervise.py`. The dispatcher in `bot_bottle/cli/__init__.py` and the help text both update. - **Strip the curses loop to proposal-only.** The remaining @@ -167,7 +167,7 @@ problem is everything that got bolted onto that core after. - Any new feature in the supervise TUI. The cut is purely subtractive (except for the rename). -- Behavior changes in `./cli.py start`, `cli.py cleanup`, +- Behavior changes in `bot-bottle start`, `cli.py cleanup`, `cli.py resume`, `cli.py list`, `cli.py info`, `cli.py edit`, `cli.py init` — unchanged. - Changes to the supervise sidecar (`supervise_server.py`, @@ -181,7 +181,7 @@ problem is everything that got bolted onto that core after. ### Final shape of the TUI -After this PRD the `./cli.py supervise` curses surface is: +After this PRD the `bot-bottle supervise` curses surface is: ``` bot-bottle supervise (3 pending) @@ -307,8 +307,8 @@ The PR closes issue #174. 1. **`e` / `p` operator-initiated edits — gone for good or moved to a separate CLI verb?** The PRD removes them with no - replacement. The simplest replacement is `./cli.py routes - edit ` and `./cli.py pipelock edit `, sharing + replacement. The simplest replacement is `bot-bottle routes + edit ` and `bot-bottle pipelock edit `, sharing the existing `apply_routes_change` / `apply_allowlist_change` engines. If the loss is felt within the first parallel run after this lands, that follow-up is a small PR. Leaving diff --git a/docs/prds/0051-launch-selector.md b/docs/prds/0051-launch-selector.md index c31e11ff..220074b1 100644 --- a/docs/prds/0051-launch-selector.md +++ b/docs/prds/0051-launch-selector.md @@ -7,12 +7,12 @@ ## Summary -When `./cli.py start` is run without an agent name, or without a backend +When `bot-bottle start` is run without an agent name, or without a backend explicitly specified, the user currently gets an argparse error (missing positional) or falls through to the `docker` default silently. This PRD adds a terminal UI that appears in those gaps: a filter-select screen built with `curses` that lets the operator pick the agent and/or backend -interactively rather than memorising names or consulting `./cli.py list`. +interactively rather than memorising names or consulting `bot-bottle list`. ## Problem @@ -29,15 +29,15 @@ visible. ## Goals / Success Criteria -1. `./cli.py start` (no arguments) shows an interactive agent selector; +1. `bot-bottle start` (no arguments) shows an interactive agent selector; the selected name is used exactly as if it had been passed on the command line. -2. `./cli.py start ` (no `--backend`, no `BOT_BOTTLE_BACKEND`) +2. `bot-bottle start ` (no `--backend`, no `BOT_BOTTLE_BACKEND`) shows an interactive backend selector; the selected backend is used exactly as if `--backend=` had been passed. -3. `./cli.py start --backend=` (both explicit) shows neither +3. `bot-bottle start --backend=` (both explicit) shows neither screen — no behavioural change from today. -4. `./cli.py start` (no arguments, no env backend) shows the agent +4. `bot-bottle start` (no arguments, no env backend) shows the agent selector first, then the backend selector. 5. The filter-select widget is a standalone utility (`bot_bottle/cli/tui.py`) shared by both selectors. @@ -57,7 +57,7 @@ visible. - No pagination beyond what fits in the terminal window (scroll via cursor movement is sufficient for typical agent counts). - No multi-select; exactly one item is chosen per invocation. -- No changes to `./cli.py resume`, `./cli.py list`, or any other +- No changes to `bot-bottle resume`, `bot-bottle list`, or any other subcommand. ## Design @@ -83,7 +83,7 @@ def filter_select( The widget renders to the tty file descriptor opened via `curses.initscr` (or `curses.newterm` on the tty fd so stdout remains clean for callers -that pipe `./cli.py`). +that pipe `bot-bottle`). Layout (full-width, minimal): @@ -140,7 +140,7 @@ agent picker can populate itself from the real manifest. The same `filter_select` opens `/dev/tty` and feeds it as the input file to `curses.wrapper`-equivalent code (using `curses.newterm` to avoid clobbering the caller's stdout/stderr). This keeps the picker -composable — callers can pipe `./cli.py` output without the curses +composable — callers can pipe `bot-bottle` output without the curses draw sequences contaminating the pipe. ## Implementation chunks diff --git a/docs/prds/0060-commit-bottle-state.md b/docs/prds/0060-commit-bottle-state.md index f2ba53f9..10ab50d4 100644 --- a/docs/prds/0060-commit-bottle-state.md +++ b/docs/prds/0060-commit-bottle-state.md @@ -30,12 +30,12 @@ snapshot before a planned host reboot or hardware migration. ## Goals / Success Criteria -- `./cli.py commit []` takes a snapshot of the running agent and +- `bot-bottle commit []` takes a snapshot of the running agent and stores it as a local artifact. - Without a slug argument the command shows the same interactive picker as `start` (the list of active slugs). - The committed artifact reference is stored in per-bottle state so - that the next `./cli.py resume ` automatically uses the + that the next `bot-bottle resume ` automatically uses the snapshot instead of rebuilding from the Dockerfile. - `mark_preserved` is called so the state dir survives the normal session-end cleanup. @@ -81,7 +81,7 @@ to the committed `.smolmachine` artifact. ### `commit` command ``` -./cli.py commit [] +bot-bottle commit [] ``` 1. Resolve slug (arg or interactive picker from `enumerate_active_agents`). diff --git a/docs/prds/0066-separate-agent-bottle-selection.md b/docs/prds/0066-separate-agent-bottle-selection.md index e719b21f..97994021 100644 --- a/docs/prds/0066-separate-agent-bottle-selection.md +++ b/docs/prds/0066-separate-agent-bottle-selection.md @@ -37,7 +37,7 @@ egress policy), the operator must duplicate the agent file and change the selection order, as the effective bottle for the session. 5. Confirming with an empty selection falls back to the agent's `bottle:` field. If neither is set, a ManifestError is raised pointing the operator at the fix. -6. The ordered bottle list is stored in launch metadata so `./cli.py resume` +6. The ordered bottle list is stored in launch metadata so `bot-bottle resume` uses the same bottles. 7. The preflight summary (`y/N` screen) shows the effective bottle name(s). 8. The multi-select picker supports incremental filtering, Space/Enter to toggle @@ -52,7 +52,7 @@ egress policy), the operator must duplicate the agent file and change the - Reordering the selection list from within the picker (order = insertion order; drag-and-drop is out of scope). - Storing bottle selection history / MRU. -- Changes to `./cli.py edit`, `./cli.py list`, or `./cli.py info`. +- Changes to `bot-bottle edit`, `bot-bottle list`, or `bot-bottle info`. - Removing the `bottle:` key from the agent schema (it stays, now optional). ## Design diff --git a/docs/prds/0068-smolmachines-linux.md b/docs/prds/0068-smolmachines-linux.md index e2f20506..c1102bac 100644 --- a/docs/prds/0068-smolmachines-linux.md +++ b/docs/prds/0068-smolmachines-linux.md @@ -74,7 +74,7 @@ macOS-only for v1. Three concrete blockers: ## Goals / Success Criteria -- `BOT_BOTTLE_BACKEND=smolmachines ./cli.py start ` launches, +- `BOT_BOTTLE_BACKEND=smolmachines bot-bottle start ` launches, runs, and tears down a bottle on a Linux host with `/dev/kvm`. - The TSI allowlist is enforced on Linux: PRD 0022's `tests/integration/test_sandbox_escape.py` passes against diff --git a/docs/prds/0077-macos-container-ci-runner.md b/docs/prds/0077-macos-container-ci-runner.md index 44d76375..5531f653 100644 --- a/docs/prds/0077-macos-container-ci-runner.md +++ b/docs/prds/0077-macos-container-ci-runner.md @@ -39,7 +39,7 @@ end-to-end runner that would catch the *next* macOS-only launch regression.) PR #470 (#414) already made the integration suite backend-agnostic: `skip_unless_selected_backend_available()` gates on the *selected* backend's own `is_backend_ready()` rather than `docker_available()`, and each -integration job runs `./cli.py backend status --backend=` as a preflight +integration job runs `bot-bottle backend status --backend=` as a preflight that fails loudly when the backend is missing. That is the machinery this job plugs into; this PRD supplies the runner and the job. @@ -98,7 +98,7 @@ Modeled on `integration-firecracker`: - `concurrency: { group: integration-macos-infra, cancel-in-progress: false }` to serialize runs against the singleton. - **Preflight** — `command -v container`, `container system status`, then - `./cli.py backend status --backend=macos-container`; any failure exits + `bot-bottle backend status --backend=macos-container`; any failure exits non-zero so a misprovisioned runner fails loudly instead of silently skipping. - Run the integration suite under coverage with diff --git a/docs/prds/0078-install-script.md b/docs/prds/0078-install-script.md index fbf2ccb2..09bd04a7 100644 --- a/docs/prds/0078-install-script.md +++ b/docs/prds/0078-install-script.md @@ -16,7 +16,7 @@ verifies host prerequisites after install. ## Problem There is currently no install path for new users. The only way to run -bot-bottle is to clone the repo and invoke `./cli.py`. This blocks any +bot-bottle is to clone the repo and invoke `bot-bottle`. This blocks any public demo: readers want `curl | sh` or `pipx install`, not a manual clone-and-configure flow. There is also no single command that tells a user whether their host is actually ready to run a bottle. diff --git a/docs/research/built-in-supervisor-design.md b/docs/research/built-in-supervisor-design.md index 4ddd2aac..cf0e9347 100644 --- a/docs/research/built-in-supervisor-design.md +++ b/docs/research/built-in-supervisor-design.md @@ -6,7 +6,7 @@ Can bot-bottle grow a built-in supervisor — TUI inventory plus PR-feedback rou ## Context -bot-bottle today is a fleet *executor*: `./cli.py start ` brings up one bottle (agent container + pipelock + optional git-gate + optional cred-proxy on a per-bottle internal network), and `cli.py` tears it down when the session ends. There is no inventory view, no idle-detection, no automated reaction to PR or CI events. In parallel use, a human is the supervisor — opening one terminal per bottle, switching between them, and watching upstream PR state by hand. +bot-bottle today is a fleet *executor*: `bot-bottle start ` brings up one bottle (agent container + pipelock + optional git-gate + optional cred-proxy on a per-bottle internal network), and `cli.py` tears it down when the session ends. There is no inventory view, no idle-detection, no automated reaction to PR or CI events. In parallel use, a human is the supervisor — opening one terminal per bottle, switching between them, and watching upstream PR state by hand. A separate survey of the broader ecosystem ([agent control dashboards research, mid-2026](https://gitea.dideric.is/didericis/consilium-research/src/branch/main/developer-workflow/agent-control-dashboards-2026-05-24.md)) sorts dashboards into five tiers (session managers, parallel runners, Kanban boards, mission-control SPAs, observability backends). The earlier first-pass conclusion was that a full SPA tier conflicts with bot-bottle's isolation model. This doc reconsiders the smaller question: a TUI supervisor in the existing Python CLI. @@ -25,13 +25,13 @@ A supervisor doesn't have to be heavy. A TUI built into the existing Python CLI, Three layers, each independently useful, in order of ambition: -### 1. `./cli.py status` — read-only inventory +### 1. `bot-bottle status` — read-only inventory Reads `docker ps` filtered by a bottle label and tails each bottle's session log. Reports per bottle: name, agent, uptime, last-activity timestamp, token spend if available, associated PR/branch if recorded. No new daemons. No new ports. No new credentials. ~100 lines. -### 2. `./cli.py watch` — TUI over the same data +### 2. `bot-bottle watch` — TUI over the same data Same data as `status`, rendered with auto-refresh and keyboard shortcuts that shell out to the existing `cli.py attach / stop / start` commands. @@ -39,7 +39,7 @@ Library choice: prefer the stdlib `curses` module to stay stdlib-first; fall bac This is the Claude Squad / tmux-agent-status pattern, applied to bottles instead of tmux sessions. The whole category exists *because* a TUI is the lightweight shape that doesn't require what the SPA tier requires. -### 3. `./cli.py supervise` — PR feedback router +### 3. `bot-bottle supervise` — PR feedback router The optional, more ambitious layer. The bottle manifest gains an optional field: @@ -49,7 +49,7 @@ pr_watch: branch: agent/task-42 ``` -`./cli.py supervise` polls the named upstream for new review comments and CI failures on `branch`. When one fires, it surfaces as a desktop notification or a flash in the TUI. The human decides what to do with the feedback — there is no autonomous loop that feeds the comment back into a bottle's next prompt (see "Where to be conservative" for why). +`bot-bottle supervise` polls the named upstream for new review comments and CI failures on `branch`. When one fires, it surfaces as a desktop notification or a flash in the TUI. The human decides what to do with the feedback — there is no autonomous loop that feeds the comment back into a bottle's next prompt (see "Where to be conservative" for why). The polling token is a **host** token (the same `GH_PAT` / Gitea token the host already keeps in shell env), not a bottle credential. The supervisor never holds bottle secrets. @@ -62,7 +62,7 @@ The load-bearing question is whether the supervisor introduces the privileged-ch | Reaching into running bottles | Supervisor reads `docker ps` and host-side log files. The host already sees both — Docker is the trust boundary, the supervisor is on the host side of it. | | Holding bottle credentials | The polling token is a host token. The supervisor never receives `bottle.cred_proxy.routes` entries; it has no path to them. | | Bridging between bottles | The supervisor does not relay state from bottle A to bottle B. It relays *upstream PR state* to a bottle's next prompt — and only if the manifest opts in. | -| New attack surface | All "control" actions go through `./cli.py start `, which already enforces the manifest. The supervisor is an automated caller of the existing CLI, not a parallel control plane. | +| New attack surface | All "control" actions go through `bot-bottle start `, which already enforces the manifest. The supervisor is an automated caller of the existing CLI, not a parallel control plane. | The boundary stays at the bottle wall. The supervisor looks outward at git/PR state and downward at Docker; it does not look *inward* through pipelock. diff --git a/docs/research/claude-code-pane-in-dashboard.md b/docs/research/claude-code-pane-in-dashboard.md index 34ff51ff..1ecda14d 100644 --- a/docs/research/claude-code-pane-in-dashboard.md +++ b/docs/research/claude-code-pane-in-dashboard.md @@ -13,11 +13,11 @@ What's the cheapest path to that, and where does it bottom out? ## What "interact" means -Today the flow is bimodal. `./cli.py start ` brings the +Today the flow is bimodal. `bot-bottle start ` brings the bottle up and immediately drops you into an interactive `docker exec -it bot-bottle- claude ...` — claude-code owns the whole terminal until you Ctrl-D out, at which point the -bottle tears down. The dashboard (`./cli.py dashboard`) is a +bottle tears down. The dashboard (`bot-bottle dashboard`) is a *separate* invocation that watches across bottles but never exposes the claude TUI itself. @@ -107,7 +107,7 @@ What's not good: This is the v1 the project's existing code-shape strongly prefers. It clears the bar of "let me talk to claude-code -without quitting `./cli.py dashboard`." +without quitting `bot-bottle dashboard`." ## Option 2: Embedded emulator diff --git a/docs/research/malicious-commit-scanning.md b/docs/research/malicious-commit-scanning.md index cac426f0..11a162f6 100644 --- a/docs/research/malicious-commit-scanning.md +++ b/docs/research/malicious-commit-scanning.md @@ -84,7 +84,7 @@ dangerous changes before they left*. (`supervise_gitleaks_allow`, [`git_gate_render.py`](../../bot_bottle/git_gate_render.py)). Extend the same flow to **high-risk file classes**: any commit touching CI/build/deploy scripts, auth/crypto code, egress config, or - adding/changing dependencies → route to `./cli.py supervise`. This is + adding/changing dependencies → route to `bot-bottle supervise`. This is attribution/policy, not detection, and it's the strongest thing here — a human on exactly the temporal-escape surfaces. 4. **LLM semantic diff-review — the behavioral backstop.** The only @@ -146,7 +146,7 @@ preserves the bottom-up distribution funnel. **governed code-egress review**, not "we resell inference" (the monetization notes explicitly warn against reselling compute). - **The web-console supervise/review flow — the strongest anchor.** Turn - the CLI `./cli.py supervise` approval into a real review surface: + the CLI `bot-bottle supervise` approval into a real review surface: rendered diff + finding context, approve/reject, **who-approved audit trail, RBAC on approvers, mobile/phone-control** (ties to the dashboard/vault north star). This is "central enforcement + diff --git a/docs/research/manifest-format-and-grouping.md b/docs/research/manifest-format-and-grouping.md index 5efb3756..c2991557 100644 --- a/docs/research/manifest-format-and-grouping.md +++ b/docs/research/manifest-format-and-grouping.md @@ -100,7 +100,7 @@ resolver globs each directory. becomes file ops (mkdir, mv, rm) instead of editing one file. Power users prefer that; new users may not. - Discovery requires `ls`, not "grep one file." Tooling helps - (e.g. `./cli.py list`) but the manifest is no longer a single + (e.g. `bot-bottle list`) but the manifest is no longer a single artifact to email or ship. - Atomicity: swapping a bottle name across agents touches multiple files. Git handles this fine; a one-shot text editor @@ -364,7 +364,7 @@ wins; none of the body-prose or dependency story. warns / ignores / breaks. If it warns, we'd want a different field name (e.g. `bot-bottle-bottle`) or a namespaced block. - **Migration story.** Is the project willing to ship a one-shot - `./cli.py migrate-manifest` command that does the JSON → MD + `bot-bottle migrate-manifest` command that does the JSON → MD conversion? Or do users just rewrite by hand from the new docs? - **Bottle file body content.** If most bottle .md files have an empty body, is the MD-with-frontmatter format still warranted? diff --git a/docs/research/polish-priorities.md b/docs/research/polish-priorities.md index 5199fc9e..0644d48b 100644 --- a/docs/research/polish-priorities.md +++ b/docs/research/polish-priorities.md @@ -34,7 +34,7 @@ on top of working onboarding. A first-time user today goes through five steps: install Docker, install `uv`, set `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN`, write -`bot-bottle.json`, run `./cli.py start`. One of those is +`bot-bottle.json`, run `bot-bottle start`. One of those is "author a JSON manifest." Polished tools in this category let users skip that step on day one. The fix is an `init` subcommand that drops a working `bot-bottle.json` with a default `coder` diff --git a/docs/research/remote-docker-vm-isolation.md b/docs/research/remote-docker-vm-isolation.md index fc1b0940..ad2ddfbc 100644 --- a/docs/research/remote-docker-vm-isolation.md +++ b/docs/research/remote-docker-vm-isolation.md @@ -131,7 +131,7 @@ The minimum-viable workflow, no bot-bottle code changes: 3. SSH in. 4. `git clone` bot-bottle on the VM, drop a manifest in place, inject `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN` via the provider's secrets path. -5. `./cli.py start ` — the existing launcher handles the rest. +5. `bot-bottle start ` — the existing launcher handles the rest. 6. On exit: destroy the VM. No host artifacts persist. For the "VPN pivot" failure mode, see diff --git a/scripts/firecracker-netpool.sh b/scripts/firecracker-netpool.sh index 5178ea9f..88590a3c 100755 --- a/scripts/firecracker-netpool.sh +++ b/scripts/firecracker-netpool.sh @@ -9,7 +9,7 @@ # # Why a pool + one-time setup: creating a TAP and assigning it an IP # needs CAP_NET_ADMIN. Pre-creating user-owned, pre-addressed TAPs -# means `./cli.py start` never needs root. The nft table is static +# means `bot-bottle start` never needs root. The nft table is static # (keyed on the `bbfc*` interface wildcard), so it covers every slot # without per-launch changes. # diff --git a/tests/README.md b/tests/README.md index 988ffa86..6de52675 100644 --- a/tests/README.md +++ b/tests/README.md @@ -95,7 +95,7 @@ BOT_BOTTLE_RUN_CANARIES=1 python -m scripts.unittest_gate \ ``` 4. Skip guards live in `tests._backend` and gate on the backend's own readiness check, `bot_bottle.backend.has_backend` — the same probe - behind `./cli.py backend status`: + behind `bot-bottle backend status`: - Backend-agnostic tests (go through `get_bottle_backend()`) decorate the class with `@skip_unless_selected_backend_available()` — the test runs against whichever backend `BOT_BOTTLE_BACKEND` selects and skips @@ -105,7 +105,7 @@ BOT_BOTTLE_RUN_CANARIES=1 python -m scripts.unittest_gate \ `backend.docker.*`, …) decorate with `@skip_unless_backend("docker")` so they no-op under a run targeting a different backend. - Each CI integration job runs `./cli.py backend status --backend=` + Each CI integration job runs `bot-bottle backend status --backend=` as a preflight, which prints a clear per-check summary and exits non-zero when the backend is missing — so absent infrastructure fails the job instead of hiding among per-test `unittest.skip` lines. diff --git a/tests/_backend.py b/tests/_backend.py index 54f27f87..4da33b01 100644 --- a/tests/_backend.py +++ b/tests/_backend.py @@ -2,7 +2,7 @@ Each integration test targets the backend named by ``BOT_BOTTLE_BACKEND`` (default ``docker``) and gates on that backend's full readiness check — -``is_backend_ready()`` (equivalent to ``./cli.py backend status``), not just +``is_backend_ready()`` (equivalent to ``bot-bottle backend status``), not just a binary-on-PATH probe. When the backend is not ready, diagnostic output is printed during test discovery so the operator sees a concrete reason for each skip. diff --git a/tests/unit/test_backend_skip_guards.py b/tests/unit/test_backend_skip_guards.py index ecbc4a42..68e9b50f 100644 --- a/tests/unit/test_backend_skip_guards.py +++ b/tests/unit/test_backend_skip_guards.py @@ -1,7 +1,7 @@ """Tests for the backend-aware skip guards in ``tests/_backend.py``. The guards delegate their readiness check to -``bot_bottle.backend.is_backend_ready`` (the probe behind ``./cli.py backend +``bot_bottle.backend.is_backend_ready`` (the probe behind ``bot-bottle backend status``); here that probe is mocked so the unit job asserts the selection/skip logic without either backend present on the runner. """ diff --git a/tests/unit/test_cli_backend.py b/tests/unit/test_cli_backend.py index 40f2890c..b0331277 100644 --- a/tests/unit/test_cli_backend.py +++ b/tests/unit/test_cli_backend.py @@ -1,6 +1,6 @@ """Unit: the generic `backend` CLI command. -`./cli.py backend {setup,status} [--backend=NAME]` resolves a backend +`bot-bottle backend {setup,status} [--backend=NAME]` resolves a backend and dispatches to its `setup()` / `status()` classmethods — no backend-specific commands. Also covers the docker backend's own setup/status logic (the firecracker path is exercised by