# L4D2 Host Smoke Test Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Validate the implemented `l4d2host` library and `l4d2ctl` CLI on disposable host `ckn@10.0.4.128` with explicit user approval before every server-touching phase.

**Architecture:** This is a gated smoke-test runbook, not a feature implementation. Each task executes one bounded phase on the target server, captures command evidence, stops, and asks for approval before the next phase. The host library remains unchanged unless the smoke test identifies a defect that requires a separate fix plan.

**Tech Stack:** SSH, sudo, Python virtualenv/pip, Typer CLI entry point, SteamCMD, fuse-overlayfs/fuse3, systemd user services, journald, `/opt/l4d2` runtime paths.

---

## Source Design

- `docs/superpowers/specs/2026-05-05-l4d2-host-smoke-test-design.md`

## Gating Rule

Before running any task below, ask the user for explicit approval. After running a task, report evidence and stop. Do not continue to the next task until the user approves it.

Use this approval prompt before each task, replacing `N` and the title with the concrete task number and task title from this plan:

```text
Approve Task 1: read-only inspection on ckn@10.0.4.128?
```

If any command fails, stop immediately and report:

```text
Failed command: the exact command that failed
Exit/status: the observed exit code, signal, or SSH failure status
Relevant stdout/stderr: the shortest excerpt that explains the failure
Category: environment issue | host-lib bug | packaging/deploy issue | unclear
Recommended next action: one concrete next step based on the observed failure
```

Do not perform cleanup after a failure unless the user approves cleanup.

## Files And Runtime Locations

- Read: `docs/superpowers/specs/2026-05-05-l4d2-host-smoke-test-design.md`
- Read: `l4d2host/pyproject.toml`
- Read: `l4d2host/**`
- Remote create: `~/l4d2host-smoke/`
- Remote create: `~/l4d2host-smoke/.venv/`
- Remote create: `~/l4d2host-smoke/specs/smoke.yaml`
- Remote create: `~/l4d2host-smoke/logs/`
- Remote create/modify: `/opt/l4d2/`
- Remote create/modify: `/opt/l4d2/installation/`
- Remote create/delete: `/opt/l4d2/instances/smoke/`
- Remote create/delete: `/opt/l4d2/runtime/smoke/`
- Remote create/modify: `/home/ckn/.config/systemd/user/l4d2@.service`
- Local temporary create: `/var/folders/h4/nnvk2kxs2sv7nr32kmb_4dm40000gn/T/opencode/l4d2-host-lib-smoke.tar.gz`

### Task 1: Read-Only Server Inspection

**Files:**
- Read remote host state only.
- Do not create, modify, mount, install, start, stop, or delete anything.

- [ ] **Step 1: Ask for approval**

Ask:

```text
Approve Task 1: read-only inspection on ckn@10.0.4.128?
```

Expected: user explicitly approves before commands are run.

- [ ] **Step 2: Verify SSH identity and sudo availability without changing state**

Run:

```bash
ssh ckn@10.0.4.128 'set -eu; printf "user="; whoami; printf "host="; hostname; sudo -n true && printf "sudo=noninteractive\n" || printf "sudo=requires-password-or-unavailable\n"'
```

Expected: output includes `user=ckn`, a hostname, and either `sudo=noninteractive` or `sudo=requires-password-or-unavailable`.

- [ ] **Step 3: Inspect OS, package manager, Python, and runtime commands**

Run:

```bash
ssh ckn@10.0.4.128 'set -u; printf "os_release=\n"; if [ -r /etc/os-release ]; then sed -n "1,12p" /etc/os-release; else uname -a; fi; printf "\npackage_managers=\n"; for c in apt-get dnf yum pacman zypper; do command -v "$c" || true; done; printf "\npython=\n"; command -v python3 || true; python3 --version 2>&1 || true; printf "\nruntime_commands=\n"; for c in steamcmd fuse-overlayfs fusermount3 systemctl journalctl loginctl; do printf "%s=" "$c"; command -v "$c" || true; done'
```

Expected: reports OS details, any available package manager, Python version if installed, and presence/absence of required runtime commands.

- [ ] **Step 4: Inspect systemd user state and `/opt/l4d2` without changing it**

Run:

```bash
ssh ckn@10.0.4.128 'set -u; printf "uid="; id -u; printf "groups="; id -nG; printf "\nlinger=\n"; loginctl show-user "$(whoami)" -p Linger 2>/dev/null || true; printf "\nsystemd_user=\n"; XDG_RUNTIME_DIR="/run/user/$(id -u)" systemctl --user is-system-running 2>&1 || true; printf "\nopt_l4d2=\n"; if [ -e /opt/l4d2 ]; then ls -ld /opt/l4d2 /opt/l4d2/* 2>/dev/null || true; else printf "/opt/l4d2 missing\n"; fi; printf "\nmounts=\n"; mount | grep /opt/l4d2 || true'
```

Expected: reports UID/groups, lingering state, systemd user status, `/opt/l4d2` state, and existing `/opt/l4d2` mounts if any.

- [ ] **Step 5: Report findings and stop**

Report:

```text
Task 1 evidence:
- SSH identity: report the observed user and hostname
- sudo availability: report whether noninteractive sudo worked
- OS/package manager: report OS family and detected package manager
- Python: report Python path and version, or absence
- runtime commands present/missing: report steamcmd, fuse-overlayfs, fusermount3, systemctl, journalctl, loginctl
- systemd user state: report lingering and systemctl --user result
- /opt/l4d2 state: report whether it exists, ownership, and any mounts

Approve Task 2: server preparation on ckn@10.0.4.128?
```

Expected: no server state has been changed.

### Task 2: Server Preparation

**Files:**
- Remote create/modify: `/opt/l4d2/`
- Remote modify if required: system packages
- Remote modify if required: user lingering for `ckn`

- [ ] **Step 1: Ask for approval**

Ask:

```text
Approve Task 2: server preparation on ckn@10.0.4.128?
```

Expected: user explicitly approves before commands are run.

- [ ] **Step 2: Install baseline packages using the detected package manager**

Run exactly one of these command blocks based on Task 1 package-manager output.

For Debian/Ubuntu with `apt-get`:

```bash
ssh ckn@10.0.4.128 'set -eu; sudo apt-get update; sudo DEBIAN_FRONTEND=noninteractive apt-get install -y python3 python3-venv python3-pip curl ca-certificates tar gzip fuse-overlayfs fuse3'
```

Expected: command exits 0 and packages are installed or already current.

For Fedora/RHEL-like systems with `dnf`:

```bash
ssh ckn@10.0.4.128 'set -eu; sudo dnf install -y python3 python3-pip curl ca-certificates tar gzip fuse-overlayfs fuse3'
```

Expected: command exits 0 and packages are installed or already current.

For RHEL-like systems with `yum` and no `dnf`:

```bash
ssh ckn@10.0.4.128 'set -eu; sudo yum install -y python3 python3-pip curl ca-certificates tar gzip fuse-overlayfs fuse3'
```

Expected: command exits 0 and packages are installed or already current.

For Arch with `pacman`:

```bash
ssh ckn@10.0.4.128 'set -eu; sudo pacman -Sy --noconfirm python python-pip curl ca-certificates tar gzip fuse-overlayfs fuse3'
```

Expected: command exits 0 and packages are installed or already current.

- [ ] **Step 3: Install SteamCMD from Valve tarball**

Run:

```bash
ssh ckn@10.0.4.128 'set -eu; sudo mkdir -p /opt/steamcmd; curl -fsSL https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz -o /tmp/steamcmd_linux.tar.gz; sudo tar -xzf /tmp/steamcmd_linux.tar.gz -C /opt/steamcmd; sudo ln -sf /opt/steamcmd/steamcmd.sh /usr/local/bin/steamcmd; steamcmd +quit'
```

Expected: `steamcmd +quit` exits 0 after bootstrapping or verifying SteamCMD.

- [ ] **Step 4: Prepare `/opt/l4d2` and systemd user prerequisites**

Run:

```bash
ssh ckn@10.0.4.128 'set -eu; sudo mkdir -p /opt/l4d2/installation /opt/l4d2/overlays /opt/l4d2/instances /opt/l4d2/runtime; sudo chown -R ckn:ckn /opt/l4d2; sudo loginctl enable-linger ckn; mkdir -p "$HOME/.config/systemd/user"; XDG_RUNTIME_DIR="/run/user/$(id -u)" systemctl --user daemon-reload || true'
```

