# PRD 0044: Print Parity Across Backends - **Status:** Active - **Author:** didericis-claude - **Created:** 2026-06-02 - **Issue:** #96 ## Summary Hoist `git_gate_plan`, `egress_plan`, `agent_provision`, and `supervise_plan` from the concrete `BottlePlan` subclasses up to `BottlePlan`, and implement `print` concretely there. This eliminates the two per-backend output divergences and ensures any future backend gets correct preflight rendering for free. ## Problem `BottlePlan.print` is `@abstractmethod`, so each backend provides its own implementation. The two current implementations have drifted: | Field | Docker | smolmachines | |---|---|---| | git gate lines | `upstream_host:upstream_port` from resolved `git_gate_plan.upstreams` | `Name → Upstream` from manifest `bottle.git` | | egress lines | `host [auth:scheme]` | `host` only (auth dropped) | The smolmachines docstring says "same shape as the Docker backend's so operators see one format across backends" — that intent is real but nothing enforces it. The env_names divergence previously noted in this issue was resolved by PRD 0038 (smolmachines env contract): `resolved.forwarded` is now merged into `agent_provision.guest_env` at prepare time on both backends, so displayed env names are equivalent. ## Goals / Success Criteria - `BottlePlan` carries `git_gate_plan`, `egress_plan`, `agent_provision`, and `supervise_plan` as concrete fields; subclasses no longer declare them independently. - `BottlePlan.print` is a concrete method; subclasses have no `print` implementation of their own. - Both backends render git gate lines as `name → upstream_host:upstream_port` (using `git_gate_plan.upstreams`), not the manifest-level URL. - Both backends render egress lines as `host [auth:scheme]` (dropping the annotation only when `auth_scheme` is empty). - Unit tests assert the unified output for both backends from a single shared test helper. ## Non-goals - No changes to the Docker or smolmachines launch, prepare, or cleanup paths. - No changes to how env values are resolved or injected (that is PRD 0038). - No changes to the manifest schema or `GitEntry`. - No new CLI flags or dashboard changes. ## Scope In scope: - `bot_bottle/backend/__init__.py` — add `git_gate_plan`, `egress_plan`, `agent_provision`, and `supervise_plan` fields to `BottlePlan`; replace `@abstractmethod print` with a concrete implementation. - `bot_bottle/backend/docker/bottle_plan.py` — remove the four hoisted fields and the `print` method. - `bot_bottle/backend/smolmachines/bottle_plan.py` — remove the four hoisted fields and the `print` method. - `tests/unit/` — add or update tests asserting unified preflight output; a shared helper can build a minimal plan fixture for each backend and assert the same lines appear. Out of scope: - Changes to `bot_bottle/backend/print_util.py` beyond what the new `print` implementation requires. - Changes to `BottleCleanupPlan.print` or any other print method. - Integration tests that launch a real bottle. ## Design Move the four fields that both concrete subclasses already declare — `git_gate_plan: GitGatePlan`, `egress_plan: EgressPlan`, `agent_provision: AgentProvisionPlan`, `supervise_plan: SupervisePlan | None` — up to `BottlePlan`. Both backends' `prepare` paths already produce these with the same types, so no prepare-time changes are needed. Replace the `@abstractmethod` `print` with a concrete implementation on `BottlePlan` that: 1. Builds `env_names` from `bottle.env.keys() | agent_provision.guest_env.keys()` filtered through `agent_provision.hidden_env_names`. 2. Builds git gate lines from `git_gate_plan.upstreams` as `f"{u.name} → {u.upstream_host}:{u.upstream_port}"`. 3. Builds egress lines from `egress_plan.routes` as `f"{r.host} [auth:{r.auth_scheme}]"` when `r.auth_scheme` is non-empty, else `r.host`. 4. Renders the standard two-column preflight block (leading blank line, agent, provider, env, skills, bottle, git identity, git gate, egress, trailing blank line). Docker's `forwarded_env` keys are already merged into `agent_provision.guest_env` via the `agent_provision_plan` builder, so no special handling is needed for env_names. ## Testing Strategy - Add a shared fixture builder (e.g. `make_plan(backend)`) in a new or existing unit test module that constructs a minimal `DockerBottlePlan` and `SmolmachinesBottlePlan` from the same spec and plan fields. - Assert that `plan.print(remote_control=False)` produces identical git gate and egress lines for both backends given the same `git_gate_plan` and `egress_plan`. - Test the `auth_scheme` annotation: present when non-empty, absent otherwise. - Test git gate rendering: `name → host:port` format. Run: - `python3 -m unittest discover -s tests/unit` ## Open Questions None.