---
phase: 04-animation-flow-control
plan: 11
type: execute
wave: 9
depends_on:
  - "04-10"
files_modified:
  - .planning/ROADMAP.md
  - .planning/REQUIREMENTS.md
  - .planning/STATE.md
  - .planning/phases/04-animation-flow-control/04-CRITERION-DESIGN.md
  - .planning/phases/04-animation-flow-control/criterion_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-TILES.json
autonomous: false
gap_closure: true
requirements:
  - ANIM-03

must_haves:
  truths:
    - "A committed criterion-design brief (04-CRITERION-DESIGN.md + criterion_design.py) derives every proposed parameter — ambient validity bound, delivered-ratio sanity floor, Fisher/ratio relative rule, clean escape, absolute ceiling — from the committed multi-session evidence with exact statistical power stated at the achievable per-arm sample sizes, and both files are committed BEFORE the operator checkpoint"
    - "The operator approved the complete final wording (ROADMAP criterion 3 replacement, REQUIREMENTS ANIM-03 replacement, harness gate contract) at a blocking checkpoint:decision BEFORE anything was applied — exactly one ask — and the approved-wording + harness commits strictly precede the evidence run commit (protocol-fixed-before-data)"
    - "The harness runs a paired same-session battery — ambient control, then alternating ack-gated and blind-fire streaming rounds with an IDENTICAL query prober in every block — with blind-fire constructed at instrument level only: src/ and tests/ untouched, the shipped Animator gains no flow-control toggle (D4-02)"
    - "Fresh paired evidence exists recorded exactly as the exit code says, with all four outcomes defined and honestly routed (PASS / FAIL / ENV-ERROR / INCONCLUSIVE), at most two attempts total, and any first-attempt FAIL or INCONCLUSIVE preserved under a distinct filename before the single re-run"
    - "No contradictory gates are left standing: ROADMAP criterion 3, REQUIREMENTS ANIM-03, the harness docstring/thresholds contract, the 04-06/04-07 annotations and the STATE sequencing note all agree with the operator-approved paired-relative criterion"
    - statement: "04-06 Task 2 remains the sole owner of the ANIM-03 visual checkpoint (the operator is asked for the visual verdict exactly once), executes only after this plan's headless paired PASS, and ANIM-03 completes only when both the paired-gate headless PASS and the operator visual verdict exist"
      verification: backstop
  artifacts:
    - path: ".planning/phases/04-animation-flow-control/04-CRITERION-DESIGN.md"
      provides: "evidence-derived paired-relative criterion design: parameter derivations, exact power, retrodiction table, and the complete proposed final wording (marked NOT applied until operator approval)"
    - path: ".planning/phases/04-animation-flow-control/criterion_design.py"
      provides: "reproducible statistics runner for every number in the design brief (Wilson bounds, Fisher power enumeration, retrodiction), committed before the checkpoint"
    - path: ".planning/phases/04-animation-flow-control/uat_ack_stream.py"
      provides: "the paired-mode harness: ambient control + alternating gated/blind-fire rounds, session-validity preconditions, relative + ceiling pass rule, 0/1/2/3 exit contract"
    - path: ".planning/phases/04-animation-flow-control/04-UAT-TILES.json"
      provides: "fresh paired ANIM-03 evidence (or an honestly recorded FAIL/ENV-ERROR/INCONCLUSIVE outcome)"
    - path: ".planning/ROADMAP.md"
      provides: "Phase 4 success criterion 3 in the operator-approved paired-relative form"
    - path: ".planning/REQUIREMENTS.md"
      provides: "ANIM-03 in the operator-approved paired-relative form (checkbox unchecked)"
  key_links:
    - from: ".planning/phases/04-animation-flow-control/uat_ack_stream.py"
      to: ".planning/phases/04-animation-flow-control/04-CRITERION-DESIGN.md"
      via: "gate constants and outcome contract applied verbatim from the design brief's operator-approved '## Final wording' section"
      pattern: "INCONCLUSIVE"
    - from: ".planning/phases/04-animation-flow-control/04-UAT-TILES.json"
      to: ".planning/phases/04-animation-flow-control/uat_ack_stream.py"
      via: "written by the paired fixed-contract harness (gated and blind arms in one session)"
      pattern: "blind"
    - from: ".planning/phases/04-animation-flow-control/04-CRITERION-DESIGN.md"
      to: ".planning/phases/04-animation-flow-control/criterion_design.py"
      via: "every parameter in the brief reproducible with one command against the committed evidence"
      pattern: "criterion_design.py"
---

<objective>
Redesign the ANIM-03 gate shape from absolute to PAIRED AND RELATIVE, per the operator's
fresh routing decision (verbatim reply "2", after the 04-10 amended absolute gate — the
operator's own 9.0%/round + 5.0% pooled — FAILed twice in a fresh session at pooled
11.21% then 5.74%). The evidence across sessions is unambiguous: the same code on the
same device measured pooled gated loss at 1.06% (04-08), 3.37% (04-06) and 8.30%
(04-10, both runs pooled) within one day — an ~8x session-to-session swing — so ANY
absolute threshold calibrated from one session produces coin-flip UATs (the 14.6%
blind-fire baseline was itself a single 41-query session).

Four moves, in commit order: (1) derive a concrete paired-relative criterion from the
committed multi-session evidence and write it up with exact power and a retrodiction
sanity table; (2) present the COMPLETE final wording to the operator at a blocking
checkpoint — nothing is applied without approval (hard rule this phase; both prior gap
cycles honoured it); (3) apply the approved wording to ROADMAP, REQUIREMENTS and the
harness — each UAT session now runs its own ambient control (prober only) and an
instrument-level blind-fire comparator alongside the gated arm; (4) run the paired UAT
fresh and record whatever it honestly measures, with INCONCLUSIVE (ENV-DEGRADED) now a
declared outcome distinct from PASS and FAIL.

Scope honours D4-02: the shipped Animator gets NO flow-control toggle, ever — blind-fire
is constructed in the phase-dir script only (spike 003's arm_blind mechanics via the
04-08 instrument machinery). D4-01's src/ tuning constants (gate 2 outstanding, ~1 s
expiry) are untouched — only the UAT judging rule changes. src/ and tests/ untouched.
Output: design brief + stats runner + amended governing documents + paired harness +
fresh paired 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/04-animation-flow-control/04-10-SUMMARY.md
@.planning/phases/04-animation-flow-control/04-GAP-ANALYSIS.md
@.planning/phases/04-animation-flow-control/04-08-SUMMARY.md
@.planning/phases/04-animation-flow-control/uat_ack_stream.py
@.planning/phases/04-animation-flow-control/uat_loss_investigation.py
</context>

<tasks>