Expected: `/opt/l4d2` exists and is owned by `ckn`; lingering is enabled; systemd user daemon reload either succeeds or reports a diagnosable user-manager issue.

- [ ] **Step 5: Verify prepared commands and writable runtime paths**

Run:

```bash
ssh ckn@10.0.4.128 'set -eu; command -v python3; python3 --version; command -v steamcmd; command -v fuse-overlayfs; command -v fusermount3; test -w /opt/l4d2; ls -ld /opt/l4d2 /opt/l4d2/installation /opt/l4d2/overlays /opt/l4d2/instances /opt/l4d2/runtime; XDG_RUNTIME_DIR="/run/user/$(id -u)" systemctl --user is-system-running 2>&1 || true'
```

Expected: all `command -v` checks print paths, `/opt/l4d2` is writable, and systemd user state is visible.

- [ ] **Step 6: Report findings and stop**

Report:

```text
Task 2 evidence:
- package installation: report package manager used and install command status
- steamcmd bootstrap: report steamcmd path and bootstrap command status
- /opt/l4d2 ownership: report owner/group and writability for ckn
- systemd user/linger state: report loginctl linger value and systemctl --user result

Approve Task 3: deploy current host lib on ckn@10.0.4.128?
```

Expected: server is prepared for host-lib deployment.

### Task 3: Deploy Current Host Lib

**Files:**
- Local create: `/var/folders/h4/nnvk2kxs2sv7nr32kmb_4dm40000gn/T/opencode/l4d2-host-lib-smoke.tar.gz`
- Remote create/modify: `~/l4d2host-smoke/`
- Remote create/modify: `~/l4d2host-smoke/.venv/`

- [ ] **Step 1: Ask for approval**

Ask:

```text
Approve Task 3: deploy current host lib on ckn@10.0.4.128?
```

Expected: user explicitly approves before commands are run.

- [ ] **Step 2: Create local source archive from current host-lib component**

Run from repository root:

```bash
tar --exclude='*.pyc' --exclude='__pycache__' --exclude='.pytest_cache' --exclude='*.egg-info' -C l4d2host -czf /var/folders/h4/nnvk2kxs2sv7nr32kmb_4dm40000gn/T/opencode/l4d2-host-lib-smoke.tar.gz .
```

Expected: command exits 0 and archive exists at `/var/folders/h4/nnvk2kxs2sv7nr32kmb_4dm40000gn/T/opencode/l4d2-host-lib-smoke.tar.gz`.

- [ ] **Step 3: Copy source archive to remote host and unpack it**

Run:

```bash
ssh ckn@10.0.4.128 'set -eu; rm -rf "$HOME/l4d2host-smoke"; mkdir -p "$HOME/l4d2host-smoke/src" "$HOME/l4d2host-smoke/logs" "$HOME/l4d2host-smoke/specs"'
```

Expected: remote smoke workspace is recreated.

Run:

```bash
scp /var/folders/h4/nnvk2kxs2sv7nr32kmb_4dm40000gn/T/opencode/l4d2-host-lib-smoke.tar.gz ckn@10.0.4.128:~/l4d2host-smoke/l4d2-host-lib-smoke.tar.gz
```

Expected: archive copies successfully.

Run:

```bash
ssh ckn@10.0.4.128 'set -eu; tar -xzf "$HOME/l4d2host-smoke/l4d2-host-lib-smoke.tar.gz" -C "$HOME/l4d2host-smoke/src"; test -f "$HOME/l4d2host-smoke/src/pyproject.toml"; test -f "$HOME/l4d2host-smoke/src/cli.py"'
```

Expected: source tree unpacks and expected files exist.

- [ ] **Step 4: Install host lib into remote virtualenv**

Run:

```bash
ssh ckn@10.0.4.128 'set -eu; python3 -m venv "$HOME/l4d2host-smoke/.venv"; "$HOME/l4d2host-smoke/.venv/bin/python" -m pip install --upgrade pip; "$HOME/l4d2host-smoke/.venv/bin/python" -m pip install -e "$HOME/l4d2host-smoke/src"'
```

Expected: pip exits 0 and installs `l4d2host` in editable mode.

- [ ] **Step 5: Verify CLI command surface**

Run:

