---
phase: 04-animation-flow-control
plan: 12
type: execute
wave: 10
depends_on:
  - "04-11"
files_modified:
  - .planning/phases/04-animation-flow-control/04-SWEEP-DESIGN.md
  - .planning/phases/04-animation-flow-control/sweep_design.py
  - .planning/phases/04-animation-flow-control/uat_ack_stream.py
  - .planning/phases/04-animation-flow-control/04-06-PLAN.md
  - .planning/phases/04-animation-flow-control/04-07-PLAN.md
  - .planning/phases/04-animation-flow-control/04-UAT-SWEEP.json
  - ".planning/phases/04-animation-flow-control/04-UAT-SWEEP-<serial>.json (8 per-device evidence files, serial-named)"
  - .planning/ROADMAP.md
  - .planning/REQUIREMENTS.md
  - .planning/STATE.md
autonomous: false
gap_closure: true
requirements:
  - ANIM-03
  - ANIM-04

must_haves:
  truths:
    - "A cross-device ANIM-03/ANIM-04 certification rule exists as complete concrete wording (ROADMAP criteria 3 AND 4, REQUIREMENTS ANIM-03 AND ANIM-04, harness sweep contract) with the per-device paired-relative rule reused UNCHANGED (V1 2.5%/V2 n>=100/V3 0.50, pooled ceiling 9.0%, clean escape 2.5%, Fisher alpha 0.05, ratio >= 2.0) and every NEW parameter (aggregation K-of-N, validity quorum, attempt budget, session logistics) derived by a stated rule in a committed reproducible runner"
    - statement: "The operator approved the complete final wording at a single blocking checkpoint BEFORE any governing document or harness gate was touched — exactly one ask, protocol-fixed-before-data"
      verification: backstop
    - "The operator's Tiles ruling is recorded verbatim in the design brief and encoded in the criterion: gen3 Tiles radios are known-flaky, the measured consistent 2–2.5x improvement on them IS a win, and Tiles loss numbers never gate certification — System Test Tiles II runs as reference data only, never in the aggregation numerator or denominator"
    - "The paired sweep ran with one attempt per device over the 7-gate-device roster with honest per-device outcomes (PASS/FAIL/INCONCLUSIVE/ENV-ERROR) and an honest aggregate verdict — no retry-until-pass at either level, INCONCLUSIVE never laundered, evidence commits strictly after the design and application commits"
    - "ANIM-04 framebuffer-path evidence is captured on the three ceiling-class sweep devices: packets/frame asserted against the chain-dims-derived row-aligned expectation (not a hard-coded 8) and CopyFrameBuffer ack RTT samples recorded per gated round"
    - "Every reachable production device is restored to its as-found device-level power and colour after its session, with restoration status recorded per device"
    - statement: "Exactly one visual-smoothness ask total across 04-06/04-07/04-12: the operator watched My Office Ceiling Capsule and gave explicit smoothness (ANIM-03) and geometry (ANIM-04) verdicts; 04-06 Task 2 and both 04-07 tasks are explicitly superseded with visual ownership moved to this plan"
      verification: backstop
  artifacts:
    - path: ".planning/phases/04-animation-flow-control/04-SWEEP-DESIGN.md"
      provides: "evidence-derived cross-device criterion design: aggregation derivation, quorum, attempt budget, ANIM-04 piggyback definition, visual consolidation, complete proposed final wording, operator Tiles ruling verbatim"
    - path: ".planning/phases/04-animation-flow-control/sweep_design.py"
      provides: "reproducible exact-binomial aggregation statistics runner (zero-dependency, stdlib only)"
    - path: ".planning/phases/04-animation-flow-control/uat_ack_stream.py"
      provides: "sweep-generalised paired harness: per-device sweep sessions, serial-authoritative device resolution, profile auto-selection, chain-dims-derived packet expectation, as-found state restoration, per-device + sweep-level JSON"
    - path: ".planning/phases/04-animation-flow-control/04-UAT-SWEEP.json"
      provides: "sweep-level verdict: per-device outcomes, aggregation inputs, Tiles II reference row, aggregate outcome"
  key_links:
    - from: ".planning/phases/04-animation-flow-control/04-UAT-SWEEP.json"
      to: ".planning/phases/04-animation-flow-control/uat_ack_stream.py"
      via: "written by the sweep-verdict mode from the 8 committed per-device JSONs"
      pattern: "uat_ack_stream.py --sweep-verdict"
    - from: ".planning/phases/04-animation-flow-control/04-SWEEP-DESIGN.md"
      to: ".planning/phases/04-animation-flow-control/sweep_design.py"
      via: "every aggregation number in the brief reproducible with one command"
      pattern: "uv run python sweep_design.py"
    - from: ".planning/phases/04-animation-flow-control/uat_ack_stream.py"
      to: "src/lifx/animation/packets.py"
      via: "expected packets/frame derived from reported chain dims via the row-aligned chunking rule (read-only mirror of MatrixPacketGenerator)"
      pattern: "expected_packets_per_frame(tile_count, width, height)"
    - from: ".planning/ROADMAP.md"
      to: ".planning/phases/04-animation-flow-control/04-SWEEP-DESIGN.md"
      via: "criteria 3 and 4 replaced verbatim with the operator-approved wording"
      pattern: "applied only after the Task 2 checkpoint approval"
---

<objective>
Close the ANIM-03 gap left by 04-11's double paired-gate FAIL on System Test Tiles I
(gated 2.94%/5.51% vs blind 7.20%/9.73%, ratios 2.45x/1.77x, Fisher p 0.097/0.160 —
both sessions VALID) by executing the operator's new direction: record the Tiles
flaky-radio ruling verbatim, retarget ANIM-03 certification from "standard Tiles" to a
cross-device paired sweep over 7 healthy-radio matrix devices (Tiles II retained as
known-bad-radio reference data only), piggyback the ANIM-04 framebuffer evidence on the
three ceiling-class sweep devices, and consolidate the two outstanding visual
checkpoints (04-06 Task 2, 04-07 Task 2) into ONE ask on My Office Ceiling Capsule.

The per-device paired-relative rule from 04-CRITERION-DESIGN.md is reused UNCHANGED —
the operator approved it hours ago; only the target set, the cross-device aggregation,
and the session logistics are new design, and every new parameter must be derived with
evidence and statistics, approved by the operator from complete final wording BEFORE
anything is applied, and fixed BEFORE any sweep data is collected.