<task type="auto">
  <name>Task 1: Derive the paired-relative criterion from the multi-session evidence (design brief + reproducible stats runner)</name>
  <files>.planning/phases/04-animation-flow-control/04-CRITERION-DESIGN.md, .planning/phases/04-animation-flow-control/criterion_design.py</files>
  <read_first>
    - .planning/phases/04-animation-flow-control/04-GAP-ANALYSIS.md — the prior derivation style, Wilson/Fisher framework, table (i) calibration inputs, and the R1–R5 rule discipline this brief inherits
    - .planning/phases/04-animation-flow-control/gap_analysis.py — `wilson_interval`, `rule_of_three_upper`, `fisher_exact_two_sided` (math.comb hypergeometric) and `binom_cdf` implementations to adapt with attribution
    - .planning/phases/04-animation-flow-control/04-UAT-TILES-amended-run1-FAIL.json and 04-UAT-TILES.json — session C per-round data (parse live from disk; these are the current bytes)
    - .planning/phases/04-animation-flow-control/04-UAT-TILES-run1-FAIL.json — session A run 1 (4/135); session A run 2 (5/132) bytes live at commit 5de88f6
    - .planning/phases/04-animation-flow-control/04-GAP-INVESTIGATION.json — session B five-arm aggregates (control 0/227, shipped 2/189, replica 9/185, sweep 1/210, fallback control 0/114 + shipped 4/84)
    - .planning/spikes/003-ack-paced-frames/summary-20260716-210408.json — the blind-fire baseline's raw record: arm "blind" query_n=41, query_loss_pct=14.6 (i.e. 6/41), plus photons 0/50 and the n=20 baseline block
    - .planning/phases/04-animation-flow-control/uat_ack_stream.py — current per-round query throughput reality (healthy rounds ~44–48 queries at 2 q/s; degraded rounds 30–41 because each lost query holds the prober for the 2 s timeout)
  </read_first>
  <action>
    Produce two committed artefacts BEFORE the operator checkpoint (04-09 discipline:
    the statistics runner is committed before its numbers are relied on; the brief adds
    no data and adjusts none).

    1. **criterion_design.py** (phase-dir, stdlib-only, zero new dependencies). Adapt
       `wilson_interval`, `rule_of_three_upper` and the math.comb hypergeometric Fisher
       machinery from gap_analysis.py with attribution comments, add a one-sided
       variant `fisher_one_sided(gated_lost, gated_ok, blind_lost, blind_ok)`
       (alternative: gated loss rate < blind loss rate; p = the hypergeometric lower
       tail P(X <= gated_lost) conditioning on total losses), and compute/print:

       a. **The multi-session evidence table.** Hard-code the per-session counts with a
          source citation comment per row (spike 003 blind 6/41 and photons 0/50;
          session A gated 9/267 pooled with run 1 4/135 on disk and run 2 5/132 at
          commit 5de88f6; session B control 0/227, shipped 2/189, replica 9/185,
          sweep 1/210, fallback control 0/114, fallback shipped 4/84; session C gated
          12/107 and 7/122). Where the source JSON is on disk, parse it and ASSERT the
          hard-coded counts match — the table is provably faithful to the evidence.
          Print each row with loss % and Wilson 95% CI, and print the session-level
          gated pooled rates (1.06% / 3.37% / 8.30%) as the ~8x variance demonstration.
       b. **Parameter derivations** (the proposed values, each printed with its rule):
          - Ambient validity bound 2.5%: both healthy-session control arms were
            lossless (0/227, 0/114; rule-of-three upper bounds 1.32% and 2.63%) — a
            session whose ambient prober-only loss exceeds 2.5% (~3 lost of ~120) is
            lossier than any healthy session's statistical ceiling and cannot certify
            anything. Minimum ambient sample n >= 100 (below that the bound has no
            resolution).
          - Delivered-ratio sanity floor 0.50 (session validity, NOT pass/fail): the
            worst gated round ever measured delivered 0.6033 (04-10 run 1 round 0, 21
            ack expiries) — 0.50 fires only on sessions more congested than anything
            in the evidence base. Above it, throttling is the flow control doing its
            job under ambient congestion, which is the FEATURE, not a defect.
          - Relative rule: one-sided Fisher exact p < 0.05 (alpha matches the 04-09 R3
            precedent) AND blind/gated point ratio >= 2.0 (conservative floor on
            "large improvement" — the pooled historical improvement is ~5.3x: gated
            2.78% pooled vs blind 14.6%).
          - Clean escape 2.5%: gated pooled loss at or below the healthy-ambient
            ceiling (same bound as V1) proves non-starvation directly even when the
            same-session blind-fire arm happens to be low — without it, a clean
            session could FAIL for lack of a disease to improve on.
          - Absolute ceiling 9.0% pooled gated loss: continuity with the operator's
            last-approved bound, now applied pooled; it excludes the worst measured
            session outright (session C run 1, 11.21% pooled, FAILs the ceiling
            regardless of any comparator — "relatively better but absurdly lossy"
            cannot pass); and it sits at ~0.62x the single-session blind baseline
            (whose own Wilson 95% lower bound on 6/41 is ~6.9% — print the exact
            value), so a ceiling-grazing run still needs the in-session Fisher win.
       c. **Exact power by enumeration** (no normal approximations): for per-arm n in
          {100, 120, 140}, enumerate X_g ~ Bin(n, p_g), X_b ~ Bin(n, p_b) and print
          P(relative rule fires), P(clean escape), and P(PASS overall, including the
          9.0% ceiling) at: (p_g=0.0278, p_b=0.146) — the historical rates; a
          uniformly degraded session (+3 points on both arms); and proportional
          scaling p_b = 5.25 * p_g at each observed session severity (0.0106, 0.0337,
          0.0574, 0.083). ADJUSTMENT RULE (declared now, not after seeing numbers): if
          P(PASS) at the historical rates with n=100 comes out below 0.85, raise the
          proposal's rounds-per-arm from 3 to 4 and recompute — the design must state
          power >= 0.85 at its declared sample size.
       d. **Retrodiction table**: apply the proposed rule to every historical session
          where its inputs exist. Expected shape (verify by computation): session B
          PASSes via the clean escape (1.06% <= 2.5%, ambient 0/227 valid); session C
          run 1 FAILs the ceiling outright (11.21% > 9.0%); the spike 003 session
          satisfies the relative rule (Fisher one-sided on 6/41 vs 0/50 — print p) but
          would be INCONCLUSIVE on ambient power (its baseline was n=20 < 100); and
          sessions A and C-run-2 are INDETERMINATE — no paired blind arm was run,
          which is precisely the gap this redesign closes. Honest note: the
          retrodiction cannot manufacture comparators that were never measured.

       Style gates: `uv run ruff format`, `uv run ruff check`, `uv run pyright` clean
       on the file; pre-commit hooks pass; Australian English; ASCII "--" dashes.
       Cross-check the Fisher machinery against the committed reference: the two-sided
       p on the 04-08 table (shipped [2, 187] vs replica [9, 176]) must reproduce
       0.0342, and the one-sided p on the same table must be positive and <= 0.0342.

    2. **04-CRITERION-DESIGN.md** — the decision brief, in the 04-GAP-ANALYSIS.md
       register. Every number reproducible with one command
       (`uv run python .planning/phases/04-animation-flow-control/criterion_design.py`).
       Required sections:
       - "## Why the absolute gate cannot work" — the multi-session table + the ~8x
         session variance + the single-session provenance of BOTH prior calibrations
         (spike 003's 0/50 and the 14.6% blind baseline's 6/41).
       - "## The paired-relative criterion" — the full rule, exactly as it will run:
         SESSION SHAPE (reachability probe unchanged; 60 s ambient control, prober
         only; then alternating gated/blind-fire 30 s rounds, gated first — G,B,G,B,…
         at 3 rounds per arm — with 10 s rests and the IDENTICAL DeviceConnection
         prober (max_retries=0, 2 q/s, 2 s timeout) in every block, so only the
         streaming treatment differs between arms; blind-fire is spike 003 arm_blind
         mechanics at instrument level: per-packet awaited sends of the animator's
         prebaked templates with the ack flag cleared, no probes, no gating — D4-02
         upheld, src/ untouched);
         SESSION VALIDITY (V1 ambient pooled loss <= 2.5%, V2 ambient n >= 100, V3
         every gated round delivered_ratio >= 0.50; any miss → ENV-DEGRADED →
         outcome INCONCLUSIVE, exit 3, never PASS, never FAIL);
         PASS RULE (session valid AND gated pooled <= 9.0% AND [gated pooled <= 2.5%
         OR (Fisher one-sided p < 0.05 AND blind/gated >= 2.0)]; ceiling profile
         additionally keeps its unchanged per-gated-round checks: 8 packets/frame and
         >= 1 ack RTT sample);
         OUTCOME CONTRACT (0 PASS / 1 FAIL / 2 ENV-ERROR / 3 INCONCLUSIVE, all
         defined, delivered_ratio recorded per round but no longer a pass/fail gate).
       - "## Derivations" — the per-parameter rules from 1(b), each tied to its
         evidence rows.
       - "## Statistical power" — the exact enumeration results from 1(c) and the
         declared per-arm sample expectation (~100–140 pooled queries per arm over 3
         rounds; state the adjustment-rule outcome if it fired).
       - "## Retrodiction" — the table from 1(d).
       - "## Expected wall time" — ~5.25 min per attempt (60 s ambient + 6 x 30 s
         rounds + 6 x 10 s rests + setup), at most 2 attempts → <= ~11 min of device
         time on the primary Tiles.
       - "## Final wording (PROPOSED — NOT applied; operator decision required)" —
         the complete replacement texts, verbatim-ready: (i) ROADMAP Phase 4 success
         criterion 3; (ii) REQUIREMENTS ANIM-03 bullet (checkbox stays unchecked);
         (iii) the harness gate contract summary (constants, validity rules, pass
         rule, exit codes). Draft starting point for (i), refine only for precision
         against the computed numbers, never for strength:

         > 3. Hardware UAT (standard Tiles): a paired same-session run — ambient control (prober only, no streaming), then alternating ack-gated and instrument-level blind-fire rounds under 20 FPS streaming — shows ack-gated concurrent-query loss is a large measured improvement over same-session blind-fire (one-sided Fisher exact p < 0.05 AND >= 2x lower, OR gated pooled loss <= 2.5%) within an absolute ceiling of 9.0% pooled gated loss; sessions failing validity (ambient loss > 2.5%, ambient n < 100, or any gated round delivered < 0.50) are ENV-DEGRADED → INCONCLUSIVE, never a pass or a fail (evidence-derived per 04-CRITERION-DESIGN.md across all 20 FPS shipped-path sessions; single-session blind-fire baseline 14.6% = 6/41) — over repeated rounds

         and for (ii):

         > - [ ] **ANIM-03**: Hardware validation on standard Tiles: under 20 FPS streaming, ack-gated
         >   concurrent-query loss is a large measured improvement over same-session instrument-level
         >   blind-fire (Fisher one-sided p < 0.05 and >= 2x lower, or gated pooled <= 2.5%) within a
         >   9.0% pooled absolute ceiling; ambient-degraded sessions are INCONCLUSIVE, never PASS or FAIL

       Nothing in this task edits ROADMAP.md, REQUIREMENTS.md, uat_ack_stream.py or
       src/ — the brief exists solely so the operator decides from concrete wording
       (04-09 precedent).

    Commit both files together with `git commit -s` (GPG-signed):
    `docs(04-11): derive paired-relative ANIM-03 criterion from multi-session evidence`.
    This commit MUST land before the Task 2 checkpoint is presented.
  </action>
  <verify>
    <automated>uv run ruff format --check .planning/phases/04-animation-flow-control/criterion_design.py && uv run ruff check .planning/phases/04-animation-flow-control/criterion_design.py && uv run pyright .planning/phases/04-animation-flow-control/criterion_design.py && uv run python .planning/phases/04-animation-flow-control/criterion_design.py > /dev/null && grep -q '## Final wording (PROPOSED' .planning/phases/04-animation-flow-control/04-CRITERION-DESIGN.md && grep -q '## Statistical power' .planning/phases/04-animation-flow-control/04-CRITERION-DESIGN.md && grep -q '## Retrodiction' .planning/phases/04-animation-flow-control/04-CRITERION-DESIGN.md && grep -q 'criterion_design.py' .planning/phases/04-animation-flow-control/04-CRITERION-DESIGN.md</automated>
  </verify>
  <acceptance_criteria>
    - criterion_design.py runs to completion (exit 0) with stdlib only, reproduces the 04-08 Fisher reference (two-sided p = 0.0342 on shipped [2,187] vs replica [9,176]), asserts its hard-coded counts against every on-disk source JSON, and is ruff/pyright clean
    - 04-CRITERION-DESIGN.md contains all seven required sections; every proposed parameter (2.5% ambient, n >= 100, 0.50 delivered sanity, alpha 0.05, ratio 2.0, 2.5% clean escape, 9.0% ceiling) carries a derivation tied to named evidence rows, never an unexplained number
    - Exact power is stated by enumeration at per-arm n in {100, 120, 140} for the declared rate scenarios, P(PASS) at the historical rates with the declared sample size is >= 0.85 (or the pre-declared adjustment rule fired and is recorded), and the expected per-arm sample (~100–140 queries over 3 rounds) plus the ~5.25 min/attempt wall time are stated
    - The retrodiction table shows: session B PASS (clean escape), session C run 1 FAIL (ceiling), spike 003 relative-rule satisfied but ambient-power INCONCLUSIVE, sessions A and C-run-2 honestly INDETERMINATE (no paired comparator existed)
    - The final-wording section is complete (ROADMAP text, REQUIREMENTS text, harness contract) and explicitly marked NOT applied pending the operator decision; ROADMAP.md, REQUIREMENTS.md, uat_ack_stream.py, src/ and tests/ are untouched by this task's commit
    - The commit is signed off (`git commit -s`) and precedes the Task 2 checkpoint
  </acceptance_criteria>
  <done>A committed, reproducible design brief exists in which every parameter of the paired-relative criterion is derived from the multi-session evidence with exact power at the achievable sample sizes — ready for the operator to approve, amend or reject from concrete wording.</done>