```bash
ssh ckn@10.0.4.128 'set -eu; "$HOME/l4d2host-smoke/.venv/bin/l4d2ctl" --help | tee "$HOME/l4d2host-smoke/logs/l4d2ctl-help.log"; grep -E "install|initialize|start|stop|delete" "$HOME/l4d2host-smoke/logs/l4d2ctl-help.log"'
```

Expected: help output includes `install`, `initialize`, `start`, `stop`, and `delete`.

- [ ] **Step 6: Report findings and stop**

Report:

```text
Task 3 evidence:
- archive creation/copy: report local archive path and remote unpack path
- venv/pip install: report virtualenv path and pip install status
- l4d2ctl command surface: report write commands plus status/log read commands found in help output

Approve Task 4: run l4d2ctl install on ckn@10.0.4.128?
```

Expected: host lib is installed on the server but no L4D2 server files have been downloaded by this task.

### Task 4: Run `l4d2ctl install`

**Files:**
- Remote create/modify: `/opt/l4d2/installation/`
- Remote create: `~/l4d2host-smoke/logs/install.log`

- [ ] **Step 1: Ask for approval**

Ask:

```text
Approve Task 4: run l4d2ctl install on ckn@10.0.4.128?
```

Expected: user explicitly approves before commands are run.

- [ ] **Step 2: Run install command and capture output**

Run with a long timeout when executing:

```bash
ssh ckn@10.0.4.128 'bash -lc "set -o pipefail; \"$HOME/l4d2host-smoke/.venv/bin/l4d2ctl\" install 2>&1 | tee \"$HOME/l4d2host-smoke/logs/install.log\""'
```

Expected: command exits 0 after SteamCMD completes Windows and Linux platform app updates for app `222860`.

- [ ] **Step 3: Inspect installation output paths**

Run:

```bash
ssh ckn@10.0.4.128 'set -eu; test -d /opt/l4d2/installation; find /opt/l4d2/installation -maxdepth 3 \( -name srcds_run -o -name left4dead2 \) -print; du -sh /opt/l4d2/installation; tail -n 40 "$HOME/l4d2host-smoke/logs/install.log"'
```

Expected: output includes `/opt/l4d2/installation/srcds_run`, a `left4dead2` path, install directory size, and recent SteamCMD log lines.

- [ ] **Step 4: Report findings and stop**

Report:

```text
Task 4 evidence:
- l4d2ctl install exit: report exit status and SteamCMD completion status
- installed paths: report srcds_run and left4dead2 paths found under /opt/l4d2/installation
- install log excerpt: report the last relevant SteamCMD lines

Approve Task 5: run smoke instance lifecycle on ckn@10.0.4.128?
```

Expected: L4D2 dedicated server files exist under `/opt/l4d2/installation`.

### Task 5: Run Instance Lifecycle Smoke Test

**Files:**
- Remote create: `~/l4d2host-smoke/specs/smoke.yaml`
- Remote create/modify/delete: `/opt/l4d2/instances/smoke/`
- Remote create/modify/delete: `/opt/l4d2/runtime/smoke/`
- Remote create/modify: `/home/ckn/.config/systemd/user/l4d2@.service`
- Remote create: `~/l4d2host-smoke/logs/lifecycle.log`

- [ ] **Step 1: Ask for approval**

Ask:

```text
Approve Task 5: run smoke instance lifecycle on ckn@10.0.4.128?
```

Expected: user explicitly approves before commands are run.

- [ ] **Step 2: Create minimal smoke spec**

Run:

```bash
ssh ckn@10.0.4.128 'set -eu; mkdir -p "$HOME/l4d2host-smoke/specs"; printf "%s\n" "port: 27015" "arguments:" "  - -insecure" "  - +map" "  - c5m1_waterfront" "config:" "  - hostname left4me-smoke" > "$HOME/l4d2host-smoke/specs/smoke.yaml"; sed -n "1,20p" "$HOME/l4d2host-smoke/specs/smoke.yaml"'
```

Expected: spec file shows port `27015`, no overlays, three arguments, and one config line.

- [ ] **Step 3: Initialize the smoke instance**

Run:

```bash
ssh ckn@10.0.4.128 'bash -lc "set -o pipefail; \"$HOME/l4d2host-smoke/.venv/bin/l4d2ctl\" initialize smoke -f \"$HOME/l4d2host-smoke/specs/smoke.yaml\" 2>&1 | tee \"$HOME/l4d2host-smoke/logs/initialize.log\""'
```