Purpose: ANIM-03 and ANIM-04 are the last two Phase 4 requirements; the operator's
ruling makes the twice-measured 2–2.5x improvement on flaky Tiles radios a win to be
recorded, not a gate to be failed, and moves certification onto radios that can
actually resolve the question.
Output: 04-SWEEP-DESIGN.md + sweep_design.py (design), amended ROADMAP criteria 3+4 and
REQUIREMENTS ANIM-03+ANIM-04 (post-approval), sweep-generalised uat_ack_stream.py,
04-UAT-SWEEP.json + 8 per-device evidence JSONs, and — on aggregate PASS + visual
approval — ANIM-03 and ANIM-04 marked complete.
</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/04-animation-flow-control/04-CRITERION-DESIGN.md
@.planning/phases/04-animation-flow-control/04-11-SUMMARY.md
@.planning/phases/04-animation-flow-control/uat_ack_stream.py
</context>

## The operator ruling this plan encodes

After the second valid-session paired FAIL, the operator supplied decisive domain
context and a new direction, verbatim:

> "We need to try different targets. The Tiles are notoriously flaky devices due to
> suboptimal antenna design, so even this amount of improvement is a win. There is
> also a lot of other stuff happening on my PC at the moment and the network, so
> overall we're working in a noisy environment"

Rulings to encode (recorded here; applied to governing documents ONLY after the Task 2
checkpoint approval):

1. **Tiles ruling:** gen3 Tiles radios are known-flaky; the measured consistent
   2–2.5x improvement on them IS a win; Tiles loss numbers must not gate
   certification. System Test Tiles II (192.168.18.62) stays in the sweep as
   known-bad-radio REFERENCE DATA only.
2. **Cross-device retarget:** ANIM-03 certification moves from "standard Tiles" to a
   cross-device paired sweep over healthy-radio matrix devices. Paths (outdoor lights)
   excluded — unsociable hour. All sweep targets have been disabled in Home Assistant
   (operator confirmed — no status-polling interference).

## The operator-approved sweep roster (discovery-verified reachable at planning time)

| # | Device | IP (DHCP — may drift) | Serial | Class → profile | Role |
|---|--------|----------------------|--------|-----------------|------|
| 1 | Playroom Luna | 192.168.19.182 | d073d5893c04 | MatrixLight (product 219, gen4) → tiles | gate |
| 2 | Dining Room Table Candle | 192.168.18.81 | d073d55956e8 | MatrixLight → tiles | gate |
| 3 | Makerspace Candle | 192.168.18.32 | d073d582bff4 | MatrixLight → tiles | gate |
| 4 | Makerspace Tube | 192.168.19.199 | d073d5866777 | MatrixLight → tiles | gate |
| 5 | Makerspace Ceiling | 192.168.19.119 | d073d5a132d9 | CeilingLight → ceiling | gate + ANIM-04 evidence |
| 6 | Playroom Ceiling | 192.168.19.82 | d073d5a132b8 | CeilingLight → ceiling | gate + ANIM-04 evidence |
| 7 | My Office Ceiling Capsule | 192.168.19.231 | d073d587daab | CeilingLight (product 201, 13×26) → ceiling | gate + ANIM-04 evidence + THE visual device |
| 8 | System Test Tiles II | 192.168.18.62 | d073d53e11be | MatrixLight (gen3 Tiles) → tiles | REFERENCE DATA ONLY — never gates |

IPs are DHCP leases and may drift between planning and execution: device resolution is
serial-authoritative — the serial is the device's identity; the roster IP is used only
as a fast path whose reported serial must match, with broadcast discovery by serial
(find_by_serial) as the fallback on unreachable or mismatch. A single moved/unreachable
device becomes that device's ENV-ERROR row — never a whole-sweep abort.

## Non-negotiable constraints (restated from the gap directive)

- Blocking operator checkpoint BEFORE any governing-doc/harness-gate application;
  exactly one ask; approved wording + harness commits strictly precede the sweep
  evidence commit (protocol-fixed-before-data, 04-08/04-10/04-11 discipline).
- Per-device rule parameters (2.5% / 9.0% / 2x / alpha 0.05 / validity bounds) are NOT
  re-tuned. Only the target set, aggregation, and session logistics are new design.
- src/ and tests/ untouched; blind-fire constructed at instrument level only (D4-02:
  the shipped Animator has no flow-control toggle); measurement scripts live in the
  phase directory only.
- Honest outcomes per device AND at sweep level; no retry-until-pass at either level;
  INCONCLUSIVE never launders a FAIL and is never silently retried.
- Production lights: restore each device to its as-found state (power + device-level
  colour) after its session; never leave a household light blazing test patterns.
- Signed commits (`git commit -s`, GPG automatic), uv-only, Australian English.
  Existing evidence files (04-UAT-TILES*.json, 04-GAP-*.json*) are never modified —
  all sweep evidence goes to NEW serial-named files.

<tasks>

