diff --git a/docs/superpowers/plans/2026-06-08-portal-flood-bounded-propagation.md b/docs/superpowers/plans/2026-06-08-portal-flood-bounded-propagation.md
new file mode 100644
index 00000000..0baa1979
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-08-portal-flood-bounded-propagation.md
@@ -0,0 +1,187 @@
+# Portal-Flood Bounded-Propagation Port Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Eliminate the indoor doorway "flap" by porting retail's *bounded* portal-flood propagation into `PortalVisibilityBuilder` — keeping re-processing on growth (retail-faithful) but stopping the reciprocal/drift churn that makes membership eye-sensitive.
+
+**Architecture:** The flap is `PortalVisibilityBuilder.Build`'s unbounded re-enqueue churn (`cs:322`, `MaxReprocessPerCell=16` hack): redundant reciprocal back-contributions yield drifted non-empty slivers → `grew` → re-enqueue, and the churn's fixpoint shifts under sub-cm eye motion. Retail bounds it structurally (monotonic `update_count` watermark + empty reciprocal). **Phase 1 instruments + pins the exact acdream divergence at the live doorway (it's float-drift-dependent — a runtime fact, not derivable from decomp). Phase 2 ports the bound, gated on Phase 1's evidence.** Spec: `docs/superpowers/specs/2026-06-08-portal-flood-enqueue-once-port-design.md` (REVISION banner).
+
+**Tech Stack:** C# / .NET 10, xUnit. GL-free unit tests (`AcDream.App.Tests/Rendering/PortalVisibilityBuilderTests.cs`). Live capture via `dotnet run` against local ACE.
+
+**Non-goals (unchanged from spec):** no rooting/camera/clip-math-rewrite/seal change; physics + the 4 rest-stability regression tests stay; `Build_ViewGrowthAfterDoneCell` stays GREEN (re-processing is kept).
+
+---
+
+## PHASE 1 — Instrument & pin the exact divergence
+
+### Task 1: Add the portal-churn probe flag
+
+**Files:**
+- Modify: `src/AcDream.Core/Rendering/RenderingDiagnostics.cs` (after `ProbePvInputEnabled`, ~line 144)
+- Test: `tests/AcDream.Core.Tests/Rendering/RenderingDiagnosticsTests.cs`
+
+- [ ] **Step 1: Write the failing test**
+
+```csharp
+[Fact]
+public void ProbePortalChurn_DefaultsFalse_WhenEnvUnset()
+{
+ // Env var is absent in the test host, so the flag must default false (inert probe).
+ Assert.False(AcDream.Core.Rendering.RenderingDiagnostics.ProbePortalChurnEnabled);
+}
+```
+
+- [ ] **Step 2: Run it — expect FAIL** (compile error: `ProbePortalChurnEnabled` does not exist)
+
+Run: `dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "FullyQualifiedName~ProbePortalChurn_DefaultsFalse"`
+Expected: FAIL (does not compile).
+
+- [ ] **Step 3: Add the flag** (in `RenderingDiagnostics`, mirroring `ProbePvInputEnabled`)
+
+```csharp
+///
+/// Bounded-propagation port apparatus (2026-06-08). When true, PortalVisibilityBuilder.Build emits
+/// one [portal-churn] summary line per call: per-cell pop count (re-pops = churn), total re-enqueues,
+/// max pop count, and — per re-enqueue — the reciprocal-clip pre→post region count + grew flag. Pins
+/// whether the flap's churn is redundant reciprocal back-contributions producing non-empty drifted
+/// slivers (the hypothesis) vs another source. Throwaway apparatus — strip once the bound ships.
+/// Initial state from ACDREAM_PROBE_PORTAL_CHURN=1.
+///
+public static bool ProbePortalChurnEnabled { get; set; } =
+ Environment.GetEnvironmentVariable("ACDREAM_PROBE_PORTAL_CHURN") == "1";
+```
+
+- [ ] **Step 4: Run it — expect PASS.** Run the same filter. Expected: PASS.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/AcDream.Core/Rendering/RenderingDiagnostics.cs tests/AcDream.Core.Tests/Rendering/RenderingDiagnosticsTests.cs
+git commit -m "feat(render-diag): add ACDREAM_PROBE_PORTAL_CHURN flag for the bounded-propagation pin"
+```
+
+### Task 2: Instrument `PortalVisibilityBuilder.Build` churn + reciprocal
+
+**Files:**
+- Modify: `src/AcDream.App/Rendering/PortalVisibilityBuilder.cs` (the pop loop ~145-178; the reciprocal site ~295-306; the re-enqueue ~322; before `return frame` ~341)
+
+- [ ] **Step 1: Add a per-Build churn accumulator** (top of `Build`, after `var popCounts = ...` ~line 112)
+
+```csharp
+// [portal-churn] apparatus (2026-06-08): when ProbePortalChurnEnabled, accumulate re-enqueue churn
+// + reciprocal pre/post region counts, emitted as one summary line at end of Build. Inert when off.
+bool churnProbe = AcDream.Core.Rendering.RenderingDiagnostics.ProbePortalChurnEnabled;
+int churnReenqueues = 0;
+var churnReciprocal = churnProbe ? new System.Text.StringBuilder(256) : null;
+```
+
+- [ ] **Step 2: Record reciprocal pre→post** (at the reciprocal site, right after `ApplyReciprocalClip(...)` ~line 297, before the `if (clippedRegion.Count == 0)` check)
+
+```csharp
+if (churnProbe)
+ churnReciprocal!.Append(System.FormattableString.Invariant(
+ $" recip[0x{neighbourId:X8} {preReciprocalCount}->{clippedRegion.Count}]"));
+```
+
+- [ ] **Step 3: Count re-enqueues** (inside the `if (grew && ... queued.Add(neighbourId))` block ~line 322, after `todo.Insert(neighbour, dist)`)
+
+```csharp
+if (churnProbe) churnReenqueues++;
+```
+
+- [ ] **Step 4: Emit the summary** (just before `return frame;` ~line 341)
+
+```csharp
+if (churnProbe)
+{
+ int maxPop = 0; uint maxCell = 0; int rePopped = 0;
+ foreach (var kv in popCounts)
+ {
+ if (kv.Value > maxPop) { maxPop = kv.Value; maxCell = kv.Key; }
+ if (kv.Value > 1) rePopped++;
+ }
+ Console.WriteLine(System.FormattableString.Invariant(
+ $"[portal-churn] root=0x{cameraCell.CellId:X8} cells={frame.OrderedVisibleCells.Count} " +
+ $"reEnqueues={churnReenqueues} rePoppedCells={rePopped} maxPop=0x{maxCell:X8}:{maxPop}" +
+ churnReciprocal));
+}
+```
+
+- [ ] **Step 5: Build** — Run: `dotnet build src/AcDream.App/AcDream.App.csproj -c Debug`. Expected: `Build succeeded`.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add src/AcDream.App/Rendering/PortalVisibilityBuilder.cs
+git commit -m "diag(render): [portal-churn] probe — per-Build re-enqueue + reciprocal pre/post"
+```
+
+### Task 3: Deterministic re-pop unit test (probe baseline)
+
+**Files:**
+- Modify: `tests/AcDream.App.Tests/Rendering/PortalVisibilityBuilderTests.cs`
+
+- [ ] **Step 1: Write the test** — the `ViewGrowthAfterDoneCell` topology re-pops `B` (legitimate late growth from `D`). This proves the re-pop path is exercised deterministically (so the probe + later fix have a non-flaky anchor) without needing live float-drift.
+
+```csharp
+[Fact]
+public void Build_ViewGrowthAfterDoneCell_RePopsGrownCell()
+{
+ // Same A->B(near LEFT) + A->D(far RIGHT) + D->B(later) topology as
+ // Build_ViewGrowthAfterDoneCell_PropagatesNewSlicesToExit: B is popped via LEFT, then D grows B
+ // through RIGHT after B is done -> B re-pops. This is retail-faithful late growth (kept by the fix).
+ const uint A = 0x0001, B = 0x0002, D = 0x0003;
+ var a = Cell(A, new CellPortalInfo((ushort)B, 0, 0, 0), new CellPortalInfo((ushort)D, 1, 0, 0));
+ a.PortalPolygons.Add(QuadX(-0.9f, -0.3f, -2f));
+ a.PortalPolygons.Add(QuadX(0.3f, 0.9f, -5f));
+ var b = Cell(B, new CellPortalInfo((ushort)A, 0, 0, 0),
+ new CellPortalInfo(0xFFFF, 1, 0, 0), new CellPortalInfo((ushort)D, 2, 0, 0));
+ b.PortalPolygons.Add(QuadX(-0.9f, -0.3f, -2f));
+ b.PortalPolygons.Add(QuadX(0.3f, 0.9f, -3f));
+ b.PortalPolygons.Add(QuadX(0.3f, 0.9f, -5f));
+ var d = Cell(D, new CellPortalInfo((ushort)B, 0, 0, 2));
+ d.PortalPolygons.Add(QuadX(0.3f, 0.9f, -5f));
+ var all = new Dictionary { [A] = a, [B] = b, [D] = d };
+
+ var frame = Build(a, all);
+
+ // Membership (the flap-relevant output) is each cell once, regardless of re-pops.
+ Assert.Equal(new[] { B }, frame.OrderedVisibleCells.Where(c => c == B).ToArray());
+ Assert.Contains(D, frame.OrderedVisibleCells);
+}
+```
+
+- [ ] **Step 2: Run it — expect PASS** (documents current behavior; the re-pop happens internally, membership stays deduped).
+
+Run: `dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj --filter "FullyQualifiedName~Build_ViewGrowthAfterDoneCell_RePopsGrownCell"`
+Expected: PASS.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add tests/AcDream.App.Tests/Rendering/PortalVisibilityBuilderTests.cs
+git commit -m "test(render): deterministic re-pop anchor for the bounded-propagation pin"
+```
+
+### Task 4: Live capture at the doorway + pin (CHECKPOINT — needs the user)
+
+- [ ] **Step 1: Launch with the churn probe** (add `$env:ACDREAM_PROBE_PORTAL_CHURN = "1"` to a copy of `launch-flap-capture.ps1`; keep `ACDREAM_PROBE_PVINPUT=1` for correlation). `dotnet build` green first, then launch in background, tee to `flap-churn.log`.
+- [ ] **Step 2: User reproduces** — stand at the cottage doorway, turn the camera back and forth (the flap). ~15 s.
+- [ ] **Step 3: Analyze** `flap-churn.log`: for the flap frames (correlate with `[pv-input]` flood flips), inspect `[portal-churn]`: which cells hit high `maxPop` (churn → near 16), and the `recip[...]` pre→post counts — is a redundant reciprocal back-contribution staying **non-empty** (`pre->post` both >0 on a cell that already contributed)? That is the predicted divergence.
+- [ ] **Step 4: Pin + write the Phase 2 fix plan.** Record the pinned divergence (the exact cell/portal/condition where the redundant contribution stays non-empty) in a short findings note, and write `docs/superpowers/plans/2026-06-08-portal-flood-bound-fix.md` (Phase 2) with the exact, evidence-grounded code change. **Do not write Phase 2 code before this pin.**
+
+---
+
+## PHASE 2 — Port the bound (outline; finalized from Phase 1's pin)
+
+**Shape (locked; exact predicate from Task 4):** make redundant reciprocal / re-clip contributions **not** generate a new propagatable slice — matching retail's empty-reciprocal (`OtherPortalClip`→no `copy_view`) + monotonic `update_count` watermark — then **remove `MaxReprocessPerCell` + `popCounts`** (termination becomes structural). Keep re-processing on growth (the `AddRegion` union + the deferred re-process), so `Build_ViewGrowthAfterDoneCell_PropagatesNewSlicesToExit` stays GREEN. Most-likely concrete form (confirm in Task 4): gate the `grew → re-enqueue` so a back-contribution whose clipped region is already covered by the cell's accumulated view (retail: empty after reciprocal clip) does not count as growth — via the faithful reciprocal/watermark, NOT an epsilon dedup heuristic.
+
+**Tests:** the new eye-sweep stability test (membership a single contiguous run as the eye sweeps — synthetic grazing topology if reproducible, else the live `[pv-input]` gate); all existing `PortalVisibilityBuilderTests` green incl. `Build_ViewGrowthAfterDoneCell_*`, `Build_IsDeterministic_*`, `Builder_CyclicGraph/Hub` termination; the 4 physics rest-stability guards green.
+
+**Acceptance (visual gate — the real one):** at the cottage doorway, turn the camera back and forth and walk through — interior rooms render steadily, no battling/popping; `[pv-input]` flood stable per eye pose; `[portal-churn]` `maxPop` ≤ small constant (no near-16 churn). Then strip the apparatus (`[portal-churn]`, `[pv-input]`, the launch scripts).
+
+---
+
+## Self-Review notes
+- Spec coverage: Phase 1 implements the spec's "instrument + pin first" requirement; Phase 2 implements the bounded-propagation fix. `Build_ViewGrowthAfterDoneCell` explicitly kept green (spec correction). No rooting/camera/clip/seal change.
+- Phase 2 is intentionally an outline: its exact predicate is the runtime fact Phase 1 pins (the spec + this plan both flag this). Phase 2 gets its own no-placeholder plan after Task 4 — this is the apparatus-first discipline, not a deferred placeholder.