---
phase: 03-retry-schedule-reshape
plan: 03
type: execute
wave: 3
depends_on:
  - "03-02"
files_modified:
  - .planning/phases/03-retry-schedule-reshape/uat_zero_loss.py
  - .planning/phases/03-retry-schedule-reshape/03-UAT-RESULTS.json
autonomous: true
requirements:
  - RETRY-01
  - RETRY-02
user_setup: []

must_haves:
  truths:
    - "A standalone headless harness measures packets-per-trial and per-request latency by driving the real, shipped DeviceConnection.request() against a real device — never a hand-rolled schedule (Phase 2 harness rule)"
    - "Measured results are reported honestly: actual numbers on disk, a fixed pass threshold set before the run and never adjusted afterwards, and an explicit ENV-ERROR outcome when the device is unreachable — never a fabricated or massaged pass"
    - "An unreachable device does not fail the phase: this measurement is optional per 03-CONTEXT.md (unlike Phase 2's mandatory DISC-03)"
  artifacts:
    - path: ".planning/phases/03-retry-schedule-reshape/uat_zero_loss.py"
      provides: "zero-loss packets/trial + latency measurement harness with a 0/1/2 exit-code contract"
      min_lines: 80
    - path: ".planning/phases/03-retry-schedule-reshape/03-UAT-RESULTS.json"
      provides: "machine-readable measurement evidence (or recorded ENV-ERROR outcome)"
  key_links:
    - from: ".planning/phases/03-retry-schedule-reshape/uat_zero_loss.py"
      to: "src/lifx/network/connection.py"
      via: "imports DeviceConnection and wraps the bound send_packet with a counting spy — measuring the shipped implementation, not a reimplementation"
      pattern: "from lifx.network.connection import DeviceConnection"
---

<objective>
Build and run the optional headless hardware measurement from 03-CONTEXT.md: a
zero-loss packets-per-trial check of the reshaped retry schedule against the quiesced
gen4 downlight at 192.168.18.95 — the RTT-sensitive device where spike 002 measured
1.37 packets/trial (target: 1.0) and a 62 ms median latency inflated by the jitter sleep
(target: ≈ raw RTT). This is supplementary real-network evidence for RETRY-01 and
RETRY-02 on top of the automated suite; the requirements are already closed by plan
03-02's emulator-backed tests.

Purpose: real-RTT evidence that the D3-01 floor eliminates duplicate packets and the
D3-02 listen-during-backoff eliminates the latency tail, mirroring the Phase 2
hardware-evidence pattern (uat_rounds.py) with the same honest-reporting contract.
Output: `uat_zero_loss.py` harness and `03-UAT-RESULTS.json` recorded evidence.
</objective>

<execution_context>
@$HOME/.claude/gsd-core/workflows/execute-plan.md
@$HOME/.claude/gsd-core/templates/summary.md
</execution_context>

<context>
@.planning/STATE.md
@.planning/phases/03-retry-schedule-reshape/03-CONTEXT.md
@.planning/phases/03-retry-schedule-reshape/03-RESEARCH.md
@.planning/phases/02-discovery-rebroadcast/uat_rounds.py
@.claude/skills/spike-findings-lifx-async/references/retry-schedule.md
</context>

<tasks>

