Fixes the 30s redeploy loop and adds graceful deployment upgrades — same root:
how the agent identifies a running container vs. its desired spec.
Redeploy-loop fix (container identity by id/name, not spec compare):
- `ensure_service_running` is now liveness-only — a running container is a NOOP,
no spec comparison (podman ps can't read env/volumes, which made the old
matches_spec recreate env-bearing services every tick). The periodic tick
adopts-or-restarts by name and never recreates a healthy container.
- Spec changes are detected by the reconciler's existing byte-compare of the
desired-state JSON, not by the runtime. The agent records each container's id
(from start_service) in DeploymentState.container_ids.
- Deleted the matches_spec FIXME and its always-drift hack.
Graceful roll-forward upgrade:
- PodmanV0Score.lifecycle: Option<LifecyclePolicy> (SIGTERM, 30s grace, SIGKILL
fallback). stop_signal baked into the container at create; grace drives
`podman stop --time`.
- On a changed score the reconciler stops the exact old container by its
recorded id (graceful), then starts the new one; dropped services are stopped.
Roll-forward only — a failure reports Phase::Failed, never reverts.
New ContainerRuntime methods: start_service (→id), container_status, stop_service
(graceful). Reconciler is now generic over `dyn ContainerRuntime`, unit-tested
against a FakeRuntime: tick-idempotency (loop killed), graceful-replace-by-id,
roll-forward-no-revert, unchanged-noop.
Architecture + flagged VM v1->v2->v3 e2e in
ROADMAP/fleet_platform/ch5-graceful-upgrade-status.md.