<task type="auto">
  <name>Task 1: Cross-device sweep criterion design brief + reproducible aggregation runner</name>
  <files>.planning/phases/04-animation-flow-control/04-SWEEP-DESIGN.md, .planning/phases/04-animation-flow-control/sweep_design.py</files>
  <read_first>
    - .planning/phases/04-animation-flow-control/04-CRITERION-DESIGN.md — the per-device paired-relative rule (reused UNCHANGED), its derivation table, the declared exact power P(PASS | historical rates, n=100/arm) = 0.9008, and the brief-plus-runner discipline this task must follow
    - .planning/phases/04-animation-flow-control/criterion_design.py — the Fisher and exact-enumeration machinery to copy (standalone-script discipline, 04-11 precedent)
    - .planning/phases/04-animation-flow-control/04-UAT-TILES-paired-run1-FAIL.json and 04-UAT-TILES.json — the paired double-FAIL evidence that motivated the retarget (gated 2.94%/5.51%, blind 7.20%/9.73%, ratios 2.45/1.77, p 0.0973/0.1601, both sessions valid)
    - .planning/phases/04-animation-flow-control/04-11-SUMMARY.md — checkpoint precedent, commit-ordering discipline, and the honest-reporting record
    - .planning/phases/04-animation-flow-control/uat_ack_stream.py — the current session shape (60 s ambient + 3 G/B round pairs at 30 s + 10 s rests ≈ 5.25–5.5 min/device) and the per-device constants that must be restated unchanged
    - src/lifx/animation/packets.py (READ-ONLY) — MatrixPacketGenerator's row-aligned chunking rule, the source of the ANIM-04 packets/frame formula
  </read_first>
  <action>
    Produce `04-SWEEP-DESIGN.md` (the brief) and `sweep_design.py` (the runner), 04-11
    discipline: the runner is committed with the brief, every number in the brief is
    reproducible with one command (`uv run python
    .planning/phases/04-animation-flow-control/sweep_design.py`, stdlib only — the
    project is zero-dependency), and NOTHING in the brief is applied to ROADMAP.md,
    REQUIREMENTS.md, uat_ack_stream.py or src/ until the Task 2 approval.

    The brief must contain, in order:

    1. **The operator ruling, verbatim** — quote the ruling exactly as recorded in
       this plan's "The operator ruling this plan encodes" section, plus the two
       derived rulings (Tiles-as-reference, cross-device retarget) and the roster
       table with roles. Record that all targets are HA-disabled (operator confirmed)
       and that outdoor paths are excluded (unsociable hour).
    2. **The per-device rule, restated UNCHANGED** — session shape, V1/V2/V3
       validity, pooled ceiling 9.0%, clean escape 2.5%, Fisher alpha 0.05, ratio
       >= 2.0, exit contract 0/1/2/3. State explicitly that no per-device parameter
       is re-tuned (operator approved them ~a day ago at the 04-11 checkpoint) and
       that the ceiling profile keeps its per-gated-round checks with ONE
       generalisation: the packets/frame expectation becomes chain-dims-derived
       (section 5 below) because the hard-coded 8 is valid only for the 13×26
       Capsule, and the sweep adds ceiling-class devices with other geometries.
    3. **Cross-device aggregation, derived in sweep_design.py** — the rule family is:
       sweep PASS iff (valid gate devices N_valid >= quorum Q) AND (per-device PASSes
       among them >= K), where the 7 gate devices are the population, per-device
       INCONCLUSIVE and ENV-ERROR rows are EXCLUDED from N_valid but always reported,
       per-device FAILs count in N_valid, and Tiles II is never in either count.
       The runner must compute, by exact binomial enumeration (math.comb, no
       approximations):
       - P(sweep PASS) for allowed-FAIL counts 0, 1 and 2 (i.e. K = N_valid,
         N_valid-1, N_valid-2) across N_valid in {5, 6, 7} and per-device
         P(PASS) in a grid that includes the declared historical 0.9008 plus at
         least one pessimistic (~0.85) and one optimistic (~0.95) rate — healthy
         radios plausibly pass via the 2.5% clean escape more often than Tiles I did.
       - A NULL-scenario row (false-pass risk, computed — never prose-only):
         per-device P(PASS | flow control gives no improvement, p_gated = p_blind)
         at plausible ambient-healthy rates — at minimum both arms at 0.0278 and
         both at 0.146 — where the relative rule cannot systematically fire and
         only the clean escape (plus chance Fisher wins at the low rate) can
         produce a per-device PASS; then P(sweep PASS | null) for each candidate K
         by the same exact binomial enumeration.
       - The all-must-pass power at the historical rate MUST be surfaced explicitly
         (0.9008^7 ≈ 0.48): if the brief nevertheless chooses zero-allowed-FAILs it
         does so with that number in front of the operator, never blindly.
       - A cross-check that re-derives the per-device P(PASS | historical rates,
         p_g=0.0278, p_b=0.146, n=100) = 0.9008 using the exact-enumeration
         machinery copied from criterion_design.py, before that number is used as an
         aggregation input.
       - A false-certification argument for the chosen rule: what it certifies, and
         why the clean escape (gated pooled <= 2.5% = direct non-starvation evidence
         at the healthy-ambient ceiling) is an honest per-device PASS path even when
         the same-session blind arm happens to be low. The brief MUST state
         P(sweep PASS | null) for the CHOSEN K alongside its power, from the
         null-scenario rows above.
       Choose K and Q by a stated rule (04-11 precedent: pre-declare a minimum
       acceptable P(sweep PASS | historical per-device rate) — 0.85, matching the
       04-11 adjustment-rule bar — and pick the strictest K meeting it; derive Q so
       that a sweep cannot certify from a minority of the roster). State the chosen
       rule in one sentence a harness can implement.
    4. **Attempt budget and session logistics, derived** — default: ONE attempt per
       device, no per-device re-runs. Justification to state: the single-device mode
       needed a transient-rule-out re-run because one session was the entire
       evidence; the sweep's replication is cross-device (7 independent sessions),
       so a transient hit on one device is absorbed by the aggregation rule instead
       of a re-run, and one-attempt removes any retry-until-pass surface. State the
       contingency: if the operator amends the budget upward, first attempts are
       preserved as `04-UAT-SWEEP-<serial>-run1-<OUTCOME>.json` before any re-run.
       Ambient block: keep 60 s — show the arithmetic that at the fixed 2 q/s
       cadence a 60 s block yields at most ~120 queries and measured healthy
       sessions yielded 113–114, so V2's non-retunable n >= 100 leaves no material
       shortening room. Wall time: state ~5.25–5.5 min per device paired session,
       8 devices sequential (never parallel — measurement isolation and network
       load) ≈ 44 min streaming plus resolution/restore overhead → ~45–55 min total.
    5. **ANIM-04 piggyback definition** — what the sweep captures on the three
       ceiling-class devices so 04-07 Task 1 is fully covered: reported chain
       dimensions; every sent gated frame asserted against
       expected_packets_per_frame(tile_count, width, height), the read-only mirror
       of MatrixPacketGenerator's row-aligned rule — pixels = width × height; if
       pixels <= 64 the expectation is tile_count (one Set64 per tile, no
       CopyFrameBuffer); else rows_per_packet = 64 // width, set64_per_tile =
       ceil(height / rows_per_packet), expectation = tile_count × (set64_per_tile
       + 1) for the final CopyFrameBuffer per tile. Worked examples to include:
       13×26 → 64//13=4, ceil(26/4)=7, +1 = 8; 16×8 → 64//16=4, ceil(8/4)=2, +1 = 3;
       a 5-tile 8×8 chain → 5. Plus: CopyFrameBuffer ack RTT median/p95 and expiry
       counts per gated round (>= 1 sample per gated round, the unchanged A1
       evidence check), and the A1/A2 dispositions (CopyFB attachment per D4-04
       confirmed or contradicted-with-evidence). State what remains human-only:
       rendering geometry (striping/garbled regions) — the visual checkpoint.
    6. **Visual checkpoint consolidation** — propose ONE visual ask total, on My
       Office Ceiling Capsule (the operator is at their desk; the Capsule is in
       their office), with a DUAL verdict: smoothness (ANIM-03's human-only
       criterion) and geometry (ANIM-04's striping check — the row-aligned fix on
       the exact 13-wide hardware the old code would have corrupted). Disposition to
       propose: 04-06 Task 2 superseded (visual ownership moves to 04-12 Task 5);
       04-07 superseded in full (Task 1 by the Capsule's sweep session, Task 2 by
       04-12 Task 5); the sole-visual-owner rule moves to this plan.
    7. **Retrodiction, honest** — no sweep-roster device has any prior paired data,
       so retrodiction is limited: reclassify the historical Tiles I paired sessions
       under the new criterion as reference data (they show consistent 1.77–2.45x
       gated wins on a known-flaky radio — supporting evidence for the ruling, no
       longer a gate) and state plainly that no comparator can be manufactured for
       devices never measured.
    8. **Final wording (PROPOSED — NOT applied)** — complete concrete replacements
       for: (i) ROADMAP Phase 4 success criterion 3 (cross-device paired sweep,
       per-device rule unchanged, aggregation rule, Tiles-as-reference clause,
       INCONCLUSIVE/quorum handling); (ii) ROADMAP Phase 4 success criterion 4
       (framebuffer path validated on the sweep's ceiling-class devices under the
       same flow control, packets/frame derived from chain dims, CopyFB attachment
       decision recorded with ack RTT evidence, geometry confirmed visually on the
       Capsule); (iii) REQUIREMENTS ANIM-03 replacement (checkbox stays unchecked);
       (iv) REQUIREMENTS ANIM-04 replacement (checkbox stays unchecked); (v) the
       harness sweep contract — roster constant, serial-authoritative resolution
       (roster IP as a serial-checked fast path, broadcast discovery by serial as
       the fallback), profile auto-selection (CeilingLight → ceiling, other MatrixLight →
       tiles), as-found capture/restore protocol, per-device JSON schema and
       sweep-level JSON schema (must include a `devices` list of 8 entries each
       carrying serial/label/role/outcome, an `aggregation` block with K/N_valid/
       quorum, and a top-level `outcome`), CLI shape (`--sweep-device SERIAL` per
       session; `--sweep-verdict` deterministic aggregation from the on-disk
       per-device JSONs), and the sweep-level exit contract 0 PASS / 1 FAIL /
       2 ENV-ERROR / 3 INCONCLUSIVE (below quorum); (vi) the exact supersession
       notice text for 04-06 and 04-07.

    Commit both files (`docs(04-12): ...`, signed with -s) BEFORE Task 2. Australian
    English throughout.
  </action>
  <verify>
    <automated>uv run python .planning/phases/04-animation-flow-control/sweep_design.py && test -f .planning/phases/04-animation-flow-control/04-SWEEP-DESIGN.md</automated>
  </verify>
  <acceptance_criteria>
    - The operator quote appears verbatim in 04-SWEEP-DESIGN.md
    - Every per-device rule parameter is restated unchanged; every NEW parameter (K, Q, attempt budget, ambient retention) has a stated derivation whose numbers the runner reproduces
    - The all-must-pass power at the historical per-device rate (≈ 0.48) is surfaced explicitly, and the runner cross-checks the 0.9008 per-device input by exact enumeration before using it
    - The grid includes the null-scenario rows (p_gated = p_blind at 0.0278 and at 0.146) and the brief states P(sweep PASS | null) for the chosen K alongside its power
    - The packets/frame formula is stated with the three worked examples (13×26 → 8, 16×8 → 3, five 8×8 tiles → 5)
    - The final-wording section contains all six items (both ROADMAP criteria, both REQUIREMENTS entries, the full harness sweep contract, the 04-06/04-07 supersession text) and is marked PROPOSED — NOT applied
    - Both files committed before the Task 2 checkpoint; git diff for this task touches no src/, tests/, ROADMAP.md, REQUIREMENTS.md or uat_ack_stream.py paths
  </acceptance_criteria>
  <done>The complete cross-device certification design exists on disk with every new parameter evidence-derived and reproducible, and the operator can decide from concrete final wording.</done>