Expected: command exits 0.

Run:

```bash
ssh ckn@10.0.4.128 'set -eu; test -f /opt/l4d2/instances/smoke/instance.env; test -f /opt/l4d2/instances/smoke/server.cfg; test -d /opt/l4d2/runtime/smoke/upper; test -d /opt/l4d2/runtime/smoke/work; test -d /opt/l4d2/runtime/smoke/merged; sed -n "1,20p" /opt/l4d2/instances/smoke/instance.env; sed -n "1,20p" /opt/l4d2/instances/smoke/server.cfg; test -f "$HOME/.config/systemd/user/l4d2@.service"'
```

Expected: instance files and runtime directories exist; `instance.env` contains `L4D2_PORT=27015` and `L4D2_LOWERDIRS=/opt/l4d2/installation`.

- [ ] **Step 4: Start the smoke instance**

Run:

```bash
ssh ckn@10.0.4.128 'bash -lc "set -o pipefail; XDG_RUNTIME_DIR=/run/user/$(id -u) \"$HOME/l4d2host-smoke/.venv/bin/l4d2ctl\" start smoke 2>&1 | tee \"$HOME/l4d2host-smoke/logs/start.log\""'
```

Expected: command exits 0 or fails with actionable stdout/stderr that identifies an environment or host-lib issue.

- [ ] **Step 5: Inspect service, mount, status API, and logs API**

Run:

```bash
ssh ckn@10.0.4.128 'set -u; XDG_RUNTIME_DIR="/run/user/$(id -u)" systemctl --user status l4d2@smoke.service --no-pager 2>&1 | sed -n "1,80p"; printf "\nmount_state=\n"; mount | grep "/opt/l4d2/runtime/smoke/merged" || true; printf "\nstatus_api=\n"; "$HOME/l4d2host-smoke/.venv/bin/python" -c "from l4d2host.status import get_instance_status; print(get_instance_status(\"smoke\"))"; printf "\nlogs_api=\n"; "$HOME/l4d2host-smoke/.venv/bin/python" -c "from itertools import islice; from l4d2host.logs import stream_instance_logs; [print(line) for line in islice(stream_instance_logs(\"smoke\", lines=50, follow=False), 20)]"'
```

Expected: service status is visible, mount state is reported, status API prints an `InstanceStatus`, and logs API prints up to 20 recent journal lines.

- [ ] **Step 6: Stop the smoke instance**

Run:

```bash
ssh ckn@10.0.4.128 'bash -lc "set -o pipefail; XDG_RUNTIME_DIR=/run/user/$(id -u) \"$HOME/l4d2host-smoke/.venv/bin/l4d2ctl\" stop smoke 2>&1 | tee \"$HOME/l4d2host-smoke/logs/stop.log\""'
```

Expected: command exits 0, service stops, and overlay unmount command succeeds.

- [ ] **Step 7: Delete the smoke instance and verify repeated delete**

Run:

```bash
ssh ckn@10.0.4.128 'bash -lc "set -o pipefail; XDG_RUNTIME_DIR=/run/user/$(id -u) \"$HOME/l4d2host-smoke/.venv/bin/l4d2ctl\" delete smoke 2>&1 | tee \"$HOME/l4d2host-smoke/logs/delete-1.log\""'
```

Expected: command exits 0 and removes `/opt/l4d2/instances/smoke` and `/opt/l4d2/runtime/smoke`.

Run:

```bash
ssh ckn@10.0.4.128 'set -u; if [ -e /opt/l4d2/instances/smoke ] || [ -e /opt/l4d2/runtime/smoke ]; then ls -ld /opt/l4d2/instances/smoke /opt/l4d2/runtime/smoke 2>/dev/null; exit 1; fi; printf "smoke instance/runtime removed\n"'
```

Expected: output is `smoke instance/runtime removed`.

Run:

```bash
ssh ckn@10.0.4.128 'bash -lc "set -o pipefail; XDG_RUNTIME_DIR=/run/user/$(id -u) \"$HOME/l4d2host-smoke/.venv/bin/l4d2ctl\" delete smoke 2>&1 | tee \"$HOME/l4d2host-smoke/logs/delete-2.log\""'
```

Expected: command exits 0, proving missing instance/runtime delete is a no-op success.