</task>

<task type="checkpoint:decision" gate="blocking">
  <name>Task 2: Operator approval of the complete paired-relative wording (blocking — nothing is applied without it)</name>
  <decision>Approve the paired-relative ANIM-03 criterion — the complete final wording for ROADMAP success criterion 3, REQUIREMENTS ANIM-03, and the harness gate contract — BEFORE anything is applied</decision>
  <context>
    The operator routed the twice-FAILed absolute gate to a paired-relative redesign
    (verbatim reply "2", recorded in this plan's objective). Criterion and threshold
    changes are operator decisions — the hard rule this phase; 04-09 and 04-10 both
    honoured it. This checkpoint is the exactly-one ask.

    Present IN THE CHECKPOINT MESSAGE ITSELF (not just by reference): the complete
    "## Final wording" section from 04-CRITERION-DESIGN.md verbatim (all three
    texts), the parameter table with its one-line derivations, the exact power
    numbers at the declared sample size, the retrodiction table, and the ~5.25
    min/attempt (max 2 attempts, <= ~11 min device time) wall-time statement. Also
    state plainly what changes for downstream plans: the per-round 9.0% / pooled
    5.0% / delivered 0.85 gates are replaced wholesale; delivered_ratio moves from
    pass/fail to a 0.50 session-validity sanity floor; INCONCLUSIVE (exit 3) becomes
    a declared outcome; 04-07 (Ceiling) inherits the paired shape with its
    ceiling-specific checks unchanged; 04-06 Task 2 stays the sole visual owner.

    If the operator amends any value or wording, the amendment is applied verbatim
    in Tasks 3–4 in place of the proposal, recorded verbatim in the summary, and the
    design brief is annotated (never rewritten) with the delta. If the operator
    rejects the shape outright, STOP after this task and surface the outcome — do
    not apply anything, do not run hardware.
  </context>
  <options>
    <option id="approve">
      <name>Approve the proposed wording as presented</name>
      <pros>Evidence-derived, power-stated, robust to the measured ~8x session variance; keeps honest fixed rules (validity and pass conditions declared before data)</pros>
      <cons>More moving parts than an absolute gate; each UAT costs ~5.25 min of device time per attempt instead of ~1.5 min</cons>
    </option>
    <option id="amend">
      <name>Approve with amendments (operator supplies the changed values/wording)</name>
      <pros>Operator retains full authority over every threshold; amendments applied verbatim with provenance</pros>
      <cons>Any amended parameter departs from the derived evidence basis — the summary must record it as operator-authored</cons>
    </option>
  </options>
  <read_first>
    - .planning/phases/04-animation-flow-control/04-CRITERION-DESIGN.md — the brief the operator decides from (present its "## Final wording", parameter derivations, "## Statistical power", "## Retrodiction" and "## Expected wall time" sections verbatim)
  </read_first>
  <action>
    Present the complete final wording and the supporting material listed in the
    context above, then BLOCK until the operator responds. Record the operator's
    reply VERBATIM in the 04-11 summary. This task applies nothing: ROADMAP.md,
    REQUIREMENTS.md, uat_ack_stream.py and src/ are untouched here in every branch —
    Task 3 applies the approved (or operator-amended) wording under its own commits.
    On "approve": proceed to Task 3 with the proposal as the governing text. On
    amendments: substitute them verbatim as the governing text, annotate (never
    rewrite) 04-CRITERION-DESIGN.md with the delta, and proceed. On rejection of the
    shape: STOP the plan, record the outcome and the exact next command
    (/gsd-plan-phase 4 --gaps with the design brief as evidence), and leave 04-06
    Task 2 and 04-07 gated.
  </action>
  <verify>
    <human-check>Operator has read the complete final wording (all three texts), the derivations, power and retrodiction, and given an explicit reply — approve, amend (with the amendments), or reject — recorded verbatim</human-check>
  </verify>
  <acceptance_criteria>
    - The operator's reply appears verbatim in the 04-11 summary alongside the exact wording they were shown
    - The checkpoint message contained the full final wording inline (not just a file reference), the parameter/derivation table, the exact power at the declared sample size, the retrodiction table and the wall-time statement — one ask, complete information
    - No file outside the summary (and, on amendment, the design-brief annotation) is modified by this task; Tasks 3–4 run only on approve/amend
  </acceptance_criteria>
  <done>The paired-relative criterion's fate was decided by the operator from complete concrete wording BEFORE anything was applied — never by an executor.</done>
  <resume-signal>Reply "approve", or supply the amended wording/values (applied verbatim in place of the proposal), or reject the shape (plan stops, nothing applied)</resume-signal>
</task>

<task type="auto">
  <name>Task 3: Apply the approved wording — governing documents, annotations, and the paired-mode harness</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-CRITERION-DESIGN.md — the "## Final wording" section as approved (or as amended by the operator at Task 2 — the approved text governs, verbatim)
    - .planning/ROADMAP.md — "### Phase 4: Animation Flow Control" Success Criteria list item 3 and the Phase 4 plan-list bullets for 04-06/04-07
    - .planning/REQUIREMENTS.md — the ANIM-03 bullet (two-space continuation indent; Traceability row stays "Pending")
    - .planning/phases/04-animation-flow-control/uat_ack_stream.py — every location the amendment touches: docstring (profiles, honest-reporting rules, exit contract), the pass-threshold constants block, query_prober, stream_round/run_round, _pooled_query_loss_pct, _evaluate, _thresholds, main()
    - .planning/phases/04-animation-flow-control/uat_loss_investigation.py — ReplicaStreamer (per-packet awaited sends via own UdpTransport, animator-private template reach with the spike licence) and run_control_block (prober-only block) to adapt with attribution
    - .planning/phases/04-animation-flow-control/criterion_design.py — fisher_one_sided to copy with attribution (phase-dir scripts stay standalone)
  </read_first>
  <action>
    Apply the operator-approved wording EXACTLY. Any deviation must be flagged in the
    summary, never silently reworded. Two commits, both strictly before Task 4's
    evidence commit (protocol-fixed-before-data, 04-08/04-10 discipline). Per D4-02,
    src/ and tests/ stay untouched in both commits; D4-01's src/ tuning constants
    (gate 2 outstanding, ~1 s expiry) do not change — only the UAT judging rule does.

    **Commit A — governing documents + annotations**
    (`docs(04-11): apply operator-approved paired-relative ANIM-03 criterion`):

    1. ROADMAP.md: in "### Phase 4: Animation Flow Control" → Success Criteria,
       replace list item 3 (the item beginning "3. Hardware UAT (standard Tiles):")
       with the approved replacement, reflowed onto a single line to match the list
       style (content verbatim — the only permitted deviation, 04-10 precedent).
       Refresh the two stale plan-list bullets: the 04-06 bullet should say Task 2
       (visual checkpoint) executes after 04-11's paired headless PASS; the 04-07
       bullet should say it inherits the paired-relative shared gate per the 04-11
       annotation (its old parenthetical names the superseded per-round/pooled
       percentages — replace that clause).
    2. REQUIREMENTS.md: replace the ANIM-03 bullet with the approved text, preserving
       the "- [ ]" checkbox (UNCHECKED — completion belongs to 04-06 Task 2) and the
       file's two-space continuation indent. Do NOT touch the Traceability table row
       (ANIM-03 stays "Pending").
    3. 04-06-PLAN.md: append a SECOND notice block immediately after the existing
       "AMENDMENT NOTICE (2026-07-17, applied by 04-10)" block (annotate, never
       rewrite; every other byte untouched), headed
       "AMENDMENT NOTICE (2026-07-17, applied by 04-11):" and stating: the operator
       routed the twice-FAILed 04-10 absolute gate to a paired-relative redesign
       (verbatim reply "2"); 04-11 owns the paired headless run (ambient control +
       gated vs same-session instrument-level blind-fire, per 04-CRITERION-DESIGN.md);
       the 04-10 notice's per-round/pooled percentages are superseded in turn; Task 2
       (operator visual verdict) REMAINS LIVE as the single owner of the ANIM-03
       visual checkpoint and executes only after 04-11's headless PASS, against the IP
       04-11's run actually used.
    4. 04-07-PLAN.md: append a matching second notice block after its existing 04-10
       notice: the shared harness now runs the paired-relative gate for BOTH profiles
       (ambient validity preconditions, Fisher/ratio/clean relative rule, 9.0% pooled
       gated ceiling, INCONCLUSIVE outcome); the 04-10 notice's per-round/pooled
       percentages and the delivered >= 0.85 pass/fail floor are superseded
       (delivered_ratio is now a 0.50 session-validity sanity floor, recorded per
       round); ceiling-specific criteria are UNCHANGED (8 packets/frame on gated
       rounds, >= 1 ack RTT sample per gated round, power-on + settle precondition);
       blind-fire rounds on the ceiling stream its full 8-packet frames unpaced at
       instrument level; the plan still executes only after 04-06 Task 2 approval.

    **Commit B — paired-mode harness**
    (`feat(04-11): rework uat_ack_stream into the paired ambient/gated/blind harness`):

    5. Constants: replace the pass-threshold block with the approved paired-gate
       constants (names fixed here so the results JSON schema is stable):
       `AMBIENT_CONTROL_SECONDS = 60.0`, `MAX_AMBIENT_LOSS_PCT = 2.5`,
       `MIN_AMBIENT_QUERIES = 100`, `MIN_GATED_DELIVERED_SANITY = 0.50`,
       `FISHER_ALPHA = 0.05`, `MIN_IMPROVEMENT_RATIO = 2.0`,
       `CLEAN_GATED_LOSS_PCT = 2.5`, `MAX_GATED_POOLED_LOSS_PCT = 9.0`,
       `INTER_ROUND_REST_SECONDS = 10.0` (values per the operator-approved wording —
       if amended at Task 2, use the approved values). Delete the three superseded
       pass/fail constants (the per-round loss gate, the pooled-only loss gate, and
       the 0.85 delivered floor). Keep `EXPECTED_CEILING_PACKETS_PER_FRAME`,
       `CEILING_MIN_ACK_RTT_SAMPLES` and the `BASELINE` dict (historical fact, not a
       threshold). Extend the provenance comment: redesigned 2026-07-17 by operator
       decision (routing verbatim "2" after the amended absolute gate FAILed twice;
       design in 04-CRITERION-DESIGN.md; approved at the 04-11 checkpoint) — fixed
       BEFORE any paired run, never after one.
    6. `BlindStreamer`: adapt ReplicaStreamer from uat_loss_investigation.py with an
       attribution comment — animator-private template/framebuffer/generator reach
       (measurement-only, 03-03/04-05 precedent), per-packet awaited sends on its own
       `UdpTransport`, and the ack flag CLEARED on every packet (spike 003 arm_blind:
       no probes, no gating, no ack collection — pure blind fire). No ack-related
       state at all.
    7. `run_ambient_block`: adapt run_control_block from uat_loss_investigation.py —
       prober only, no streaming, AMBIENT_CONTROL_SECONDS on the target device;
       returns the same queries_ok/queries_lost/loss/latency aggregates.
    8. `run_blind_round`: fresh `Animator.for_matrix()` for templates only, a
       BlindStreamer on its own transport, and the SAME `query_prober`
       (DeviceConnection, max_retries=0, 2 q/s, 2 s timeout) used by gated rounds —
       the query measurement instrument is byte-identical across all three blocks;
       only the streaming treatment differs. Record offered/sent counts and the query
       aggregates; tag the round dict with `"arm": "blind"`. Tag gated round dicts
       (existing run_round) with `"arm": "gated"`.
    9. Session flow in main(): reachability probe unchanged (exit 2 ENV-ERROR, no
       streaming, fallback contract unchanged) → chain info → ceiling power-on +
       settle unchanged → ambient block → alternating rounds gated-first
       (G,B,G,B,... at --rounds per arm, default 3) with INTER_ROUND_REST_SECONDS
       between every block. --rounds now means rounds PER ARM; document it in the
       flag help.
    10. `fisher_one_sided(gated_lost, gated_ok, blind_lost, blind_ok)`: copy from
        criterion_design.py with attribution (standalone script discipline).
    11. `_evaluate(ambient, gated_rounds, blind_rounds, profile)` → returns
        `(outcome, reasons)` where ambient is the ambient-block aggregate dict,
        the round lists are the arm-tagged per-round dicts, outcome is one of
        "PASS", "FAIL", "INCONCLUSIVE" and reasons is a list of human-readable
        strings for every rule that fired:
        - Validity first: ambient loss > MAX_AMBIENT_LOSS_PCT, ambient n <
          MIN_AMBIENT_QUERIES, or any gated round delivered_ratio <
          MIN_GATED_DELIVERED_SANITY → ("INCONCLUSIVE", reasons). INCONCLUSIVE is
          terminal — no pass/fail rule is evaluated on an invalid session.
        - Pass rule: gated pooled loss <= MAX_GATED_POOLED_LOSS_PCT AND (gated pooled
          <= CLEAN_GATED_LOSS_PCT OR (fisher_one_sided p < FISHER_ALPHA AND
          improvement ratio >= MIN_IMPROVEMENT_RATIO)). Improvement ratio =
          blind_loss_pct / gated_loss_pct, treated as satisfied when gated_lost == 0
          and blind_lost > 0; when both arms lost 0 the clean escape already decides.
        - Ceiling profile keeps its unchanged per-gated-round checks (packet shape,
          ack RTT samples) as pass/fail conditions.
    12. Exit contract: 0 PASS / 1 FAIL / 2 ENV-ERROR / 3 INCONCLUSIVE. Results JSON:
        ambient block, all rounds in execution order (each tagged with its arm),
        `gated_pooled` and `blind_pooled` ({lost, n, loss_pct}), `fisher_one_sided_p`,
        `improvement_ratio` (null when undefined), `session_valid`,
        `validity_reasons`, `outcome`, `pass`, and `_thresholds()` rewritten to the
        new constants. ENV-ERROR branch keeps its current shape plus the new
        thresholds block.
    13. Docstring rewrite — the written contract must match the code in every
        location: profile descriptions (tiles = the paired-relative ANIM-03 rule;
        ceiling = paired shape + unchanged 8-packet/CopyFB evidence), the session
        shape, the honest-reporting rules (append: the gate shape was redesigned
        once, 2026-07-17, by operator decision after the amended absolute gate FAILed
        twice — routing verbatim "2", design in 04-CRITERION-DESIGN.md, approved at
        the 04-11 checkpoint, fixed BEFORE any paired run; INCONCLUSIVE is a declared
        outcome, never silently retried), and the four-code exit contract. Keep the
        ASCII "--" dash style and Australian English throughout.
    14. Style gates: `uv run ruff format` + `uv run ruff check` clean;
        `uv run pyright .planning/phases/04-animation-flow-control/uat_ack_stream.py`
        0 errors; `--help` exits 0; pre-commit hooks (ruff-format, ruff, bandit,
        codespell, trailing-whitespace) pass at commit time. No new dependencies.

    If the operator amended values at Task 2, substitute them throughout (constants,
    docstring, synthetic-scenario numbers in the verify below) and record the
    substitution in the summary.
  </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 .planning/phases/04-animation-flow-control/uat_ack_stream.py --help > /dev/null && uv run python -c "
import sys; sys.path.insert(0, '.planning/phases/04-animation-flow-control')
import uat_ack_stream as m
g = lambda ok, lost, dr=0.9: {'arm': 'gated', 'queries_ok': ok, 'queries_lost': lost, 'delivered_ratio': dr, 'packet_shape_ok': True, 'ack_rtt_samples': 10}
b = lambda ok, lost: {'arm': 'blind', 'queries_ok': ok, 'queries_lost': lost}
amb = lambda ok, lost: {'queries_ok': ok, 'queries_lost': lost}
assert m.MAX_GATED_POOLED_LOSS_PCT == 9.0 and m.CLEAN_GATED_LOSS_PCT == 2.5 and m.MIN_GATED_DELIVERED_SANITY == 0.50
o, _ = m._evaluate(amb(114, 0), [g(38, 2)] * 3, [b(28, 6)] * 3, 'tiles'); assert o == 'PASS', o
o, _ = m._evaluate(amb(114, 0), [g(37, 3)] * 3, [b(36, 4)] * 3, 'tiles'); assert o == 'FAIL', o
o, _ = m._evaluate(amb(114, 0), [g(30, 4)] * 3, [b(20, 10)] * 3, 'tiles'); assert o == 'FAIL', o
o, _ = m._evaluate(amb(114, 0), [g(43, 0), g(43, 1), g(44, 0)], [b(43, 0), b(43, 1), b(44, 0)], 'tiles'); assert o == 'PASS', o
o, _ = m._evaluate(amb(110, 4), [g(38, 2)] * 3, [b(28, 6)] * 3, 'tiles'); assert o == 'INCONCLUSIVE', o
o, _ = m._evaluate(amb(114, 0), [g(38, 2), g(38, 2, 0.45), g(38, 2)], [b(28, 6)] * 3, 'tiles'); assert o == 'INCONCLUSIVE', o
p2 = m.fisher_one_sided(2, 187, 9, 176); assert 0.0 < p2 <= 0.0342, p2
print('paired gate verified: PASS via relative rule, FAIL on weak/ceiling, PASS via clean escape, INCONCLUSIVE on ambient and delivered validity')" && grep -q 'blind-fire' .planning/ROADMAP.md && grep -iq 'INCONCLUSIVE' .planning/REQUIREMENTS.md && grep -c 'AMENDMENT NOTICE (2026-07-17, applied by 04-11)' .planning/phases/04-animation-flow-control/04-06-PLAN.md | grep -qx 1 && grep -c 'AMENDMENT NOTICE (2026-07-17, applied by 04-11)' .planning/phases/04-animation-flow-control/04-07-PLAN.md | grep -qx 1 && grep -q '\- \[ \] \*\*ANIM-03\*\*' .planning/REQUIREMENTS.md</automated>
  </verify>
  <acceptance_criteria>
    - ROADMAP Phase 4 success criterion 3 carries the approved paired-relative wording (single-line reflow only); the criterion-3 line mentions the same-session blind-fire comparison and the INCONCLUSIVE/ENV-DEGRADED outcome; the previous absolute-only loss thresholds and the 0.85 delivered floor no longer appear as live gates anywhere in ROADMAP.md or REQUIREMENTS.md (the historical percentages may remain only inside superseded-notice annotations and completed-plan bullets)
    - REQUIREMENTS ANIM-03 reads the approved wording, its checkbox is still "- [ ]", and the Traceability row still says "Pending"
    - 04-06-PLAN.md and 04-07-PLAN.md each gained exactly one additional 04-11 notice block appended after their 04-10 notices; every other line of both plans is byte-identical to before this task
    - The harness functional check passes all six synthetic scenarios (relative-rule PASS; no-significance FAIL; ceiling FAIL despite a >= 2x relative win; clean-escape PASS with a low blind arm; ambient-validity INCONCLUSIVE; delivered-sanity INCONCLUSIVE) and fisher_one_sided reproduces the committed 04-08 reference table direction (0 < one-sided p <= two-sided 0.0342)
    - The blind arm exists only inside the phase-dir script (instrument-level: own transport, ack flag cleared on every packet, prober identical to the gated arm); `git diff` for both commits touches no src/ or tests/ paths and installs nothing (D4-02, D4-01 upheld)
    - The docstring's profiles, session shape, honest-reporting rules and 0/1/2/3 exit contract agree with the code in every location; ruff format --check, ruff check and pyright all clean; both commits signed off (`git commit -s`) and strictly before Task 4's evidence commit
  </acceptance_criteria>
  <done>The operator-approved paired-relative criterion is live in both governing documents and the harness, the two downstream UAT plans are annotated onto the new gate without churn, and the amendment is provably committed ahead of any run that will be judged by it.</done>
</task>

<task type="auto">
  <name>Task 4: Fresh paired ANIM-03 evidence (ambient + gated vs blind-fire, Tiles) + STATE gate-note update</name>
  <files>.planning/phases/04-animation-flow-control/04-UAT-TILES.json, .planning/STATE.md</files>
  <read_first>
    - .planning/phases/04-animation-flow-control/uat_ack_stream.py — as reworked by Task 3: the four-code exit contract this task interprets strictly
    - .planning/phases/04-animation-flow-control/04-CRITERION-DESIGN.md — the approved rule and the expected wall time (~5.25 min per attempt)
    - .planning/STATE.md — the "UAT sequencing gate" bullet under Blockers/Concerns that this task replaces
  </read_first>
  <action>
    Fixed rules for this run — approved at Task 2, committed by Task 3, and NEVER
    adjusted after a run regardless of outcome. Confirm before running that the Task 1
    and both Task 3 commits are in history (`git log --oneline -4`) — the protocol
    must precede the data.

    ATTEMPT BUDGET (declared now): at most TWO attempts total, whatever the outcome
    mix. Every attempt's JSON is preserved; the final attempt's outcome governs the
    routing. No third attempt exists — re-running until the environment cooperates is
    the dishonesty this redesign eliminates.

    1. Run (expect ~5.25 min):
       `uv run python .planning/phases/04-animation-flow-control/uat_ack_stream.py
       --ip 192.168.19.243 --profile tiles --rounds 3
       --json-out .planning/phases/04-animation-flow-control/04-UAT-TILES.json`.
       This intentionally overwrites the amended-absolute-gate FAIL evidence in
       place; the superseded bytes remain at commit d154ccf and must be cited in the
       summary (04-10 precedent: supersede in place, cite the commit).
    2. Exit 2 (ENV-ERROR) on the primary: retry ONCE with `--ip 192.168.18.62`
       (fallback on ENV-ERROR ONLY — never on FAIL, never on INCONCLUSIVE). If both
       IPs return ENV-ERROR: commit the recorded ENV-ERROR JSON, update the STATE
       gate note with the outcome, and STOP — surface the blocker; never fabricate,
       never stream at an absent device.
    3. Exit 3 (INCONCLUSIVE — ENV-DEGRADED): a declared outcome, not a failure and
       not a retry loop. Preserve the JSON as
       .planning/phases/04-animation-flow-control/04-UAT-TILES-paired-run1-INCONCLUSIVE.json,
       wait at least 5 minutes (cool-down — the point of validity is that the
       environment, not the gate, was the problem), then use the second and final
       attempt with identical flags. If the second attempt is also INCONCLUSIVE:
       commit both JSONs, leave 04-06 Task 2 and 04-07 gated, update the STATE note
       to say two consecutive sessions failed validity and the operator should choose
       when to re-attempt (a later session is legitimate — the criterion itself needs
       no new decision), and stop.
    4. Exit 1 (FAIL): a genuine signal under the operator's own approved rule. First
       preserve the evidence as
       .planning/phases/04-animation-flow-control/04-UAT-TILES-paired-run1-FAIL.json,
       then use the second and final attempt with identical flags (transient
       rule-out, 04-06/04-10 precedent). If the second attempt also FAILs: commit
       BOTH JSONs, flag loudly in the summary with the gated/blind pooled figures,
       Fisher p, improvement ratio and ceiling status, leave the ANIM-03 checkbox
       unchecked, leave 04-06 Task 2 and 04-07 gated, and update the STATE note: the
       paired gate itself FAILed and fresh operator routing is required
       (`/gsd-plan-phase 4 --gaps` with the new evidence). Rules are NEVER adjusted —
       a FAIL under the approved rule is a new operator decision, not an executor
       edit.
    5. Exit 0 (PASS): record in the summary — ambient loss and n (vs the validity
       bounds), per-round and pooled gated vs blind loss (vs the clean escape, ratio
       and ceiling), fisher_one_sided_p (vs alpha), improvement_ratio, delivered
       ratios per gated round (vs the 0.50 sanity floor, noting these are recorded
       evidence, not pass/fail), and ack RTT median/p95 vs the ~98/150 ms spike
       reference.
    6. Update .planning/STATE.md: replace the body of the "UAT sequencing gate"
       bullet under Blockers/Concerns with the actual outcome. On PASS state: the
       paired-relative criterion is applied (ROADMAP criterion 3, REQUIREMENTS
       ANIM-03, uat_ack_stream.py; approval at the 04-11 checkpoint; commits in
       04-11-SUMMARY.md) and the fresh paired run PASSed (04-UAT-TILES.json, exit 0);
       04-06 Task 2 (Tiles visual checkpoint — the single remaining ANIM-03 item,
       operator asked exactly once) is now UNGATED and executes next via
       `/gsd-execute-phase 4` resuming 04-06, against the IP this run actually used;
       04-07 (Ceiling) unblocks after 04-06 Task 2 approval, inheriting the paired
       shared gate; ANIM-03's checkbox flips only at 04-06 Task 2 completion. On
       FAIL / ENV-ERROR / INCONCLUSIVE keep the gate in force and state the honest
       outcome and routing from steps 2–4 instead. Do NOT mark ANIM-03 complete in
       REQUIREMENTS.md in ANY outcome — completion is owned by 04-06 Task 2
       (04-05/04-08/04-09/04-10 precedent).
    7. Commit evidence + STATE together with `git commit -s`:
       `test(04-11): record paired ANIM-03 evidence under the operator-approved relative gate`
       (adjust the verb honestly per outcome). This commit must be strictly after
       both Task 3 commits.
  </action>
  <verify>
    <automated>test -f .planning/phases/04-animation-flow-control/04-UAT-TILES.json && uv run python -c "
import json
d = json.load(open('.planning/phases/04-animation-flow-control/04-UAT-TILES.json'))
t = d['thresholds']
assert 'max_gated_pooled_loss_pct' in t and 'fisher_alpha' in t and 'max_ambient_loss_pct' in t, t
assert d['outcome'] in ('PASS', 'FAIL', 'ENV-ERROR', 'INCONCLUSIVE'), d['outcome']
print('paired-gate evidence on disk:', d['outcome'])"</automated>
  </verify>
  <acceptance_criteria>
    - 04-UAT-TILES.json exists with real measured data (or a recorded ENV-ERROR), its thresholds block records the paired-gate constants, and on a completed session it contains the ambient block, arm-tagged rounds, gated/blind pooled counts, fisher_one_sided_p and improvement_ratio — never placeholder numbers
    - `git log` shows the Task 1 design commit and both Task 3 application commits strictly BEFORE this task's evidence commit; the superseded absolute-gate evidence is cited at commit d154ccf
    - At most two attempts were made in total; any first-attempt FAIL or INCONCLUSIVE is preserved under its distinct filename before the single re-run; the fallback IP was used only on a primary ENV-ERROR; no thresholds or wording were touched after any run
    - The summary records the outcome with the full figure set from step 5 (or the honest ENV-ERROR/INCONCLUSIVE/FAIL routing from steps 2–4), and the STATE "UAT sequencing gate" bullet reflects the actual outcome including 04-06 Task 2's status as sole visual-checkpoint owner and 04-07's inheritance
    - REQUIREMENTS ANIM-03's checkbox remains unchecked in every outcome; src/ and tests/ untouched across all of this plan's commits
  </acceptance_criteria>
  <done>ANIM-03's headless evidence is re-established under a criterion that measures the gate against its own session's environment — PASS clearing the way for 04-06 Task 2 as the final ANIM-03 item, or a loudly-flagged FAIL/ENV-ERROR/INCONCLUSIVE routed honestly — with the approved protocol provably fixed in history before the data.</done>
</task>

</tasks>

## Sequencing after this plan (gate resolution)

1. **04-11 (this plan)** designs the paired-relative criterion, obtains operator
   approval BEFORE application, applies it, and owns the paired headless run —
   superseding the 04-10 amended-absolute-gate evidence in place (bytes remain at
   d154ccf; nothing rewritten).
2. **On headless PASS → 04-06 resumes at Task 2 only** (`/gsd-execute-phase 4`): the
   operator visual-smoothness checkpoint, the single time the operator is asked for
   the Tiles visual verdict, against the IP this plan's run actually used.
3. **ANIM-03 completes** only when both exist: this plan's paired headless PASS AND
   the 04-06 Task 2 operator approval. The REQUIREMENTS checkbox and Traceability row
   flip at 04-06 Task 2 completion — owned by 04-06's execution, not this plan.
4. **04-07 (Ceiling, ANIM-04)** unblocks after 04-06 completes, inheriting the
   paired-relative shared gate (annotated in Task 3) with its ceiling-specific
   criteria unchanged.
5. **On headless FAIL**: 04-06 Task 2 and 04-07 stay gated; fresh operator routing.
   **On INCONCLUSIVE ×2**: gates stay; the operator chooses when to re-attempt in a
   later session — the criterion itself needs no new decision. **On ENV-ERROR ×2**:
   gates stay; environment blocker surfaced. Rules are never adjusted by executors in
   any branch.

## Artifacts this plan produces

| Artifact | Kind | Location |
|----------|------|----------|
| Paired-relative criterion design brief (derivations, exact power, retrodiction, proposed final wording) | Decision brief | `.planning/phases/04-animation-flow-control/04-CRITERION-DESIGN.md` |
| Reproducible statistics runner (committed before the checkpoint) | Analysis instrument | `.planning/phases/04-animation-flow-control/criterion_design.py` |
| Operator approval of the complete final wording (blocking checkpoint, exactly one ask) | Decision record | 04-11-SUMMARY.md (verbatim reply) |
| Amended ROADMAP Phase 4 success criterion 3 (operator-approved paired-relative wording) | Governing document amendment | `.planning/ROADMAP.md` |
| Amended REQUIREMENTS ANIM-03 (operator-approved wording, checkbox unchecked) | Governing document amendment | `.planning/REQUIREMENTS.md` |
| Second-generation supersession notices on the downstream UAT plans | Plan annotations | `.planning/phases/04-animation-flow-control/04-06-PLAN.md`, `04-07-PLAN.md` |
| Paired-mode harness: ambient control + alternating gated/blind-fire rounds, validity preconditions, relative + ceiling rule, 0/1/2/3 exit contract | UAT instrument rework | `.planning/phases/04-animation-flow-control/uat_ack_stream.py` |
| Fresh paired evidence (first-attempt FAIL/INCONCLUSIVE preserved as `04-UAT-TILES-paired-run1-FAIL.json` / `04-UAT-TILES-paired-run1-INCONCLUSIVE.json`) | Evidence artefact | `.planning/phases/04-animation-flow-control/04-UAT-TILES.json` |
| Updated "UAT sequencing gate" note reflecting the post-run state | State record | `.planning/STATE.md` |

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| Harness → LAN Tiles | Outbound streaming + query load against user-owned quiesced test devices (192.168.19.243 primary, 192.168.18.62 fallback), now including deliberately UNPACED blind-fire rounds |
| Operator decision → repo gates | A criterion redesign is transcribed into gate-bearing documents and code |
| Evidence → derived thresholds | The design brief turns committed measurements into the parameters that will judge future runs |

## STRIDE Threat Register

| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-04-23 | Tampering | Criterion redesign (parameter cherry-picking to guarantee a pass) | medium | mitigate | Every parameter derived by a stated rule from committed evidence, reproducible via criterion_design.py committed BEFORE the checkpoint; power adjustment rule pre-declared; blocking operator approval of the complete wording before application; approved wording applied verbatim; rules never adjusted post-run |
| T-04-24 | DoS (outbound, self-inflicted) | Blind-fire rounds intentionally stream unpaced (~80 pkt/s Tiles, ~160 pkt/s ceiling profile) plus ambient/gated blocks | low | mitigate | Fixed 30 s rounds, 3 per arm, 10 s rests, 60 s ambient; reachability guard prevents streaming at an absent device; quiesced test devices; hard two-attempt budget; fallback IP only on ENV-ERROR |
| T-04-25 | Repudiation | Overwriting 04-UAT-TILES.json; INCONCLUSIVE as a potential retry laundering channel | medium | mitigate | Superseded absolute-gate bytes preserved in git history (commit d154ccf) and cited; every attempt's JSON preserved (distinct first-attempt filenames); attempt budget of two declared in the plan before any run; INCONCLUSIVE is a recorded outcome with its own exit code, never silently retried |
| T-04-26 | Spoofing | Unauthenticated LAN UDP responses to the prober/ack paths | low | accept | ASVS L1, measurement-only instrument on the operator's own trusted test LAN (04-05/04-08 precedent); source/sequence matching already filters stray datagrams |

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

<verification>
- 04-CRITERION-DESIGN.md + criterion_design.py committed before the checkpoint; every parameter evidence-derived and reproducible; exact power stated at the achievable per-arm sample sizes
- Operator approval recorded verbatim at the blocking checkpoint BEFORE any application; approved wording live in ROADMAP criterion 3 and REQUIREMENTS ANIM-03 with no absolute-only gate left standing as live
- uat_ack_stream.py runs the paired session (ambient + alternating gated/blind with an identical prober), enforces validity-then-pass evaluation with the 0/1/2/3 contract, passes the six synthetic gate scenarios, and is ruff/pyright clean
- Fresh 04-UAT-TILES.json on disk whose thresholds block records the paired-gate constants, outcome recorded exactly as the exit code said; design/application commits strictly precede the evidence commit in `git log`
- `git diff` across all plan commits touches no src/ or tests/ paths and installs nothing (D4-02/D4-01); 04-06/04-07 annotated (never rewritten); STATE gate note updated; ANIM-03 checkbox still unchecked pending 04-06 Task 2
</verification>

<success_criteria>
- The operator's paired-and-relative routing decision (verbatim "2") is fully realised: a criterion robust to the measured ~8x session-to-session variance — same-session blind-fire comparator, ambient validity precondition, sane absolute ceiling, INCONCLUSIVE as a declared outcome — approved by the operator from complete concrete wording before anything was applied, applied verbatim with the protocol provably fixed before the data, and exercised once against real hardware with every outcome honestly routed and exactly one contradiction-free path forward: 04-06 Task 2 (sole visual owner) then 04-07
</success_criteria>

<output>
Create `.planning/phases/04-animation-flow-control/04-11-SUMMARY.md` when done, recording:
the design/application commit hashes and their order relative to the evidence commit, the
operator's checkpoint reply VERBATIM (and any amendments applied in place of the
proposal), the exact power numbers the design declared, the run's exit code with the full
figure set (ambient, gated vs blind pooled, Fisher p, improvement ratio, ceiling status,
delivered ratios as recorded evidence), the d154ccf supersession citation, the attempt
count with any preserved first-attempt filenames, and the updated STATE gate note.
</output>