</task>

<task type="checkpoint:decision" gate="blocking">
  <name>Task 2: Blocking operator approval — cross-device sweep criterion, complete final wording (exactly one ask)</name>
  <decision>Adopt the cross-device paired-sweep certification for ANIM-03 and ANIM-04 (replacing the twice-FAILed single-device Tiles gate), as complete concrete wording</decision>
  <context>
    The paired-relative gate you approved at the 04-11 checkpoint FAILed twice on
    System Test Tiles I with both sessions VALID (gated 2.94%/5.51% vs blind
    7.20%/9.73% — the gated arm won both times, 2.45x/1.77x, but missed the
    significance-plus-ratio bar). Your ruling: Tiles radios are notoriously flaky,
    that improvement IS a win, try different targets. This ask presents EVERYTHING
    that ruling changes, in final wording, before any of it is applied:

    - The proposed ROADMAP Phase 4 criterion 3 AND criterion 4 replacements, and the
      REQUIREMENTS ANIM-03 AND ANIM-04 replacements (verbatim, from 04-SWEEP-DESIGN.md
      section 8)
    - Your Tiles ruling recorded verbatim, with Tiles II encoded as reference-only
    - The per-device rule UNCHANGED; the derived aggregation rule (K of N_valid with
      quorum Q, INCONCLUSIVE/unreachable excluded from the denominator but reported)
      with its exact-binomial power table, including the surfaced all-must-pass power
    - The attempt budget (one per device, derivation stated) and the wall-time
      estimate (~45–55 min total, 8 sequential sessions, ~5.5 min each)
    - The harness sweep contract (roster, serial-authoritative resolution, profile
      auto-selection, chain-dims-derived ceiling packet expectation replacing the
      hard-coded 8, as-found power+colour capture/restore, per-device + sweep-level
      JSON, exit contract)
    - The visual consolidation: ONE ask total, dual smoothness+geometry verdict on My
      Office Ceiling Capsule (in your office, visible from your desk); 04-06 Task 2
      and all of 04-07 superseded, visual ownership moving to this plan
    - The retrodiction note: historical Tiles I paired sessions reclassify as
      supporting reference data (consistent 1.77–2.45x gated wins on a flaky radio)

    Present the full final wording INLINE at this checkpoint (04-09/04-11 precedent —
    the operator decides from concrete text, never from a summary). Nothing is applied
    before approval. Task 1's commits contain only the design brief and runner.
  </context>
  <options>
    <option id="1">
      <name>Approve as presented</name>
      <pros>The retarget lands exactly as derived; protocol fixed before any sweep data; single approval covers both criteria, both requirements, the harness and the visual consolidation</pros>
      <cons>Accepts the derived aggregation bar and one-attempt budget as-is</cons>
    </option>
    <option id="2">
      <name>Approve with amendments (state them)</name>
      <pros>Any parameter you name (K, quorum, attempt budget, roster membership, visual device) is applied exactly as you amend it</pros>
      <cons>Amended values are recorded as operator-set rather than evidence-derived</cons>
    </option>
    <option id="3">
      <name>Reject — different routing</name>
      <pros>Nothing is applied; the design brief stands as evidence for a fresh direction</pros>
      <cons>ANIM-03/ANIM-04 remain gated pending a new plan</cons>
    </option>
  </options>
  <action>
    Present the complete final wording from 04-SWEEP-DESIGN.md section 8 INLINE at
    this checkpoint — both ROADMAP criteria, both REQUIREMENTS entries, the harness
    sweep contract, the 04-06/04-07 disposition text — together with the Tiles ruling
    verbatim, the aggregation power table (all-must-pass power surfaced), the attempt
    budget derivation and the wall-time estimate. This is the ONLY operator ask before
    application. Then BLOCK until the operator responds. Record the verbatim reply in
    the summary. Nothing in Task 3 executes before an approving reply; on "3", record
    the routing and stop the plan.
  </action>
  <verify>
    <human-check>Operator has replied to the single consolidated ask with an explicit decision (1, 2 plus amendments, or 3 plus direction)</human-check>
  </verify>
  <done>The operator's decision on the complete final wording is recorded verbatim, and the plan proceeds only on approval — with amendments, if any, captured exactly as stated.</done>
  <resume-signal>Reply "1" to approve as presented, "2" plus your amendments, or "3" plus direction. Your reply is recorded verbatim in the summary.</resume-signal>