- [ ] **Step 8: Report findings and stop**

Report:

```text
Task 5 evidence:
- spec creation: report remote spec path and rendered YAML
- initialize: report exit status and created instance/runtime files
- start: report exit status and start log result
- service/mount status: report systemd user service state and overlay mount state
- status API: report printed InstanceStatus value
- logs API: report whether journal lines were returned
- stop: report exit status and unmount result
- delete and repeated delete: report first delete status, path removal status, and second delete status

Approve Task 6: cleanup decision on ckn@10.0.4.128?
```

Expected: smoke instance has been deleted, and `/opt/l4d2/installation` remains available for later web-app testing unless cleanup removes it.

### Task 6: Cleanup Decision

**Files:**
- Remote optional delete: `~/l4d2host-smoke/`
- Remote optional delete: `/opt/l4d2/installation/`
- Remote optional delete: `/opt/l4d2/`

- [ ] **Step 1: Ask for cleanup preference**

Ask:

```text
Cleanup options for ckn@10.0.4.128:
1. Keep /opt/l4d2/installation and remove only ~/l4d2host-smoke
2. Keep everything for debugging/later web testing
3. Remove all smoke-test artifacts including /opt/l4d2

Which cleanup option should I run?
```

Expected: user selects one option before cleanup commands are run.

- [ ] **Step 2: Run selected cleanup command**

For option 1:

```bash
ssh ckn@10.0.4.128 'set -eu; rm -rf "$HOME/l4d2host-smoke"; printf "kept /opt/l4d2, removed ~/l4d2host-smoke\n"'
```

Expected: remote smoke workspace is removed; `/opt/l4d2/installation` remains.

For option 2:

```bash
ssh ckn@10.0.4.128 'set -eu; printf "kept ~/l4d2host-smoke and /opt/l4d2 for later inspection\n"; ls -ld "$HOME/l4d2host-smoke" /opt/l4d2 /opt/l4d2/installation 2>/dev/null || true'
```

Expected: no cleanup is performed; paths are listed if present.

For option 3:

```bash
ssh ckn@10.0.4.128 'set -eu; rm -rf "$HOME/l4d2host-smoke"; sudo rm -rf /opt/l4d2; printf "removed ~/l4d2host-smoke and /opt/l4d2\n"'
```

Expected: remote smoke workspace and `/opt/l4d2` are removed.

- [ ] **Step 3: Report final state**

Run:

```bash
ssh ckn@10.0.4.128 'set -u; printf "workspace=\n"; ls -ld "$HOME/l4d2host-smoke" 2>/dev/null || printf "~/l4d2host-smoke missing\n"; printf "\nopt_l4d2=\n"; ls -ld /opt/l4d2 /opt/l4d2/installation 2>/dev/null || printf "/opt/l4d2 or installation missing\n"; printf "\nsmoke_mounts=\n"; mount | grep /opt/l4d2 || true; printf "\nsmoke_service=\n"; XDG_RUNTIME_DIR="/run/user/$(id -u)" systemctl --user status l4d2@smoke.service --no-pager 2>&1 | sed -n "1,30p" || true'
```

Expected: final state matches selected cleanup option; no smoke mounts remain.

- [ ] **Step 4: Summarize smoke-test result**

Report:

```text
Host-lib smoke-test result:
- Server inspected: state whether Task 1 completed successfully
- Server prepared: state whether Task 2 completed successfully
- Host lib deployed: state whether Task 3 completed successfully
- l4d2ctl install validated: state whether Task 4 completed successfully
- lifecycle validated: state whether Task 5 completed successfully
- cleanup option applied: state which cleanup option was applied
- host-lib defects found: list defects found, or state that none were found during the smoke test
- recommended next phase: choose web-app lifecycle job wiring or host-lib fix plan based on evidence
```

Expected: final report clearly states whether to proceed to web-app lifecycle job wiring or stop for a host-lib fix.

---

## Self-Review Checklist

- [ ] Spec coverage: all six design steps are represented as gated tasks.
- [ ] Approval constraint: every server-touching task starts with an explicit approval step.
- [ ] Failure policy: failure reporting and no automatic cleanup are documented.
- [ ] Evidence: each task has exact commands and expected evidence.
- [ ] Scope: no web-app implementation is included in this plan.
