diff --git a/bot_bottle/agent_provider.py b/bot_bottle/agent_provider.py index 3e9c245..b557c3e 100644 --- a/bot_bottle/agent_provider.py +++ b/bot_bottle/agent_provider.py @@ -9,11 +9,13 @@ from __future__ import annotations from dataclasses import dataclass from pathlib import Path +from typing import Literal PROVIDER_CLAUDE = "claude" PROVIDER_CODEX = "codex" PROVIDER_TEMPLATES = frozenset({PROVIDER_CLAUDE, PROVIDER_CODEX}) +PromptMode = Literal["append_file", "read_prompt_file"] @dataclass(frozen=True) @@ -24,7 +26,7 @@ class AgentProviderRuntime: dockerfile: str auth_role: str placeholder_env: str - prompt_mode: str + prompt_mode: PromptMode bypass_args: tuple[str, ...] resume_args: tuple[str, ...] remote_control_args: tuple[str, ...] @@ -41,7 +43,7 @@ _RUNTIMES = { dockerfile=str(_REPO_ROOT / "Dockerfile.claude"), auth_role="claude_code_oauth", placeholder_env="CLAUDE_CODE_OAUTH_TOKEN", - prompt_mode="claude_append_file", + prompt_mode="append_file", bypass_args=("--dangerously-skip-permissions",), resume_args=("--continue",), remote_control_args=("--remote-control",), @@ -53,7 +55,7 @@ _RUNTIMES = { dockerfile=str(_REPO_ROOT / "Dockerfile.codex"), auth_role="codex_auth", placeholder_env="OPENAI_API_KEY", - prompt_mode="codex_read_prompt_file", + prompt_mode="read_prompt_file", bypass_args=("--dangerously-bypass-approvals-and-sandbox",), resume_args=("resume", "--last"), remote_control_args=(), @@ -66,13 +68,16 @@ def runtime_for(template: str) -> AgentProviderRuntime: def prompt_args( - prompt_mode: str, prompt_path: str | None, *, argv: list[str] | None = None, + prompt_mode: PromptMode, + prompt_path: str | None, + *, + argv: list[str] | None = None, ) -> list[str]: if not prompt_path: return [] - if prompt_mode == "claude_append_file": + if prompt_mode == "append_file": return ["--append-system-prompt-file", prompt_path] - if prompt_mode == "codex_read_prompt_file": + if prompt_mode == "read_prompt_file": if argv and "resume" in argv: return [] return [f"Read and follow the instructions in {prompt_path}."] diff --git a/bot_bottle/backend/__init__.py b/bot_bottle/backend/__init__.py index 9cd8193..bd680de 100644 --- a/bot_bottle/backend/__init__.py +++ b/bot_bottle/backend/__init__.py @@ -130,8 +130,8 @@ class ActiveAgent: class Bottle(ABC): """Handle to a running bottle. Yielded by a backend's launch step. - `exec_claude` runs `claude` inside the bottle and blocks until the - session ends. `exec` runs a POSIX shell script inside the bottle + `exec_agent` runs the selected agent CLI inside the bottle and + blocks until the session ends. `exec` runs a POSIX shell script inside the bottle and returns the captured result. `cp_in` copies a host path into the bottle. `close` is an idempotent alias for context-manager teardown. @@ -140,11 +140,11 @@ class Bottle(ABC): name: str @abstractmethod - def claude_argv( + def agent_argv( self, argv: list[str], *, tty: bool = True, ) -> list[str]: - """Return the host-side argv that runs `claude ` - inside the bottle. Used by `exec_claude` for foreground + """Return the host-side argv that runs the selected agent + inside the bottle. Used by `exec_agent` for foreground handoffs and by the dashboard's tmux `respawn-pane` flow, which needs the argv up front (it spawns claude in a tmux pane rather than as a child of the current process). @@ -155,7 +155,7 @@ class Bottle(ABC): ... @abstractmethod - def exec_claude(self, argv: list[str], *, tty: bool = True) -> int: ... + def exec_agent(self, argv: list[str], *, tty: bool = True) -> int: ... @abstractmethod def exec(self, script: str, *, user: str = "node") -> ExecResult: @@ -270,7 +270,7 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]): backend-specific terms (Docker: resolved container name; fly: machine id). Returns the in-container prompt path if a prompt was provisioned, else None — the Bottle handle uses it to - decide whether to add --append-system-prompt-file to claude's + decide whether to add provider-specific prompt args to the agent's argv. Default orchestration: ca → prompt → skills → git → @@ -305,7 +305,7 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]): """Copy the prompt file into the running bottle. Returns the in-container path iff the agent has a non-empty prompt; callers use the return value to decide whether to add - --append-system-prompt-file to claude's argv.""" + provider-specific prompt args to the agent's argv.""" @abstractmethod def provision_skills(self, plan: PlanT, target: str) -> None: diff --git a/bot_bottle/backend/docker/bottle.py b/bot_bottle/backend/docker/bottle.py index 5d4ba3e..7c94c40 100644 --- a/bot_bottle/backend/docker/bottle.py +++ b/bot_bottle/backend/docker/bottle.py @@ -5,7 +5,7 @@ from __future__ import annotations import subprocess from typing import Callable -from ...agent_provider import prompt_args +from ...agent_provider import PromptMode, prompt_args from .. import Bottle, ExecResult @@ -19,12 +19,11 @@ class DockerBottle(Bottle): prompt_path_in_container: str | None, *, agent_command: str = "claude", - agent_prompt_mode: str = "claude_append_file", + agent_prompt_mode: PromptMode = "append_file", ): self.name = container self._teardown = teardown self._prompt_path = prompt_path_in_container - self._agent_command = agent_command self._agent_prompt_mode = agent_prompt_mode self.agent_command = agent_command self.agent_provider_template = ( @@ -32,7 +31,7 @@ class DockerBottle(Bottle): ) self._closed = False - def claude_argv( + def agent_argv( self, argv: list[str], *, tty: bool = True, ) -> list[str]: full_argv = list(argv) @@ -42,12 +41,12 @@ class DockerBottle(Bottle): cmd = ["docker", "exec"] if tty: cmd.append("-it") - cmd.extend([self.name, self._agent_command, *full_argv]) + cmd.extend([self.name, self.agent_command, *full_argv]) return cmd - def exec_claude(self, argv: list[str], *, tty: bool = True) -> int: + def exec_agent(self, argv: list[str], *, tty: bool = True) -> int: return subprocess.run( - self.claude_argv(argv, tty=tty), check=False, + self.agent_argv(argv, tty=tty), check=False, ).returncode def exec(self, script: str, *, user: str = "node") -> ExecResult: diff --git a/bot_bottle/backend/docker/bottle_plan.py b/bot_bottle/backend/docker/bottle_plan.py index 31fa0ce..63d7e2c 100644 --- a/bot_bottle/backend/docker/bottle_plan.py +++ b/bot_bottle/backend/docker/bottle_plan.py @@ -11,6 +11,7 @@ import sys from dataclasses import dataclass, field from pathlib import Path +from ...agent_provider import PromptMode from ...egress import EgressPlan from ...git_gate import GitGatePlan from ...log import info @@ -52,7 +53,7 @@ class DockerBottlePlan(BottlePlan): supervise_plan: SupervisePlan | None use_runsc: bool agent_command: str = "claude" - agent_prompt_mode: str = "claude_append_file" + agent_prompt_mode: PromptMode = "append_file" agent_provider_template: str = "claude" def print(self, *, remote_control: bool) -> None: diff --git a/bot_bottle/backend/docker/launch.py b/bot_bottle/backend/docker/launch.py index 935fadd..37dfa65 100644 --- a/bot_bottle/backend/docker/launch.py +++ b/bot_bottle/backend/docker/launch.py @@ -23,7 +23,7 @@ The flow is: entries inherit without rendering values into the file). 8. Provision (CA install, prompt copy, skills, git, supervise config) — unchanged, uses `docker exec`. - 9. Yield a DockerBottle handle. `exec_claude` runs claude via + 9. Yield a DockerBottle handle. `exec_agent` runs claude via `docker exec -it` exactly like the pre-compose world. Teardown (ExitStack callbacks fire in reverse): @@ -204,7 +204,7 @@ def launch( # the agent container by its known name. prompt_path = provision(plan, plan.container_name) - # Step 9: yield. exec_claude continues to use `docker exec -it` + # Step 9: yield. exec_agent continues to use `docker exec -it` # — the agent runs `sleep infinity` per the renderer's # service spec. yield DockerBottle( diff --git a/bot_bottle/backend/smolmachines/bottle.py b/bot_bottle/backend/smolmachines/bottle.py index 5173f63..5cebc4c 100644 --- a/bot_bottle/backend/smolmachines/bottle.py +++ b/bot_bottle/backend/smolmachines/bottle.py @@ -1,15 +1,15 @@ """SmolmachinesBottle — running-instance handle (PRD 0023 chunk 2d). -Routes `exec_claude` / `exec` / `cp_in` through `smolvm machine +Routes `exec_agent` / `exec` / `cp_in` through `smolvm machine exec` / `smolvm machine cp`. The handle is yielded by `launch` and torn down via the surrounding ExitStack on context exit; `close` is a no-op idempotent alias so the BottleBackend ABC's context-manager contract is satisfied. User context: `smolvm machine exec` runs commands as root in the -VM, but the agent image's USER is `node` and claude-code refuses -to run as root with `--dangerously-skip-permissions`. Both -`exec_claude` and `exec` switch to the requested user (default +VM, but the agent image's USER is `node` and agent CLIs may refuse +to run as root in bypass modes. Both +`exec_agent` and `exec` switch to the requested user (default `node`) via `runuser -u --` and set `HOME` / `USER` through `smolvm -e` — avoiding `runuser -l`'s login-shell wiring (PAM session setup, /etc/profile sourcing) which can hang on a @@ -21,7 +21,7 @@ import subprocess import sys from typing import Mapping -from ...agent_provider import prompt_args +from ...agent_provider import PromptMode, prompt_args from .. import Bottle, ExecResult from . import pty_resize as _pty_resize from . import smolvm as _smolvm @@ -34,8 +34,8 @@ from . import smolvm as _smolvm _PTY_RESIZE_SCRIPT = _pty_resize.__file__ -# Per-user env the agent image's USER (node) expects. claude -# reads ~/.claude.json + writes session state under ~/.claude/; +# Per-user env the agent image's USER (node) expects. Some providers +# write session state under the user's home directory; # bare `runuser -u` inherits root's HOME=/root, which claude # can't write to. Set HOME / USER explicitly through smolvm -e # so the child process sees them. @@ -74,7 +74,7 @@ class SmolmachinesBottle(Bottle): prompt_path: str | None = None, guest_env: Mapping[str, str] | None = None, agent_command: str = "claude", - agent_prompt_mode: str = "claude_append_file", + agent_prompt_mode: PromptMode = "append_file", ) -> None: self.name = machine_name # In-VM path to the agent's prompt file. None when the @@ -86,14 +86,13 @@ class SmolmachinesBottle(Bottle): # Forwarded on every `smolvm machine exec` via `-e K=V` # because exec doesn't inherit from machine_create's env. self._guest_env = dict(guest_env or {}) - self._agent_command = agent_command self._agent_prompt_mode = agent_prompt_mode self.agent_command = agent_command self.agent_provider_template = ( "codex" if agent_command == "codex" else "claude" ) - def claude_argv( + def agent_argv( self, argv: list[str], *, tty: bool = True, ) -> list[str]: flags = ["smolvm", "machine", "exec", "--name", self.name] @@ -101,17 +100,17 @@ class SmolmachinesBottle(Bottle): flags += ["-i", "-t"] flags += _env_flags_for("node") flags += _guest_env_flags(self._guest_env) - claude_tail = [self._agent_command] + agent_tail = [self.agent_command] provider_prompt_args = prompt_args( self._agent_prompt_mode, self._prompt_path, argv=argv, ) - if self._agent_prompt_mode == "codex_read_prompt_file": - claude_tail += argv - claude_tail += provider_prompt_args + if self._agent_prompt_mode == "read_prompt_file": + agent_tail += argv + agent_tail += provider_prompt_args else: - claude_tail += provider_prompt_args - claude_tail += argv - flags += ["--", "runuser", "-u", "node", "--", *claude_tail] + agent_tail += provider_prompt_args + agent_tail += argv + flags += ["--", "runuser", "-u", "node", "--", *agent_tail] if not tty: # No PTY allocated — no SIGWINCH to forward, no resize # bridge needed. Skip the wrapper so non-interactive @@ -123,10 +122,10 @@ class SmolmachinesBottle(Bottle): self.name, "--", *flags, ] - def exec_claude(self, argv: list[str], *, tty: bool = True) -> int: - """Run `claude` interactively inside the VM as the `node` + def exec_agent(self, argv: list[str], *, tty: bool = True) -> int: + """Run the selected agent interactively inside the VM as the `node` user. Inherits the operator's terminal (stdin / stdout / - stderr) so the session feels native. Blocks until claude + stderr) so the session feels native. Blocks until the agent exits; returns the in-VM exit code. We bypass the captured-output `machine_exec` helper here @@ -138,7 +137,7 @@ class SmolmachinesBottle(Bottle): avoid login-shell wiring. HOME / USER come from `smolvm -e` instead, which sets them on the process env.""" return subprocess.run( - self.claude_argv(argv, tty=tty), check=False, + self.agent_argv(argv, tty=tty), check=False, ).returncode def exec(self, script: str, *, user: str = "node") -> ExecResult: diff --git a/bot_bottle/backend/smolmachines/bottle_plan.py b/bot_bottle/backend/smolmachines/bottle_plan.py index ed6fb14..c97ac4d 100644 --- a/bot_bottle/backend/smolmachines/bottle_plan.py +++ b/bot_bottle/backend/smolmachines/bottle_plan.py @@ -12,6 +12,7 @@ import sys from dataclasses import dataclass from pathlib import Path +from ...agent_provider import PromptMode from ...egress import EgressPlan from ...git_gate import GitGatePlan from ...log import info @@ -93,7 +94,7 @@ class SmolmachinesBottlePlan(BottlePlan): agent_git_gate_host: str = "" agent_supervise_url: str = "" agent_command: str = "claude" - agent_prompt_mode: str = "claude_append_file" + agent_prompt_mode: PromptMode = "append_file" agent_provider_template: str = "claude" agent_dockerfile_path: str = "" diff --git a/bot_bottle/backend/smolmachines/launch.py b/bot_bottle/backend/smolmachines/launch.py index 9ccc95f..ff41e0f 100644 --- a/bot_bottle/backend/smolmachines/launch.py +++ b/bot_bottle/backend/smolmachines/launch.py @@ -183,7 +183,7 @@ def launch( # Stamp the URLs onto the plan + guest_env. provision_git # and provision_supervise read the plan fields; the agent - # reads guest_env on every exec_claude. + # reads guest_env on every exec_agent. # # NO_PROXY has to include the per-bottle loopback alias — # otherwise claude's HTTPS_PROXY catches direct calls to diff --git a/bot_bottle/backend/smolmachines/pty_resize.py b/bot_bottle/backend/smolmachines/pty_resize.py index cb81311..311836b 100644 --- a/bot_bottle/backend/smolmachines/pty_resize.py +++ b/bot_bottle/backend/smolmachines/pty_resize.py @@ -24,7 +24,7 @@ process that: extra signalling. 3. Waits on the child and exits with its returncode. -The dashboard's tmux pane respawn calls `bottle.claude_argv` +The dashboard's tmux pane respawn calls `bottle.agent_argv` which now prepends `[sys.executable, -m, ..., , --, ...]` to the smolvm argv. Foreground handoff (curses endwin → subprocess.run) goes through the same path so behavior is diff --git a/bot_bottle/cli/dashboard.py b/bot_bottle/cli/dashboard.py index 9c1357e..1b45ff9 100644 --- a/bot_bottle/cli/dashboard.py +++ b/bot_bottle/cli/dashboard.py @@ -74,8 +74,8 @@ from ..supervise import ( ) from ._common import PROG, USER_CWD from .start import ( - attach_claude, - capture_session_state, + attach_agent, + capture_claude_session_state, prepare_with_preflight, settle_state, ) @@ -650,7 +650,7 @@ def _bottle_for_slug( if slug in bottles: _cm, bottle, _identity = bottles[slug] return bottle, "" - # The container hosting the agent's claude process is named + # The container hosting the agent's agent process is named # `bot-bottle-` — set by the compose renderer # (no service suffix on the agent service, by design). container_name = f"bot-bottle-{slug}" @@ -705,7 +705,7 @@ def _stop_bottle_flow( # settle_state below. try: if getattr(bottle, "agent_provider_template", "claude") == "claude": - capture_session_state(identity, exit_code=0) + capture_claude_session_state(identity, exit_code=0) except BaseException: pass try: @@ -715,7 +715,7 @@ def _stop_bottle_flow( # Mirror the bringup path's stderr → right-pane routing. # Reuses any existing right pane (which is probably the - # agent's own claude session) via `_ensure_right_pane`; the + # agent's own agent session) via `_ensure_right_pane`; the # final buffered output stays visible after settle_state # removes the state dir (tail-F handles file removal). try: @@ -752,7 +752,7 @@ def _stop_bottle_flow( # pane of a two-pane window with the operator's currently-selected # agent in the right pane. First attach creates the right pane via # `tmux split-window`; subsequent attaches respawn that pane with -# the new agent's claude session. The dashboard remembers the +# the new agent's agent session. The dashboard remembers the # pane id + occupant slug in `tmux_state` so the same pane is # reused across attaches. @@ -763,14 +763,14 @@ def _in_tmux() -> bool: return bool(os.environ.get("TMUX")) -def _claude_runtime_args( - *, resume: bool, remote_control: bool = False, provider_template: str = "claude", +def _agent_runtime_args( + *, resume: bool, remote_control: bool = False, agent_provider_template: str = "claude", ) -> list[str]: - """The argv the dashboard hands to `bottle.claude_argv` - on every attach — matches what `attach_claude` builds for the + """The argv the dashboard hands to `bottle.agent_argv` + on every attach — matches what `attach_agent` builds for the foreground handoff so both surfaces produce the same claude invocation.""" - runtime = runtime_for(provider_template) + runtime = runtime_for(agent_provider_template) args = list(runtime.bypass_args) if remote_control: args.extend(runtime.remote_control_args) @@ -780,7 +780,7 @@ def _claude_runtime_args( def _build_resume_argv_with_fallback( - bottle, *, remote_control: bool = False, provider_template: str = "claude", + bottle, *, remote_control: bool = False, agent_provider_template: str = "claude", ) -> list[str]: """Build a backend-exec argv that runs `claude --continue` and falls back to plain `claude` if no prior session exists. @@ -795,44 +795,44 @@ def _build_resume_argv_with_fallback( the fallback only kicks in when --continue would have failed anyway. - Works across backends because `bottle.claude_argv` always + Works across backends because `bottle.agent_argv` always surfaces the `claude` token preceded by the backend's exec framing (docker: `docker exec -it `; smolmachines: `smolvm machine exec --name -- runuser -u node --`). Splitting at `claude` keeps the framing as the prefix and - wraps just the claude tail in `sh -c`.""" - if provider_template != "claude": - return bottle.claude_argv( - _claude_runtime_args( + wraps just the agent tail in `sh -c`.""" + if agent_provider_template != "claude": + return bottle.agent_argv( + _agent_runtime_args( resume=True, remote_control=remote_control, - provider_template=provider_template, + agent_provider_template=agent_provider_template, ) ) - base_args = _claude_runtime_args( + base_args = _agent_runtime_args( resume=False, remote_control=remote_control, - provider_template=provider_template, + agent_provider_template=agent_provider_template, ) - base_exec = bottle.claude_argv(base_args) - # Split exec-framing prefix from the claude-and-args tail so + base_exec = bottle.agent_argv(base_args) + # Split exec-framing prefix from the agent-and-args tail so # we can compose ` --continue || ` inside # `sh -c`. The provider command token is the marker. - command = getattr(bottle, "agent_command", runtime_for(provider_template).command) - claude_idx = base_exec.index(command) - prefix = base_exec[:claude_idx] - claude_cmd = " ".join(shlex.quote(a) for a in base_exec[claude_idx:]) + command = getattr(bottle, "agent_command", runtime_for(agent_provider_template).command) + agent_idx = base_exec.index(command) + prefix = base_exec[:agent_idx] + agent_cmd = " ".join(shlex.quote(a) for a in base_exec[agent_idx:]) resume_args = " ".join( - shlex.quote(a) for a in runtime_for(provider_template).resume_args + shlex.quote(a) for a in runtime_for(agent_provider_template).resume_args ) return [ *prefix, "sh", "-c", - f"{claude_cmd} {resume_args} || {claude_cmd}", + f"{agent_cmd} {resume_args} || {agent_cmd}", ] -def _build_split_pane_argv(claude_argv: list[str]) -> list[str]: +def _build_split_pane_argv(agent_argv: list[str]) -> list[str]: """Pure helper: wrap a backend-exec argv with `tmux split-window -h -P -F '#{pane_id}'`. The `-P -F` combo tells tmux to print the new pane's id on stdout so we can track it for later @@ -840,15 +840,15 @@ def _build_split_pane_argv(claude_argv: list[str]) -> list[str]: return [ "tmux", "split-window", "-h", "-P", "-F", "#{pane_id}", - *claude_argv, + *agent_argv, ] -def _build_respawn_pane_argv(pane_id: str, claude_argv: list[str]) -> list[str]: +def _build_respawn_pane_argv(pane_id: str, agent_argv: list[str]) -> list[str]: """Pure helper: wrap a backend-exec argv with `tmux respawn-pane -k -t `. `-k` kills the existing process in the pane before respawning.""" - return ["tmux", "respawn-pane", "-k", "-t", pane_id, *claude_argv] + return ["tmux", "respawn-pane", "-k", "-t", pane_id, *agent_argv] @contextlib.contextmanager @@ -952,7 +952,7 @@ def _route_op_to_right_pane( def _tmux_close_right_pane(tmux_state: dict) -> None: """Close the tracked right pane via `tmux kill-pane`. Clears both pane_id and slug in `tmux_state`. Used after the last - dashboard-owned agent is stopped — no claude session left + dashboard-owned agent is stopped — no agent session left to host, so the pane shouldn't linger.""" pane_id = tmux_state.get("pane_id") if pane_id and _tmux_pane_exists(pane_id): @@ -992,7 +992,7 @@ def _ensure_right_pane(tmux_state: dict, argv: list[str]) -> str | None: returns the pane id on success, None on failure. This is the single place where "respawn or create" lives — - used by `_attach_in_tmux` for claude sessions AND by + used by `_attach_in_tmux` for agent sessions AND by `_new_agent_flow` for the bringup-log tail. Without this, every new-agent start would pile up a fresh right pane instead of reusing the one already next to the dashboard.""" @@ -1037,18 +1037,18 @@ def _attach_via_handoff( `_attach_in_tmux` when tmux misbehaves).""" curses.endwin() try: - provider_template = getattr(bottle, "agent_provider_template", "claude") - exit_code = attach_claude( + agent_provider_template = getattr(bottle, "agent_provider_template", "claude") + exit_code = attach_agent( bottle, remote_control=False, resume=resume, - provider_template=provider_template, + agent_provider_template=agent_provider_template, ) except BaseException: stdscr.refresh() raise stdscr.refresh() - return f"[{slug}] claude session ended (exit {exit_code})" + return f"[{slug}] agent session ended (exit {exit_code})" def _attach_in_tmux( @@ -1067,28 +1067,28 @@ def _attach_in_tmux( explicit-stop hook). `focus_right_pane=True` runs `tmux select-pane` after the - respawn so the operator is dropped into claude immediately. + respawn so the operator is dropped into agent immediately. The Enter re-attach key passes this; passive paths (the auto-attach after a stop) leave it False so the operator stays in the dashboard pane.""" if resume: - provider_template = getattr(bottle, "agent_provider_template", "claude") + agent_provider_template = getattr(bottle, "agent_provider_template", "claude") # `--continue` exits non-zero when no prior session # exists (agent spun up but never typed at). Wrap with a # shell-level fallback so the pane lands in a fresh - # claude instead of crashing. - claude_argv = _build_resume_argv_with_fallback( - bottle, provider_template=provider_template, + # agent instead of crashing. + agent_argv = _build_resume_argv_with_fallback( + bottle, agent_provider_template=agent_provider_template, ) else: - provider_template = getattr(bottle, "agent_provider_template", "claude") - claude_argv = bottle.claude_argv( - _claude_runtime_args( + agent_provider_template = getattr(bottle, "agent_provider_template", "claude") + agent_argv = bottle.agent_argv( + _agent_runtime_args( resume=False, - provider_template=provider_template, + agent_provider_template=agent_provider_template, ), ) - pane_id = _ensure_right_pane(tmux_state, claude_argv) + pane_id = _ensure_right_pane(tmux_state, agent_argv) if pane_id is None: # tmux failed (missing binary, server died, size error). # One status-line failover to the curses handoff so the @@ -1121,7 +1121,7 @@ def _attach_to_bottle( tmux_state: dict | None = None, ) -> str: """Re-attach to a running bottle. Inside tmux (`$TMUX` set + - `tmux_state` provided) the claude session opens in the + `tmux_state` provided) the agent session opens in the right pane (created on first attach, respawned on subsequent). Outside tmux it's a curses-endwin handoff that blocks until the operator exits claude. Re-attach always uses @@ -1129,7 +1129,7 @@ def _attach_to_bottle( if _in_tmux() and tmux_state is not None: # Enter re-attach is an explicit "I want to interact with # this agent" signal — move tmux focus to the right pane - # so keypresses land in claude instead of the dashboard. + # so keypresses land in agent instead of the dashboard. return _attach_in_tmux( stdscr, bottle, slug, resume=True, tmux_state=tmux_state, @@ -1147,7 +1147,7 @@ def _new_agent_flow( ) -> str: """Open the picker, prepare + preflight (modal), launch (enter the context manager but DON'T close it), then route - the first claude session into the right pane (in-tmux) or + the first agent session into the right pane (in-tmux) or foreground handoff (otherwise). Returns a status-line message for the dashboard footer. The (cm, bottle) tuple lands in `bottles` keyed by slug; chunk 4 uses it for explicit stop.""" @@ -1235,20 +1235,20 @@ def _new_agent_flow( raise bottles[plan.slug] = (cm, bottle, identity) - # Foreground handoff: claude owns the terminal until exit, + # Foreground handoff: the agent owns the terminal until exit, # then we restore curses. try: - provider_template = getattr(plan, "agent_provider_template", "claude") - exit_code = attach_claude( + agent_provider_template = getattr(plan, "agent_provider_template", "claude") + exit_code = attach_agent( bottle, remote_control=False, - provider_template=provider_template, + agent_provider_template=agent_provider_template, ) - if provider_template == "claude": - capture_session_state(identity, exit_code) + if agent_provider_template == "claude": + capture_claude_session_state(identity, exit_code) finally: stdscr.refresh() - return f"[{plan.slug}] claude session ended (exit {exit_code})" + return f"[{plan.slug}] agent session ended (exit {exit_code})" finally: # stage_dir was the prepare scratch dir; after PRD 0018 # chunk 2 it holds nothing the running bottle needs. Reap @@ -1540,7 +1540,7 @@ def _main_loop(stdscr: "curses._CursesWindow") -> None: # PRD 0021 follow-up: after stop, slide focus # to the next agent in the list (the one that # filled the stopped row) and respawn the - # right pane with its claude session. If + # right pane with its agent session. If # nothing's left, close the right pane. pick = _pick_next_after_stop( agents, selected_agent, target.slug, diff --git a/bot_bottle/cli/start.py b/bot_bottle/cli/start.py index f19960c..8c1871b 100644 --- a/bot_bottle/cli/start.py +++ b/bot_bottle/cli/start.py @@ -4,7 +4,7 @@ session ends. The launch core is shared with `cli.py resume ` and (PRD 0020 chunk 1+) the dashboard's in-process start flow: see the -public helpers `prepare_with_preflight`, `attach_claude`, and the +public helpers `prepare_with_preflight`, `attach_agent`, and the private orchestrator `_launch_bottle`. """ @@ -113,12 +113,13 @@ def prepare_with_preflight( return plan, identity -def attach_claude( +def attach_agent( bottle: Bottle, *, remote_control: bool = False, resume: bool = False, - provider_template: str = "claude", + agent_provider_template: str = "claude", ) -> int: - """Run claude inside `bottle` as an interactive session. Blocks - until the session ends; returns the claude process's exit code. + """Run the selected provider CLI inside `bottle` as an + interactive session. Blocks until the session ends; returns the + agent process's exit code. `resume=True` adds `--continue` so claude picks up its most recent session non-interactively (no session-picker prompt) — @@ -130,25 +131,28 @@ def attach_claude( Used as the inner step of `./cli.py start` (one-shot) and by the dashboard, which calls it from inside a `curses.endwin → … → stdscr.refresh()` handoff so the curses surface gets out of the - terminal's way while claude has it.""" - runtime = runtime_for(provider_template) + terminal's way while the agent has it.""" + runtime = runtime_for(agent_provider_template) info( - f"attaching interactive {provider_template} session " + f"attaching interactive {agent_provider_template} session " "(Ctrl-D or 'exit' to leave; container will be removed)" ) - claude_args = list(runtime.bypass_args) + agent_args = list(runtime.bypass_args) if remote_control: - claude_args.extend(runtime.remote_control_args) + agent_args.extend(runtime.remote_control_args) if resume: - claude_args.extend(runtime.resume_args) - return bottle.exec_claude(claude_args, tty=True) + agent_args.extend(runtime.resume_args) + return bottle.exec_agent(agent_args, tty=True) -def capture_session_state(identity: str, exit_code: int) -> None: +def capture_claude_session_state(identity: str, exit_code: int) -> None: """Inside the launch context, while the container is still alive: snapshot the transcript and mark for preservation if claude crashed. Public for the dashboard's death-handling path (PRD 0020 open question 3).""" + # FIXME: this captures Claude-specific session state. A follow-up + # spike should explore freezing provider-neutral container state + # instead of relying on each agent's transcript layout. if not identity: return snapshot_transcript(identity) @@ -218,11 +222,11 @@ def _launch_bottle( backend = get_bottle_backend(backend_name) with backend.launch(plan) as bottle: - provider_template = getattr(plan, "agent_provider_template", "claude") - exit_code = attach_claude( + agent_provider_template = getattr(plan, "agent_provider_template", "claude") + exit_code = attach_agent( bottle, remote_control=remote_control, - provider_template=provider_template, + agent_provider_template=agent_provider_template, ) info( f"session ended (exit {exit_code}); " @@ -236,8 +240,8 @@ def _launch_bottle( # way. snapshot_transcript is best-effort so the # capability-block path's prior snapshot isn't clobbered # when the container is already gone. - if provider_template == "claude": - capture_session_state(identity, exit_code) + if agent_provider_template == "claude": + capture_claude_session_state(identity, exit_code) return 0 finally: # PRD 0018 chunk 2: prepare now writes the bottle's bind-mount diff --git a/bot_bottle/manifest.py b/bot_bottle/manifest.py index 0b04912..220dbe0 100644 --- a/bot_bottle/manifest.py +++ b/bot_bottle/manifest.py @@ -478,7 +478,7 @@ class EgressConfig: @classmethod def from_dict( - cls, bottle_name: str, raw: object, *, provider_template: str = "claude", + cls, bottle_name: str, raw: object, *, agent_provider_template: str = "claude", ) -> "EgressConfig": d = _as_json_object(raw, f"bottle '{bottle_name}' egress") routes_raw = d.get("routes") @@ -495,7 +495,7 @@ class EgressConfig: for i, entry in enumerate(routes_list) ) _validate_egress_routes( - bottle_name, routes, provider_template=provider_template, + bottle_name, routes, agent_provider_template=agent_provider_template, ) for k in d: if k != "routes": @@ -589,7 +589,7 @@ class Bottle: egress = ( EgressConfig.from_dict( name, d["egress"], - provider_template=agent_provider.template, + agent_provider_template=agent_provider.template, ) if "egress" in d else EgressConfig() @@ -887,7 +887,7 @@ def _validate_egress_routes( bottle_name: str, routes: tuple[EgressRoute, ...], *, - provider_template: str = "claude", + agent_provider_template: str = "claude", ) -> None: """Cross-validation for `bottle.egress.routes`: @@ -919,14 +919,14 @@ def _validate_egress_routes( f"routes with role {role!r} (hosts: {hosts}); this role drives a " f"single launch-step side effect — pick one." ) - allowed_roles = PROVIDER_EGRESS_ROLES[provider_template] + allowed_roles = PROVIDER_EGRESS_ROLES[agent_provider_template] for route in routes: for role in route.Role: if role not in allowed_roles: die( f"bottle '{bottle_name}' egress route for host " f"{route.Host!r} has role {role!r}, but provider " - f"{provider_template!r} only accepts roles " + f"{agent_provider_template!r} only accepts roles " f"{', '.join(sorted(allowed_roles)) or '(none)'}" ) @@ -1141,7 +1141,7 @@ def _merge_bottles( ) _validate_egress_routes( name, merged_egress.routes, - provider_template=merged_agent_provider.template, + agent_provider_template=merged_agent_provider.template, ) return Bottle( diff --git a/docs/prds/0003-bottle-backend-abstraction.md b/docs/prds/0003-bottle-backend-abstraction.md index 72e0b40..cca8d4d 100644 --- a/docs/prds/0003-bottle-backend-abstraction.md +++ b/docs/prds/0003-bottle-backend-abstraction.md @@ -62,7 +62,7 @@ The feature is **done** when all of the following ship: `Bottle`) plus a `bot_bottle/backend/docker/` subpackage containing the `DockerBottleBackend` implementation. - `DockerBottleBackend.launch(plan)` returns a context manager - yielding a `Bottle` handle exposing `exec_claude(argv, *, tty=True)`, + yielding a `Bottle` handle exposing `exec_agent(argv, *, tty=True)`, `cp_in(host, ctr)`, and teardown on context exit. - Every existing `subprocess.run(["docker", ...])` call in `cli/start.py`, `pipelock.py`, `network.py`, `ssh.py`, and @@ -205,7 +205,7 @@ and a Docker subpackage: - **`bot_bottle/cli/start.py`** — replace the inline docker orchestration with `backend = get_bottle_backend(); plan = backend.prepare(spec, stage_dir=...); with backend.launch(plan) as - bottle: bottle.exec_claude(...)`. The y/N preflight is rendered by + bottle: bottle.exec_agent(...)`. The y/N preflight is rendered by `plan.print(...)`. - **`bot_bottle/manifest.py`** — drop the `runtime` field from the Bottle dataclass and its validation. Existing manifests with diff --git a/docs/prds/0018-compose-per-instance.md b/docs/prds/0018-compose-per-instance.md index 7aa2a06..c1a555d 100644 --- a/docs/prds/0018-compose-per-instance.md +++ b/docs/prds/0018-compose-per-instance.md @@ -312,7 +312,7 @@ existing prefix scan as a fallback for one release. 2. **How does `claude` reach the agent's TTY?** Decided: keep today's `docker exec -it` model. Agent runs `sleep infinity` - under compose; `DockerBottle.exec_claude` runs + under compose; `DockerBottle.exec_agent` runs `docker exec -it bot-bottle- claude ...` exactly like today. Compose owns the lifecycle (so `compose logs` includes the agent's stdout, `compose down` tears it down), but the diff --git a/docs/prds/0020-start-and-attach-from-dashboard.md b/docs/prds/0020-start-and-attach-from-dashboard.md index e83e6e5..297fadd 100644 --- a/docs/prds/0020-start-and-attach-from-dashboard.md +++ b/docs/prds/0020-start-and-attach-from-dashboard.md @@ -154,7 +154,7 @@ Today's flow: ``` ./cli.py start agent └─ with backend.launch(plan) as bottle: ← bottle alive while inside `with` - bottle.exec_claude([...], tty=True) ← blocks until claude exits + bottle.exec_agent([...], tty=True) ← blocks until claude exits # context exits → compose down → state cleanup ``` @@ -171,7 +171,7 @@ The proposed dashboard-driven flow: # operator interacts via: curses.endwin() - bottle.exec_claude([...], tty=True) ← blocks; returns on Ctrl-D + bottle.exec_agent([...], tty=True) ← blocks; returns on Ctrl-D stdscr.refresh() # bottle is STILL ALIVE — only the claude process exited @@ -265,7 +265,7 @@ if modal proves fiddly. Same handoff pattern the new-agent flow uses. For an agent the dashboard started this session, the dashboard holds the `DockerBottle` handle in its `bottles` dict and calls -`bottle.exec_claude(...)`. For an agent it discovered via +`bottle.exec_agent(...)`. For an agent it discovered via `list_active_slugs` (previous-dashboard or external start), the dashboard synthesizes a one-shot `DockerBottle` from the slug — container name is `bot-bottle-`, no prompt @@ -304,17 +304,17 @@ acting surface, not a lifetime owner. Sized for one PR each. -1. **Refactor `_launch_bottle` so the launch + exec_claude +1. **Refactor `_launch_bottle` so the launch + exec_agent pieces are separable.** Today's `cli/start.py` runs both inside one function. Extract `prepare_with_preflight(spec, - *, render_preflight, prompt_yes)` and `attach_claude(bottle, + *, render_preflight, prompt_yes)` and `attach_agent(bottle, *, remote_control)`. The CLI's existing one-shot use binds them as before; the dashboard binds them with curses-aware render + prompt callables. No behavior change. 2. **Agent picker modal + new-agent flow.** New key `n` opens the picker; `prepare_with_preflight` runs against the selected agent; on Y, `backend.launch(plan)` enters the - dashboard's ExitStack; handoff invokes `attach_claude`. + 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 @@ -390,7 +390,7 @@ Sized for one PR each. - PRD 0019 — active-agents pane + selection model (the agents-pane row the re-attach + stop verbs hook into) - `docs/research/claude-code-pane-in-dashboard.md` — option 1 - (handoff) is what `attach_claude` implements here; options 2 + (handoff) is what `attach_agent` implements here; options 2 / 3 are out of scope for this PRD - `bot_bottle/cli/start.py:_launch_bottle` — the function chunk 1 extracts the prepare + attach pieces out of diff --git a/docs/prds/0021-dashboard-tmux-split-pane.md b/docs/prds/0021-dashboard-tmux-split-pane.md index fff40ab..e6c00ab 100644 --- a/docs/prds/0021-dashboard-tmux-split-pane.md +++ b/docs/prds/0021-dashboard-tmux-split-pane.md @@ -163,7 +163,7 @@ def _attach_in_tmux(bottle, slug, *, resume) -> str: The non-tmux path is unchanged from PRD 0020 — `_attach_via_ handoff` is what those two flows already do today (curses. -endwin → attach_claude → stdscr.refresh). +endwin → attach_agent → stdscr.refresh). ### Pane creation @@ -230,10 +230,10 @@ def _tmux_pane_exists(pane_id) -> bool: The tmux helpers need the full docker-exec argv for claude — specifically including the `--append-system-prompt-file ` -flag that `DockerBottle.exec_claude` appends today when the -bottle has a prompt path. Refactor: split `exec_claude` into a +flag that `DockerBottle.exec_agent` appends today when the +bottle has a prompt path. Refactor: split `exec_agent` into a pure `claude_docker_argv(args, *, tty)` that returns the argv -and a thin `exec_claude` that calls `subprocess.run` on it. +and a thin `exec_agent` that calls `subprocess.run` on it. Both the tmux path AND the existing foreground path use the same argv builder. @@ -272,7 +272,7 @@ Three failure modes worth handling: Sized small. 1. **`claude_docker_argv` refactor.** Pure-ish split of - `DockerBottle.exec_claude` so both foreground and tmux + `DockerBottle.exec_agent` so both foreground and tmux paths build on the same argv. No behavior change for the existing tests. 2. **tmux helpers + pane state.** Add `_in_tmux`, diff --git a/docs/prds/0022-sandbox-escape-integration-test.md b/docs/prds/0022-sandbox-escape-integration-test.md index c65d12d..6b92c79 100644 --- a/docs/prds/0022-sandbox-escape-integration-test.md +++ b/docs/prds/0022-sandbox-escape-integration-test.md @@ -176,7 +176,7 @@ bottle so the suite is ~15s wall-clock total instead of bottle: dev --- -(no prompt — exec_claude isn't called) +(no prompt — exec_agent isn't called) ``` ```yaml diff --git a/docs/prds/0023-smolmachines-backend.md b/docs/prds/0023-smolmachines-backend.md index 8ffc18c..6a66038 100644 --- a/docs/prds/0023-smolmachines-backend.md +++ b/docs/prds/0023-smolmachines-backend.md @@ -317,7 +317,7 @@ The feature is **done** when all of the following ship: bot_bottle/backend/smolmachines/ __init__.py re-exports SmolmachinesBottleBackend backend.py SmolmachinesBottleBackend façade - bottle.py SmolmachinesBottle (exec_claude / exec / cp_in / close) + bottle.py SmolmachinesBottle (exec_agent / exec / cp_in / close) bottle_plan.py SmolmachinesBottlePlan + .print() bottle_cleanup_plan.py SmolmachinesBottleCleanupPlan prepare.py resolve_plan(spec, stage_dir, ...) -> SmolmachinesBottlePlan @@ -436,7 +436,7 @@ Three changes vs. the Docker backend: layer enforces it. 3. Provisioning: CA install → prompt → skills → git → supervise config, each via `smolvm machine exec` / `smolvm machine cp`. -4. Yield a `SmolmachinesBottle` whose `exec_claude` / `exec` / +4. Yield a `SmolmachinesBottle` whose `exec_agent` / `exec` / `cp_in` all funnel through `smolvm machine exec` / `smolvm machine cp`. 5. Teardown: stop and delete the VM → stop + remove the bundle diff --git a/tests/integration/test_capability_apply.py b/tests/integration/test_capability_apply.py index 8841bd3..18ee8fa 100644 --- a/tests/integration/test_capability_apply.py +++ b/tests/integration/test_capability_apply.py @@ -3,7 +3,7 @@ container that mimics the agent's name + filesystem layout (PRD 0016). The real `cli.py start ` flow is too heavy for an integration test (it builds the agent image, brings up all the sidecars, attaches -an interactive claude session). Instead, this test stages the +an interactive agent session). Instead, this test stages the minimum the orchestrator interacts with: - A lightweight `alpine:latest sleep infinity` container named diff --git a/tests/unit/test_cli_start_settle.py b/tests/unit/test_cli_start_settle.py index 267698c..bee5de5 100644 --- a/tests/unit/test_cli_start_settle.py +++ b/tests/unit/test_cli_start_settle.py @@ -1,7 +1,7 @@ """Unit: cli/start.py session-end state capture (crash preservation). The launch-context machinery is covered by integration; this isolates -the post-exec_claude decision: snapshot transcript + mark for +the post-exec_agent decision: snapshot transcript + mark for preservation if non-zero exit, no-op for clean exit.""" import tempfile @@ -45,25 +45,25 @@ class TestCaptureSessionState(_FakeHomeMixin, unittest.TestCase): self._teardown_fake_home() def test_clean_exit_snapshots_but_does_not_mark(self): - start_mod.capture_session_state("dev-abc", exit_code=0) + start_mod.capture_claude_session_state("dev-abc", exit_code=0) self.assertEqual(["dev-abc"], self._snap_calls) self.assertFalse(bottle_state.is_preserved("dev-abc")) def test_crash_snapshots_and_marks(self): - start_mod.capture_session_state("dev-abc", exit_code=137) + start_mod.capture_claude_session_state("dev-abc", exit_code=137) self.assertEqual(["dev-abc"], self._snap_calls) self.assertTrue(bottle_state.is_preserved("dev-abc")) def test_ctrl_c_treated_as_crash(self): # SIGINT delivers exit 130; the operator may have Ctrl-C'd # because something went wrong, so we preserve. - start_mod.capture_session_state("dev-abc", exit_code=130) + start_mod.capture_claude_session_state("dev-abc", exit_code=130) self.assertTrue(bottle_state.is_preserved("dev-abc")) def test_empty_identity_is_noop(self): # Backends without an identity field shouldn't crash this # path (the _identity_from_plan helper falls back to ""). - start_mod.capture_session_state("", exit_code=137) + start_mod.capture_claude_session_state("", exit_code=137) self.assertEqual([], self._snap_calls) diff --git a/tests/unit/test_dashboard_active_agents.py b/tests/unit/test_dashboard_active_agents.py index d720bdf..e285044 100644 --- a/tests/unit/test_dashboard_active_agents.py +++ b/tests/unit/test_dashboard_active_agents.py @@ -369,24 +369,24 @@ class TestResumeArgvWithFallback(unittest.TestCase): class TestClaudeRuntimeArgs(unittest.TestCase): - """The argv passed to `bottle.claude_argv` on each + """The argv passed to `bottle.agent_argv` on each attach. Locked here so the tmux + foreground paths build - identical claude invocations.""" + identical agent invocations.""" def test_default_skip_permissions_only(self): self.assertEqual( ["--dangerously-skip-permissions"], - dashboard._claude_runtime_args(resume=False), + dashboard._agent_runtime_args(resume=False), ) def test_resume_appends_continue(self): self.assertEqual( ["--dangerously-skip-permissions", "--continue"], - dashboard._claude_runtime_args(resume=True), + dashboard._agent_runtime_args(resume=True), ) def test_remote_control(self): - args = dashboard._claude_runtime_args( + args = dashboard._agent_runtime_args( resume=False, remote_control=True, ) self.assertIn("--remote-control", args) diff --git a/tests/unit/test_docker_bottle.py b/tests/unit/test_docker_bottle.py index 93354f0..639c31b 100644 --- a/tests/unit/test_docker_bottle.py +++ b/tests/unit/test_docker_bottle.py @@ -1,6 +1,6 @@ """Unit: DockerBottle's argv builder (PRD 0021 chunk 1). -`claude_argv` is the pure helper that `exec_claude` and the +`agent_argv` is the pure helper that `exec_agent` and the PRD-0021 tmux helpers both build on. It encodes two non-trivial rules — the optional `--append-system-prompt-file` flag and the optional `-it` for TTY mode — that we lock down here so the tmux @@ -28,20 +28,20 @@ def _codex_bottle(prompt_path: str | None = None) -> DockerBottle: teardown=lambda: None, prompt_path_in_container=prompt_path, agent_command="codex", - agent_prompt_mode="codex_read_prompt_file", + agent_prompt_mode="read_prompt_file", ) class TestClaudeArgv(unittest.TestCase): def test_minimal_argv_no_prompt(self): - argv = _bottle().claude_argv([]) + argv = _bottle().agent_argv([]) self.assertEqual( ["docker", "exec", "-it", "bot-bottle-dev-abc", "claude"], argv, ) def test_appends_passed_args_after_claude(self): - argv = _bottle().claude_argv( + argv = _bottle().agent_argv( ["--dangerously-skip-permissions", "--continue"], ) self.assertEqual( @@ -51,7 +51,7 @@ class TestClaudeArgv(unittest.TestCase): ) def test_appends_prompt_file_flag_when_set(self): - argv = _bottle("/home/node/.bot-bottle-prompt.txt").claude_argv( + argv = _bottle("/home/node/.bot-bottle-prompt.txt").agent_argv( ["--dangerously-skip-permissions"], ) self.assertEqual( @@ -63,34 +63,34 @@ class TestClaudeArgv(unittest.TestCase): ) def test_no_prompt_flag_when_none(self): - argv = _bottle(None).claude_argv(["--continue"]) + argv = _bottle(None).agent_argv(["--continue"]) self.assertNotIn("--append-system-prompt-file", argv) def test_empty_prompt_string_is_treated_as_no_prompt(self): - # Matches the existing exec_claude behavior: falsy + # Matches the existing exec_agent behavior: falsy # prompt_path means "skip the flag." The synth path in # dashboard.py relies on this when metadata is missing. - argv = _bottle("").claude_argv(["--continue"]) + argv = _bottle("").agent_argv(["--continue"]) self.assertNotIn("--append-system-prompt-file", argv) def test_tty_false_drops_it_flag(self): - argv = _bottle().claude_argv([], tty=False) + argv = _bottle().agent_argv([], tty=False) self.assertEqual( ["docker", "exec", "bot-bottle-dev-abc", "claude"], argv, ) def test_caller_argv_not_mutated(self): - # `claude_argv` builds `full_argv` from a copy, so a + # `agent_argv` builds `full_argv` from a copy, so a # caller passing a long-lived list (e.g., the dashboard's - # _claude_args fixture) doesn't get extra flags appended to + # _agent_args fixture) doesn't get extra flags appended to # it on subsequent calls. original = ["--continue"] - _bottle("/x").claude_argv(original) + _bottle("/x").agent_argv(original) self.assertEqual(["--continue"], original) def test_codex_provider_uses_codex_command(self): - argv = _codex_bottle().claude_argv( + argv = _codex_bottle().agent_argv( ["--dangerously-bypass-approvals-and-sandbox"], ) self.assertEqual( @@ -100,7 +100,7 @@ class TestClaudeArgv(unittest.TestCase): ) def test_codex_provider_passes_prompt_reference_as_initial_prompt(self): - argv = _codex_bottle("/home/node/.bot-bottle-prompt.txt").claude_argv([]) + argv = _codex_bottle("/home/node/.bot-bottle-prompt.txt").agent_argv([]) self.assertEqual( ["docker", "exec", "-it", "bot-bottle-dev-abc", "codex", "Read and follow the instructions in " @@ -109,7 +109,7 @@ class TestClaudeArgv(unittest.TestCase): ) def test_codex_resume_does_not_append_initial_prompt(self): - argv = _codex_bottle("/home/node/.bot-bottle-prompt.txt").claude_argv( + argv = _codex_bottle("/home/node/.bot-bottle-prompt.txt").agent_argv( ["--dangerously-bypass-approvals-and-sandbox", "resume", "--last"], ) self.assertEqual( diff --git a/tests/unit/test_smolmachines_bottle.py b/tests/unit/test_smolmachines_bottle.py index f773454..2c42df4 100644 --- a/tests/unit/test_smolmachines_bottle.py +++ b/tests/unit/test_smolmachines_bottle.py @@ -1,9 +1,9 @@ -"""Unit: SmolmachinesBottle's `claude_argv` builder. +"""Unit: SmolmachinesBottle's `agent_argv` builder. -The dashboard's tmux pane-respawn path calls `bottle.claude_argv` +The dashboard's tmux pane-respawn path calls `bottle.agent_argv` directly (it spawns claude inside a tmux pane rather than as a child of the current process), so the argv shape is the -non-trivial part. `exec_claude` is a thin wrapper around the same +non-trivial part. `exec_agent` is a thin wrapper around the same builder + `subprocess.run`; we lock the shape here. The TTY-mode argv is wrapped in the pty_resize helper (issue #82 @@ -40,7 +40,7 @@ class TestClaudeArgvWrapped(unittest.TestCase): """TTY-mode argv: pty_resize wrapper + inner smolvm exec.""" def test_pty_resize_wrapper_prefix(self): - argv = _bottle().claude_argv([]) + argv = _bottle().agent_argv([]) # Absolute script path (not `-m `) so the tmux # pane's cwd doesn't matter — see the `_PTY_RESIZE_SCRIPT` # docstring in bottle.py. @@ -53,7 +53,7 @@ class TestClaudeArgvWrapped(unittest.TestCase): ) def test_minimal_inner_argv_no_prompt(self): - argv = _unwrap(_bottle().claude_argv([])) + argv = _unwrap(_bottle().agent_argv([])) self.assertEqual( [ "smolvm", "machine", "exec", "--name", @@ -69,7 +69,7 @@ class TestClaudeArgvWrapped(unittest.TestCase): ) def test_appends_passed_args_after_claude(self): - argv = _unwrap(_bottle().claude_argv( + argv = _unwrap(_bottle().agent_argv( ["--dangerously-skip-permissions", "--continue"], )) self.assertEqual( @@ -79,7 +79,7 @@ class TestClaudeArgvWrapped(unittest.TestCase): def test_appends_prompt_file_flag_when_set(self): argv = _unwrap( - _bottle("/home/node/.bot-bottle-prompt.txt").claude_argv( + _bottle("/home/node/.bot-bottle-prompt.txt").agent_argv( ["--dangerously-skip-permissions"], ) ) @@ -94,11 +94,11 @@ class TestClaudeArgvWrapped(unittest.TestCase): ) def test_no_prompt_flag_when_none(self): - argv = _bottle(None).claude_argv(["--continue"]) + argv = _bottle(None).agent_argv(["--continue"]) self.assertNotIn("--append-system-prompt-file", argv) def test_empty_prompt_string_is_treated_as_no_prompt(self): - argv = _bottle("").claude_argv(["--continue"]) + argv = _bottle("").agent_argv(["--continue"]) self.assertNotIn("--append-system-prompt-file", argv) def test_guest_env_forwarded_as_e_flags(self): @@ -106,7 +106,7 @@ class TestClaudeArgvWrapped(unittest.TestCase): None, HTTPS_PROXY="http://127.0.0.1:1234", NO_PROXY="localhost", - ).claude_argv([])) + ).agent_argv([])) self.assertIn("-e", argv) self.assertIn("HTTPS_PROXY=http://127.0.0.1:1234", argv) self.assertIn("NO_PROXY=localhost", argv) @@ -116,11 +116,11 @@ class TestClaudeArgvWrapped(unittest.TestCase): # the `claude` token to split exec-framing from the claude # tail. `runuser -u node --` must sit on the prefix side so # the shell wrap inherits the UID switch. - argv = _bottle().claude_argv([]) - claude_idx = argv.index("claude") + argv = _bottle().agent_argv([]) + agent_idx = argv.index("claude") self.assertEqual( ["runuser", "-u", "node", "--"], - argv[claude_idx - 4:claude_idx], + argv[agent_idx - 4:agent_idx], ) @@ -129,12 +129,12 @@ class TestClaudeArgvNoTTY(unittest.TestCase): PTY whose SIGWINCH we'd need to bridge.""" def test_no_wrapper_when_tty_false(self): - argv = _bottle().claude_argv([], tty=False) + argv = _bottle().agent_argv([], tty=False) self.assertEqual("smolvm", argv[0]) self.assertFalse(any("pty_resize" in a for a in argv)) def test_tty_false_drops_it_flags(self): - argv = _bottle().claude_argv([], tty=False) + argv = _bottle().agent_argv([], tty=False) self.assertNotIn("-i", argv) self.assertNotIn("-t", argv)