</task>

<task type="auto">
  <name>Task 3: Apply the approved wording (governing docs) + generalise the harness to sweep mode</name>
  <files>.planning/ROADMAP.md, .planning/REQUIREMENTS.md, .planning/phases/04-animation-flow-control/04-06-PLAN.md, .planning/phases/04-animation-flow-control/04-07-PLAN.md, .planning/phases/04-animation-flow-control/uat_ack_stream.py</files>
  <read_first>
    - .planning/phases/04-animation-flow-control/04-SWEEP-DESIGN.md — the approved (or operator-amended) final wording to apply verbatim
    - .planning/phases/04-animation-flow-control/uat_ack_stream.py — the harness being generalised; the per-device rule constants and `_evaluate` MUST survive byte-identical in behaviour
    - src/lifx/animation/packets.py (READ-ONLY) — MatrixPacketGenerator.__init__ rows/packets arithmetic and _create_large_tile_templates (+1 CopyFrameBuffer per tile) that expected_packets_per_frame mirrors
    - src/lifx/api.py (READ-ONLY) — find_by_serial signature for serial-authoritative resolution
    - src/lifx/products/registry.py (READ-ONLY) — get_product/get_device_class_name if class detection via version lookup is needed on the from_ip fallback path
  </read_first>
  <action>
    Two commits, in order, BOTH strictly before any Task 4 evidence commit. Do NOT
    execute this task unless Task 2's reply approved (option 1, or 2 with amendments —
    apply amendments exactly as stated; on option 3, stop and record the routing).

    **Commit 3a — governing documents (`docs(04-12): ...`, signed):**
    1. ROADMAP.md: replace Phase 4 success criterion 3 AND criterion 4 with the
       approved wording EXACTLY (single-line reflow to match list style is the only
       permitted deviation — 04-10/04-11 precedent). Refresh the 04-06/04-07 plan-list
       bullets to name the 04-12 supersession.
    2. REQUIREMENTS.md: replace the ANIM-03 and ANIM-04 entries with the approved
       wording; both checkboxes STAY UNCHECKED; Traceability rows stay "Pending"
       (completion is owned by Task 5, per the approved consolidation).
    3. 04-06-PLAN.md and 04-07-PLAN.md: append exactly ONE new 04-12 SUPERSESSION
       NOTICE block each (after their 04-11 notices; every other line byte-identical),
       using the approved disposition text: 04-06 Task 2 superseded — visual ownership
       moves to 04-12 Task 5; 04-07 superseded in full — Task 1 covered by the
       Capsule's sweep session, Task 2 by 04-12 Task 5.

    **Commit 3b — harness generalisation (`feat(04-12): ...`, signed), single file
    uat_ack_stream.py, per the approved contract:**
    4. Module-level ROSTER constant: the 8 roster entries (label, ip, serial, role)
       with gate vs reference encoded; Tiles II role "reference".
    5. New CLI modes alongside the preserved single-device mode (prior evidence must
       stay reproducible): `--sweep-device SERIAL` runs ONE paired session for that
       roster entry and writes `04-UAT-SWEEP-<serial>.json` (existing per-session
       exit contract 0/1/2/3); `--sweep-verdict` deterministically aggregates the 8
       on-disk per-device JSONs into `04-UAT-SWEEP.json` per the approved K/quorum
       rule (Tiles II reported, never counted) and exits 0 PASS / 1 FAIL /
       2 ENV-ERROR / 3 INCONCLUSIVE (below quorum) at sweep level. The sweep JSON
       carries the approved schema — at minimum a `devices` list of 8 entries
       (serial/label/role/outcome), an `aggregation` block (K, N_valid, quorum,
       counts), the rules/thresholds block, and a top-level `outcome`.
    6. Serial-authoritative device resolution per session — the serial is the
       device's identity; the roster IP is only a fast path: attempt
       MatrixLight.from_ip on the roster IP and confirm the reported serial matches
       the roster serial; on unreachable or mismatch, fall back to broadcast
       discovery by serial (find_by_serial); both failing → that device's
       ENV-ERROR JSON is written and the sweep continues — one moved DHCP lease
       never aborts the sweep.
    7. Profile auto-selection: CeilingLight instance → ceiling profile; any other
       MatrixLight → tiles profile (on the from_ip fallback path, detect class via
       get_version + the products registry if isinstance is not available).
    8. Ceiling packet-shape generalisation: add `expected_packets_per_frame(
       tile_count, width, height)` mirroring the row-aligned rule in packets.py —
       pixels = width × height; pixels <= 64 → tile_count; else rows_per_packet =
       64 // width, set64_per_tile = ceil(height / rows_per_packet) (use the
       -(-height // rows_per_packet) idiom or math.ceil), return tile_count ×
       (set64_per_tile + 1). The ceiling profile's per-frame assertion compares
       stats.packets_sent against this value computed from the REPORTED chain
       dimensions (an independent derivation — never read the generator's own
       packets_per_tile back, which would be circular). The Capsule still expects 8;
       a 16×8 ceiling expects 3.
    9. As-found state restoration per session: before streaming, capture
       get_power() and get_color(); ceiling profile keeps its unchanged power-on +
       1 s settle + get_power() confirmation; tiles-profile sessions do NOT power
       devices on (query-loss measurement needs no visibility — least household
       disturbance at an unsociable hour). After the session (in a finally block),
       restore the captured colour then the captured power state. "As found" covers
       device-level power and the single device-level HSBK colour from get_color();
       per-pixel matrix contents and running firmware effects are NOT captured —
       that is the public-API feasibility boundary, and a device restored to
       powered-off is indistinguishable from as-found. Record restoration status
       (attempted/succeeded/failed with reason) in the per-device JSON; restoration
       failure never aborts the sweep and never alters the measurement outcome.
    10. Per-device rule constants and `_evaluate` semantics unchanged — the paired
        gate itself is not being re-tuned. Docstring rewritten to match the new modes
        in every location. No fenced examples that contradict the code.
    11. Style gates: `uv run ruff format`, `uv run ruff check --fix`, `uv run
        pyright` clean on the harness; prek/pre-commit hooks (bandit, codespell)
        pass at commit time; Australian English in all new prose. src/ and tests/
        untouched (D4-02); no dependencies installed.
  </action>
  <verify>
    <automated>uv run ruff format --check .planning/phases/04-animation-flow-control/uat_ack_stream.py && uv run ruff check .planning/phases/04-animation-flow-control/uat_ack_stream.py && uv run pyright .planning/phases/04-animation-flow-control/uat_ack_stream.py && uv run python -c "import sys; sys.path.insert(0, '.planning/phases/04-animation-flow-control'); import uat_ack_stream as u; assert u.expected_packets_per_frame(1, 13, 26) == 8; assert u.expected_packets_per_frame(1, 16, 8) == 3; assert u.expected_packets_per_frame(5, 8, 8) == 5"</automated>
  </verify>
  <acceptance_criteria>
    - Approved wording applied EXACTLY to ROADMAP criteria 3 and 4 and REQUIREMENTS ANIM-03 and ANIM-04 (reflow-only deviation); both checkboxes unchecked; Traceability rows "Pending"
    - Exactly one new supersession notice block in each of 04-06-PLAN.md and 04-07-PLAN.md; every other line of those files byte-identical
    - Harness has --sweep-device and --sweep-verdict modes, the ROSTER constant with Tiles II as reference, serial-authoritative resolution (serial-checked IP fast path + find_by_serial fallback), profile auto-selection, expected_packets_per_frame passing the three worked examples, and as-found capture/restore with recorded status
    - Per-device gate constants byte-identical to their 04-11 values; single-device CLI mode still works
    - ruff format/check + pyright clean; hooks pass; git diff across 3a+3b touches no src/ or tests/ paths; both commits signed and strictly before any sweep evidence
  </acceptance_criteria>
  <done>The approved cross-device criterion governs ROADMAP, REQUIREMENTS and the harness, the protocol is committed and frozen, and no sweep data exists yet.</done>
</task>

<task type="auto">
  <name>Task 4: Run the sweep — one attempt per device, honest per-device and aggregate outcomes</name>
  <files>.planning/phases/04-animation-flow-control/04-UAT-SWEEP.json, .planning/phases/04-animation-flow-control/04-UAT-SWEEP-d073d5893c04.json, .planning/phases/04-animation-flow-control/04-UAT-SWEEP-d073d55956e8.json, .planning/phases/04-animation-flow-control/04-UAT-SWEEP-d073d582bff4.json, .planning/phases/04-animation-flow-control/04-UAT-SWEEP-d073d5866777.json, .planning/phases/04-animation-flow-control/04-UAT-SWEEP-d073d5a132d9.json, .planning/phases/04-animation-flow-control/04-UAT-SWEEP-d073d5a132b8.json, .planning/phases/04-animation-flow-control/04-UAT-SWEEP-d073d587daab.json, .planning/phases/04-animation-flow-control/04-UAT-SWEEP-d073d53e11be.json, .planning/STATE.md</files>
  <read_first>
    - .planning/phases/04-animation-flow-control/04-SWEEP-DESIGN.md — the approved aggregation rule, quorum, budget and routing the run is judged by
    - .planning/phases/04-animation-flow-control/uat_ack_stream.py — the frozen sweep harness (verify it is byte-identical to the Task 3b commit before running: `git diff --quiet` on the file)
    - .planning/phases/02-discovery-rebroadcast/02-02-SUMMARY.md — the honest-reporting precedent (actual numbers recorded, anomalies explained, thresholds never altered after seeing results)
  </read_first>
  <action>
    Fixed rules before data. Confirm via git log that the Task 1 and Task 3 commits
    all precede this task's work and that uat_ack_stream.py has no uncommitted
    changes. Then:

    1. Run the 8 per-device sessions SEQUENTIALLY (never in parallel), one command
       per device so each invocation fits comfortably inside a 10-minute command
       budget (~5.5 min each): `uv run python
       .planning/phases/04-animation-flow-control/uat_ack_stream.py --sweep-device
       <serial>` for each roster serial in roster order, ending with Tiles II
       (d073d53e11be, reference). ONE attempt per device — no re-runs on FAIL or
       INCONCLUSIVE (the approved budget; cross-device replication replaces the
       old transient-rule-out re-run). A device that resolves neither by IP nor by
       serial is recorded as its own ENV-ERROR JSON and the sweep continues.
    2. Run `uv run python
       .planning/phases/04-animation-flow-control/uat_ack_stream.py --sweep-verdict`
       to produce 04-UAT-SWEEP.json from the 8 on-disk per-device files. Record the
       aggregate outcome exactly as the frozen rule computes it. Never adjust any
       threshold, rule, or roster entry after seeing results.
    3. Record in the summary, per device: outcome, gated/blind pooled loss, Fisher
       p, improvement ratio, validity reasons, restoration status; for the three
       ceiling-class devices additionally: reported chain dimensions, observed
       packets/frame vs the derived expectation, CopyFB ack RTT median/p95 and
       expiry counts (the ANIM-04/A1/A2 evidence); for Tiles II: the same figures
       labelled REFERENCE — never counted in the aggregation.
    4. Update the STATE.md "UAT sequencing gate" bullet: the sweep outcome, the
       per-device tally (PASS/FAIL/INCONCLUSIVE/ENV-ERROR counts), and the routing.
    5. Commit all evidence + STATE.md (`test(04-12): ...`, signed) — strictly after
       the Task 3 commits. Existing evidence files are untouched (verify the commit
       adds only new 04-UAT-SWEEP* files and modifies only STATE.md).
    6. Route: aggregate PASS → proceed to Task 5 (the consolidated visual checkpoint
       is now the only remaining ANIM-03/ANIM-04 item). Aggregate FAIL or
       INCONCLUSIVE (below quorum / inconclusive-heavy) → SKIP Task 5, leave
       ANIM-03/ANIM-04 unchecked, and record in STATE.md and the summary that fresh
       operator routing is required via `/gsd-plan-phase 4 --gaps` with
       04-UAT-SWEEP.json, the per-device JSONs and this plan's summary as the new
       evidence. A FAIL under the operator's own approved rule is a new operator
       decision, never an executor edit.
  </action>
  <verify>
    <automated>uv run python -c "import json; d = json.load(open('.planning/phases/04-animation-flow-control/04-UAT-SWEEP.json')); assert d['outcome'] in ('PASS', 'FAIL', 'INCONCLUSIVE', 'ENV-ERROR'); assert len(d['devices']) == 8; assert sum(1 for x in d['devices'] if x['role'] == 'reference') == 1"</automated>
  </verify>
  <acceptance_criteria>
    - 8 per-device JSONs + 04-UAT-SWEEP.json on disk with real measured data (never placeholders); Tiles II present as the single reference row, absent from N_valid and the PASS count
    - One attempt per device; harness byte-identical to its Task 3b commit throughout; no threshold/rule/roster edits after any result was seen
    - ANIM-04 evidence recorded for all three ceiling-class devices: chain dims, packets/frame vs the chain-dims-derived expectation, CopyFB ack RTT median/p95, expiries, A1/A2 dispositions
    - Every reachable device's restoration was attempted and its status recorded
    - STATE.md updated with the sweep outcome and routing; evidence commit strictly after the protocol commits; existing evidence files unmodified
  </acceptance_criteria>
  <done>The cross-device sweep evidence exists exactly as measured, aggregated exactly as the frozen approved rule dictates, with honest routing recorded for whichever outcome occurred.</done>
</task>

<task type="checkpoint:human-verify" gate="blocking">
  <name>Task 5: Consolidated operator visual verdict — My Office Ceiling Capsule (ANIM-03 smoothness + ANIM-04 geometry). Executes ONLY on Task 4 aggregate PASS</name>
  <what-built>
    The cross-device sweep PASSed under the operator-approved rule: ack-gated flow
    control certified across the healthy-radio roster, with framebuffer-path evidence
    on the three ceiling-class devices. This is the ONE remaining human-only item —
    the single consolidated visual ask that superseded 04-06 Task 2 and 04-07 Task 2:
    numbers cannot judge smoothness (spike 003: the ack-gated arm was the smoothest by
    eye) and cannot see striping (the row-aligned chunking fix on the exact 13-wide
    geometry the old code would have corrupted). If Task 4 did not PASS, this
    checkpoint is SKIPPED and the plan ends at Task 4's routing.
  </what-built>
  <how-to-verify>
    1. From your desk, look at My Office Ceiling Capsule (serial d073d587daab; use the
       IP its sweep session actually resolved — see 04-UAT-SWEEP-d073d587daab.json).
       The harness powers it on automatically.
    2. Run a single visual round:
       `uv run python .planning/phases/04-animation-flow-control/uat_ack_stream.py --ip <capsule-ip> --profile ceiling --rounds 1 --json-out /tmp/04-uat-capsule-visual.json`
       (visual-only artefact — not sweep evidence; the single-device mode was
       preserved for exactly this).
    3. Watch the full ~30 s gated round and judge BOTH criteria:
       a. Smoothness (ANIM-03): a continuously moving colour cycle at a steady
          ~20 FPS cadence — occasional single skipped frames are normal
          (latest-frame-wins), but NO multi-second freezes, stutter clusters, or
          persistent ~2 frames/s crawling.
       b. Geometry (ANIM-04): the colour cycle sweeps coherently across the whole
          13×26 panel — no horizontal bands out of place, no repeating/offset
          stripes, no regions stuck on stale colours.
    4. The device is restored to its as-found power and colour when the round ends.
  </how-to-verify>
  <action>
    Execute only if Task 4's aggregate outcome is PASS (otherwise record "skipped —
    aggregate not PASS" in the summary and end the plan at Task 4's routing). Present
    the how-to-verify steps with the Capsule's actual resolved IP substituted, then
    BLOCK until the operator responds. Record the verbatim verdict in the summary.

    On "approved": mark ANIM-03 AND ANIM-04 complete — REQUIREMENTS.md checkboxes
    ticked and Traceability rows set to Complete, ROADMAP Phase 4 plan bullets for
    04-06/04-07/04-12 reconciled to reflect the consolidation, STATE.md sequencing
    gate closed out — and commit (`docs(04-12): ...`, signed). Completion ownership
    moved to this task with the visual consolidation (04-11 precedent: completion is
    owned by the sole visual owner).

    On a described issue: change NOTHING — classify it (smoothness vs geometry; a
    geometry failure implicates the row-aligned chunking, a pacing failure implicates
    the D4-04 CopyFB attachment with its documented one-line probe_template_index
    fallback) and capture it verbatim as gap evidence for `/gsd-plan-phase 4 --gaps`.
  </action>
  <verify>
    <human-check>Operator has watched a full ~30 s gated round on the powered-on Capsule and given explicit verdicts on BOTH smoothness and geometry</human-check>
  </verify>
  <done>Either ANIM-03 and ANIM-04 are marked complete backed by the sweep evidence plus an explicit dual operator verdict, or the described issue is classified and captured verbatim for gap-closure — and in the non-PASS branch, the checkpoint is recorded as skipped with routing already set by Task 4.</done>
  <resume-signal>Type "approved" if the Capsule renders coherently AND smoothly (both visual criteria met), or describe exactly what you saw — striping/misplaced bands vs freezes/crawling route to different fixes</resume-signal>
</task>

</tasks>

## Artifacts this plan produces

| Artifact | Kind | Location |
|----------|------|----------|
| `04-SWEEP-DESIGN.md` — cross-device criterion design, aggregation derivation, Tiles ruling verbatim, complete proposed wording | Design brief | `.planning/phases/04-animation-flow-control/04-SWEEP-DESIGN.md` |
| `sweep_design.py` — exact-binomial aggregation statistics runner (stdlib only) | Reproducible analysis | `.planning/phases/04-animation-flow-control/sweep_design.py` |
| Operator decision on the complete final wording | Decision record (verbatim in summary) | `.planning/phases/04-animation-flow-control/04-12-SUMMARY.md` |
| Amended ROADMAP criteria 3+4, REQUIREMENTS ANIM-03+ANIM-04, 04-06/04-07 supersession notices | Governing documents | `.planning/ROADMAP.md`, `.planning/REQUIREMENTS.md`, phase plan files |
| Sweep-generalised harness (sweep modes, serial resolution, profile auto-selection, chain-dims packet expectation, state restoration) | Measurement instrument | `.planning/phases/04-animation-flow-control/uat_ack_stream.py` |
| 8 per-device paired-session evidence JSONs (serial-named; Tiles II labelled reference) | Evidence artefacts | `.planning/phases/04-animation-flow-control/04-UAT-SWEEP-<serial>.json` |
| `04-UAT-SWEEP.json` — sweep-level aggregate verdict | Evidence artefact | `.planning/phases/04-animation-flow-control/04-UAT-SWEEP.json` |
| ANIM-04 framebuffer evidence (chain dims, packets/frame vs derived expectation, CopyFB ack RTTs, A1/A2 dispositions) ×3 ceilings | Evidence (in per-device JSONs + summary) | per-device JSONs, `04-12-SUMMARY.md` |
| Consolidated operator visual verdict (smoothness + geometry, Capsule) | Human evidence (verbatim in summary) | `.planning/phases/04-animation-flow-control/04-12-SUMMARY.md` |

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| Harness → LAN fleet | Outbound streaming + query load against 7 PRODUCTION household devices plus 1 reference device — a new surface: prior UATs touched only quiesced test devices |
| Harness → device state | Power-on (ceiling profile) and colour overwrites on lights in occupied household rooms at an unsociable hour |

## STRIDE Threat Register

| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-04-26 | DoS (outbound, self-inflicted) | 8 sequential paired sessions (~45–55 min total; up to 160 pkt/s on ceiling frames) | low | mitigate | Sessions strictly sequential, never parallel; fixed rounds/durations; the shipped gate throttles under congestion; per-device reachability guard (ENV-ERROR, no streaming at an absent device); one attempt per device; HA polling disabled on all targets (operator confirmed) |
| T-04-27 | Tampering (unexpected household state change) | Power-on + colour overwrite on production lights at an unsociable hour | medium | mitigate | As-found capture/restore (device-level power + colour) in a finally block with per-device recorded status; tiles-profile sessions never power devices on (invisible measurement); ceiling power-on limited to session duration then restored; test pattern stays at the harness's existing 25% brightness; outdoor paths excluded by the operator; the visual device is in the operator's own office |
| T-04-28 | Repudiation (criterion cherry-picking / retry laundering) | Aggregation rule + sweep evidence | high | mitigate | Rule, roster, quorum and budget derived in a committed runner, operator-approved from complete wording, and committed strictly BEFORE any sweep data; one attempt per device removes the retry surface; INCONCLUSIVE/ENV-ERROR excluded from the denominator by the PRE-declared rule and always reported; Tiles II reference-only encoded in the harness, not in post-hoc judgement |
| T-04-29 | Tampering (evidence integrity) | Historical evidence files | low | mitigate | All sweep evidence goes to NEW serial-named files; 04-UAT-TILES*.json and prior JSONs never modified; 04-06/04-07 changes are append-only notice blocks; supersessions cite commits |

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

<verification>
- 04-SWEEP-DESIGN.md + sweep_design.py committed before the Task 2 checkpoint; runner exits 0 and reproduces every aggregation number in the brief
- Operator reply recorded verbatim; nothing applied before it
- ROADMAP criteria 3+4 and REQUIREMENTS ANIM-03+ANIM-04 carry the approved wording; checkboxes unchecked until Task 5 approval; 04-06/04-07 carry exactly one new supersession notice each
- Harness: sweep modes work, expected_packets_per_frame passes the three worked examples, ruff/pyright clean, per-device gate constants unchanged, src/ and tests/ untouched across all commits
- 8 per-device JSONs + 04-UAT-SWEEP.json on disk with real data; commit order design → application → evidence verified via git log
- STATE.md sequencing note updated with the sweep outcome and routing
</verification>

<success_criteria>
- The operator-approved cross-device criterion governs ANIM-03 and ANIM-04, with the Tiles ruling recorded verbatim and Tiles II as reference-only
- The sweep produced honest per-device and aggregate outcomes under rules frozen before data, with ANIM-04 framebuffer evidence captured on all three ceiling-class devices and every device restored to its as-found state
- On aggregate PASS + the consolidated dual visual approval: ANIM-03 and ANIM-04 marked complete — otherwise the honest outcome is recorded with fresh operator routing prepared, and neither requirement is checked
</success_criteria>

<output>
Create `.planning/phases/04-animation-flow-control/04-12-SUMMARY.md` when done, recording:
the operator's verbatim Task 2 reply, the applied wording commits, the per-device sweep
table (outcome, gated/blind pooled, Fisher p, ratio, validity, restoration status; chain
dims + packets/frame + CopyFB ack RTTs for the ceilings; Tiles II labelled REFERENCE),
the aggregate verdict with K/N_valid/quorum arithmetic, the Task 5 verbatim visual
verdict (or "skipped — aggregate not PASS"), commit hashes in order, and the routing.
</output>
