# 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.