<task type="auto">
  <name>Task 1: Build the zero-loss measurement harness (uat_zero_loss.py)</name>
  <files>.planning/phases/03-retry-schedule-reshape/uat_zero_loss.py</files>
  <read_first>
    - .planning/phases/02-discovery-rebroadcast/uat_rounds.py — the Phase 2 harness this mirrors: module docstring shape (what/why/run/exit codes), argparse conventions, JSON results writing, the drive-the-shipped-implementation rule, and the sanity-guard philosophy (a degraded environment must never produce a trivial pass)
    - .planning/phases/03-retry-schedule-reshape/03-RESEARCH.md — §Validation Architecture manual-only row (N × request(GetColor()) with a send spy against 192.168.18.95) and §Summary (the spike 002 baselines: 1.37 packets/trial, 62 ms gen4 median)
    - .claude/skills/spike-findings-lifx-async/references/retry-schedule.md — the measured baseline table the results JSON records for comparison
  </read_first>
  <action>
    Create `.planning/phases/03-retry-schedule-reshape/uat_zero_loss.py`, a standalone
    phase-dir script (never imported from src/ or tests/, never shipped — Phase 2
    convention). Requirements:

    1. Module docstring: RETRY-01/RETRY-02 hardware evidence, optional per 03-CONTEXT.md; the run command; the exit-code contract; and the honest-reporting rules (thresholds are fixed in this file BEFORE any run and are never adjusted in response to results; every run's actual numbers are written to the JSON).
    2. argparse: `--ip` (default 192.168.18.95, the quiesced gen4 downlight), `--trials` (default 60, matching a spike 002 regime arm), `--gap` (default 0.1 s between trials — stays well under the ~20 msg/s device ceiling), `--timeout` (default 2.0 s per request), `--json-out` (default the phase-dir 03-UAT-RESULTS.json). `--help` must work without sending any packets.
    3. Measurement loop: one persistent DeviceConnection to the target (library defaults for max_retries); wrap the bound `conn.send_packet` with a counting async wrapper by plain attribute assignment (no mock imports in a script) that increments a per-trial counter before delegating; per trial reset the counter, `await conn.request(Light.GetColor(), timeout=args.timeout)` timing with time.monotonic, and record (tx_count, latency_ms, ok). Count LifxTimeoutError as a failed trial, keep going. Sleep the gap between trials.
    4. Reachability guard: before the trial loop, one probe request with a generous timeout; if it fails, exit 2 (ENV-ERROR) immediately without hammering — an off-network run must never produce either a fabricated pass or 60 timeout storms.
    5. Results JSON: per-trial arrays, trials run, failures, packets_per_trial mean and median, latency_ms median, the fixed thresholds, the spike 002 baseline block (lifx-async 1.37 packets/trial and 62 ms gen4 median; photons 1.08), a pass flag, and the exit code.
    6. Exit-code contract (fixed here, before any run): 0 PASS — zero failed trials AND median packets/trial == 1 AND mean packets/trial <= 1.05 (tolerates up to ~3 genuine WiFi-loss retransmits in 60 trials, which are the schedule working as designed, while still proving the duplicate storm is gone); 1 FAIL — device reachable but thresholds not met (a genuine regression signal: report it, never massage it); 2 ENV-ERROR — reachability probe failed (recorded honestly; does not fail the phase since the measurement is optional). Median latency is recorded and compared against the 62 ms baseline as informational RETRY-02 evidence, not a gate (raw RTT varies by network).
    7. Style: all imports at top, Australian English, ruff-clean, stdlib + lifx only.
  </action>
  <verify>
    <automated>uv run python .planning/phases/03-retry-schedule-reshape/uat_zero_loss.py --help</automated>
  </verify>
  <acceptance_criteria>
    - `uv run python .planning/phases/03-retry-schedule-reshape/uat_zero_loss.py --help` exits 0 without sending any packets
    - `uv run ruff check .planning/phases/03-retry-schedule-reshape/uat_zero_loss.py` passes
    - The harness imports and drives DeviceConnection.request() directly — no hand-rolled schedule, no copies of connection.py logic
    - The pass thresholds and exit-code contract in the file match this plan exactly (they are set before the measurement run and must not change after it)
  </acceptance_criteria>
  <done>A standalone, ruff-clean harness exists that measures the shipped retry path's packets/trial and latency with a fixed, pre-declared pass contract.</done>
</task>

<task type="auto">
  <name>Task 2: Run the zero-loss measurement and record the result honestly</name>
  <files>.planning/phases/03-retry-schedule-reshape/03-UAT-RESULTS.json</files>
  <read_first>
    - .planning/phases/03-retry-schedule-reshape/uat_zero_loss.py — the harness and its exit-code contract as landed in Task 1
    - .planning/phases/02-discovery-rebroadcast/02-02-SUMMARY.md — the honest-reporting precedent: actual per-round numbers recorded, anomalies explained not hidden, thresholds never altered after seeing results
  </read_first>
  <action>
    Run the measurement headless and record whatever it actually says:

    1. `uv run python .planning/phases/03-retry-schedule-reshape/uat_zero_loss.py --trials 60 --json-out .planning/phases/03-retry-schedule-reshape/03-UAT-RESULTS.json` — expected wall time roughly 10-30 s on a healthy network.
    2. Interpret strictly by exit code. 0 (PASS): record the measured packets/trial and median latency against the 1.37 / 62 ms baselines in the summary. 2 (ENV-ERROR, device unreachable from this machine): record the outcome as SKIPPED-optional in the JSON and summary — the phase does NOT fail (03-CONTEXT.md marks this measurement optional) and the harness remains available for the user to re-run on-network. 1 (FAIL, reachable but over threshold): this is a genuine signal — do NOT re-run repeatedly hoping for a pass, do NOT touch the thresholds; record the actual numbers, re-run ONCE to rule out a transient network event (Phase 2 precedent), and if it fails again flag the result prominently in the summary for the developer with the per-trial data needed to diagnose.
    3. Never edit the harness's thresholds, trial count, or pass logic in this task for any reason (the Phase 2 rule: measurement contracts are fixed before the run).
  </action>
  <verify>
    <automated>uv run python .planning/phases/03-retry-schedule-reshape/uat_zero_loss.py --trials 60 --json-out .planning/phases/03-retry-schedule-reshape/03-UAT-RESULTS.json; test -f .planning/phases/03-retry-schedule-reshape/03-UAT-RESULTS.json</automated>
  </verify>
  <acceptance_criteria>
    - 03-UAT-RESULTS.json exists and contains the actual measured per-trial data (or the recorded ENV-ERROR outcome) — never placeholder or invented numbers
    - The harness exit code, its meaning, and the measured packets/trial + median latency (when measured) are recorded in the summary against the spike 002 baselines
    - The harness file is byte-identical to its Task 1 commit (thresholds untouched by the run)
  </acceptance_criteria>
  <done>The optional zero-loss measurement has been executed (or honestly recorded as environment-unavailable) with results on disk as phase evidence.</done>
</task>

</tasks>

## Artifacts this phase produces (plan 03)

| Artifact | Kind | Location |
|----------|------|----------|
| `uat_zero_loss.py` — zero-loss packets/trial + latency harness (argparse, reachability guard, fixed 0/1/2 exit-code contract) | Measurement harness (phase-dir only, never shipped) | `.planning/phases/03-retry-schedule-reshape/uat_zero_loss.py` |
| `03-UAT-RESULTS.json` — recorded measurement vs spike 002 baselines (1.37 packets/trial, 62 ms gen4 median) | Evidence artefact | `.planning/phases/03-retry-schedule-reshape/03-UAT-RESULTS.json` |

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| Harness → LAN device | Outbound request load against a single user-owned device on the user's own network |

## STRIDE Threat Register

| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-03-04 | DoS (outbound, self-inflicted) | measurement loop | low | mitigate | Fixed trial count, 0.1 s inter-trial gap (well under the ~20 msg/s device ceiling), and a reachability guard that aborts (exit 2) instead of hammering an absent device |
| T-03-05 | Repudiation | measurement evidence | low | mitigate | Results written to a machine-readable JSON with fixed pre-declared thresholds; the harness file must be byte-identical before and after the run |

No packages are installed by this plan (zero-dependency project) — no supply-chain gate applies.
</threat_model>

<verification>
- `uv run python .planning/phases/03-retry-schedule-reshape/uat_zero_loss.py --help` — exits 0, no packets
- `uv run ruff check .planning/phases/03-retry-schedule-reshape/uat_zero_loss.py` — clean
- `test -f .planning/phases/03-retry-schedule-reshape/03-UAT-RESULTS.json` — evidence on disk after the run
- Exit code interpreted per the fixed contract; harness unmodified by the run
</verification>

<success_criteria>
- The shipped retry path measured on real hardware: packets/trial and median latency recorded against the spike 002 baselines (RETRY-01, RETRY-02 supplementary evidence)
- Honest outcome regardless of result: PASS with numbers, FAIL flagged loudly with per-trial data, or ENV-ERROR recorded as skipped-optional without failing the phase
</success_criteria>

<output>
Create `.planning/phases/03-retry-schedule-reshape/03-03-SUMMARY.md` when done, recording:
the exit code, measured packets/trial mean/median and latency median vs baselines (or the
ENV-ERROR outcome), and the commit hashes.
</output>
