diff --git a/app/globals.css b/app/globals.css
index e116f86..e9ce74e 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -201,6 +201,7 @@ button, input, textarea, select { font: inherit; }
.pd-border-emerald { border-color: #a7f3d0; }.pd-border-indigo { border-color: #c7d2fe; }.pd-border-blue { border-color: #bfdbfe; }.pd-border-amber { border-color: #fde68a; }.pd-border-purple { border-color: #e9d5ff; }.pd-border-red { border-color: #fecaca; }.pd-border-slate { border-color: #cbd5e1; }
.pd-dot-emerald { background: #10b981; }.pd-dot-indigo { background: #6366f1; }.pd-dot-blue { background: #3b82f6; }.pd-dot-amber { background: #f59e0b; }.pd-dot-purple { background: #a855f7; }.pd-dot-red { background: #ef4444; }.pd-dot-slate { background: #64748b; }
.pd-mobile-nav { display: none; }
+.pd-sr-only { border: 0 !important; clip: rect(0,0,0,0) !important; height: 1px !important; margin: -1px !important; overflow: hidden !important; padding: 0 !important; position: absolute !important; white-space: nowrap !important; width: 1px !important; }
@media (max-width: 1200px) { .pd-stat-grid { grid-template-columns: repeat(2,minmax(0,1fr)); } .pd-dashboard-grid { grid-template-columns: 1fr; } }
@media (max-width: 900px) { .pd-shell { display: block; padding-bottom: 86px; } .pd-sidebar { display: none; } .pd-mobile-brand { display: flex; } .pd-topbar { align-items: stretch; flex-direction: column; } .pd-top-actions { justify-content: space-between; } .pd-header-row { align-items: flex-start; flex-direction: column; } .pd-mobile-nav { align-items: center; background: rgba(255,255,255,.94); border-top: 1px solid #e2e8f0; bottom: 0; display: grid; grid-template-columns: repeat(5,1fr); left: 0; padding: 10px 10px calc(env(safe-area-inset-bottom) + .75rem); position: fixed; right: 0; z-index: 30; } .pd-mobile-nav button { background: transparent; border: 0; color: #64748b; display: grid; font-size: .72rem; font-weight: 900; gap: 3px; place-items: center; } .pd-mobile-nav span { position: relative; } .pd-mobile-nav i { background: #f59e0b; border-radius: 999px; height: 7px; position: absolute; right: -5px; top: -3px; width: 7px; } }
@media (max-width: 620px) { .pd-content, .pd-topbar { padding: 18px; } .pd-stat-grid, .pd-mini-grid { grid-template-columns: 1fr; } .pd-hero-actions, .pd-badges { align-items: stretch; flex-direction: column; } .pd-primary-btn, .pd-secondary-btn, .pd-evidence { width: 100%; } .pd-event-title { align-items: flex-start; flex-direction: column; gap: 2px; } }
diff --git a/components/pandora/MobileBottomNav.tsx b/components/pandora/MobileBottomNav.tsx
index 7040a2b..eb0df40 100644
--- a/components/pandora/MobileBottomNav.tsx
+++ b/components/pandora/MobileBottomNav.tsx
@@ -1,4 +1,4 @@
import { Brain, Gauge, History, ListChecks, MoreHorizontal } from "lucide-react";
import { mobileNavItems } from "./nav-items";
const icons = [Gauge, History, ListChecks, Brain, MoreHorizontal];
-export function MobileBottomNav({ activeNav, onNavChange }: { activeNav: string; onNavChange: (item: string) => void }) { return ; }
+export function MobileBottomNav({ activeNav, onNavChange }: { activeNav: string; onNavChange: (item: string) => void }) { return ; }
diff --git a/docs/roadmap.md b/docs/roadmap.md
new file mode 100644
index 0000000..8ac3045
--- /dev/null
+++ b/docs/roadmap.md
@@ -0,0 +1,66 @@
+# Pandora Memory Engine — Roadmap
+
+Last updated: 2026-07-15.
+
+This is the committed roadmap. Earlier "roadmap Sprint 1" / "roadmap #9" references in commits
+and PRs (#123, #124, #125) pointed at a numbered list that was never checked into the repo;
+this document replaces it. Update this file when a stage lands, with evidence, per
+`skills/phase-status-ledger`.
+
+## Status snapshot (evidence-backed)
+
+| Stage | Status | Evidence |
+|---|---|---|
+| Phase 1–3F (foundation → append proof → readback/browser/verification/closeout) | complete | merged PRs #93–#97, phase docs, production routes |
+| Phase 4A proposals + daily ChatGPT bridge | complete | migrations `20260626040000`, `20260626041000` applied |
+| Phase 4B Pandora MCP (13 tools, error envelope) | complete | PR #121, #125; `lib/services/pandora-mcp-server.ts` |
+| Phase 4C adaptive memory intelligence | complete | migration `20260627040000` applied; PR #122 |
+| Phase 5A autopilot capture / 5B candidate review / 5C compaction | complete | migrations applied; `/admin/memory/candidates`; supersede smoke 2026-07-03 |
+| Phase 5D scoring/pruning | partial | code + migration `20260629000000` applied; dry-runs NOT yet run/reviewed |
+| Phase 6A operating brain + 6A.2 smoke | complete | migration `20260701000000`; CI run 28467956363; Vercel READY |
+| Phase 6B project context engine | complete | migration `20260701010000`; `/operating/projects` |
+| Ops track: dashboard, verification console, action center, read-only runner | complete | PRs #131–#135 merged and deployed |
+| Shadow context pack lab | complete | PR #136; migration applied 2026-07-15 (`20260715200130`) |
+| Shadow pack preflight | complete | PR #137; migration applied 2026-07-15 (`20260715200149`) |
+| Promotion request board (plans only, no execution) | complete | PR #138 merged 2026-07-15; migration `20260715200514`; Vercel READY on `22b8b1a` |
+
+2026-07-15 operations note: five merged migrations had never been applied to production
+(adaptive profile versioning columns, operator action center, runner statuses, shadow lab,
+preflight). They were reviewed and applied on 2026-07-15 as versions `20260715200029`–
+`20260715200149`, and the local files were renamed to match the ledger. Stale duplicate
+PRs #96, #99, #118, #119 were closed with evidence comments.
+
+## Next (queued, in order)
+
+1. **Promotion executor** — the gated, human-approved path that promotes an approved shadow
+ candidate to the active master. Chain: lab → preflight → request board (all live) →
+ execution (NOT built). Requires its own PR, migration/policy review, dry-run proof, and
+ explicit production approval. Until then every surface must keep saying
+ "execution unavailable".
+2. **Phase 5D closure** per the execution rule in `CLAUDE.md`: protected dry-runs for
+ `real_life` and `au`, human review of dry-run output, explicit approval before any
+ `dryRun:false` run, post-run database verification. Pruning stays review-only regardless
+ (`lib/config/phase-5d-config.ts` hard-codes `review_only`).
+3. **Checkpoint follow-ups** from `docs/pandora-production-checkpoint-2026-07-03.md`:
+ the one-active-master-pack regression test and the mobile Queue-dot accessibility polish
+ landed 2026-07-15; the live dashboard data route landed via PRs #131/#132.
+
+## Gated ladder (requires explicit approval to start)
+
+- **Phase 6C prediction** — blocked until project context is manually verified in production
+ (guardrail in `docs/phase-6b-project-context-engine.md`). After that, in rough order of
+ risk: Gmail/Calendar connectors, LLM-based prediction, intervention engine, autonomous
+ actions.
+- **Capability gates still off by design**: model calls, embeddings, semantic retrieval,
+ GPT Actions enablement, public MCP expansion, batch/automatic memory writes, pruning
+ execution. Each flips only with explicit instruction, review, and deployed proof.
+- **Unbuilt tail of `docs/codex-execution-plan.md`** (the original 36-task V1 plan): AU
+ continuity engine (canon guard, scene aftermath, retcon manager, character/relationship
+ state APIs), real-life analyzers (`/api/real/*`), AI routes (`/api/ai/*`), full AU/canon
+ UI. Never cancelled in writing — adopt into the ladder or park explicitly.
+
+## Housekeeping decisions pending
+
+- The production Supabase project also contains ~25 `plp_*` tables (bookings/resort) whose
+ migrations are not in this repo. Decide whether to split that project into its own
+ database per `17-pandora-architecture-boundary`.
diff --git a/supabase/migrations/20260702000000_adaptive_profile_versioning_columns.sql b/supabase/migrations/20260715200029_adaptive_profile_versioning_columns.sql
similarity index 100%
rename from supabase/migrations/20260702000000_adaptive_profile_versioning_columns.sql
rename to supabase/migrations/20260715200029_adaptive_profile_versioning_columns.sql
diff --git a/supabase/migrations/20260703000000_pandora_operator_action_center.sql b/supabase/migrations/20260715200058_pandora_operator_action_center.sql
similarity index 100%
rename from supabase/migrations/20260703000000_pandora_operator_action_center.sql
rename to supabase/migrations/20260715200058_pandora_operator_action_center.sql
diff --git a/supabase/migrations/20260703010000_pandora_operator_action_runner_statuses.sql b/supabase/migrations/20260715200108_pandora_operator_action_runner_statuses.sql
similarity index 100%
rename from supabase/migrations/20260703010000_pandora_operator_action_runner_statuses.sql
rename to supabase/migrations/20260715200108_pandora_operator_action_runner_statuses.sql
diff --git a/supabase/migrations/20260703020000_pandora_shadow_context_pack_lab.sql b/supabase/migrations/20260715200130_pandora_shadow_context_pack_lab.sql
similarity index 100%
rename from supabase/migrations/20260703020000_pandora_shadow_context_pack_lab.sql
rename to supabase/migrations/20260715200130_pandora_shadow_context_pack_lab.sql
diff --git a/supabase/migrations/20260704010000_pandora_shadow_pack_preflight.sql b/supabase/migrations/20260715200149_pandora_shadow_pack_preflight.sql
similarity index 100%
rename from supabase/migrations/20260704010000_pandora_shadow_pack_preflight.sql
rename to supabase/migrations/20260715200149_pandora_shadow_pack_preflight.sql
diff --git a/supabase/migrations/20260704020000_pandora_promotion_request_board.sql b/supabase/migrations/20260715200514_pandora_promotion_request_board.sql
similarity index 100%
rename from supabase/migrations/20260704020000_pandora_promotion_request_board.sql
rename to supabase/migrations/20260715200514_pandora_promotion_request_board.sql
diff --git a/tests/unit/retrieval-eval.test.ts b/tests/unit/retrieval-eval.test.ts
index 33b0812..2e20f19 100644
--- a/tests/unit/retrieval-eval.test.ts
+++ b/tests/unit/retrieval-eval.test.ts
@@ -191,4 +191,37 @@ describe("supersede-on-distill", () => {
expect(store.memory_context_packs.find((p) => p.id === "au-keep")!.status).toBe("active");
expect(store.memory_context_packs.find((p) => p.id === "rl-old")!.status).toBe("archived");
});
+
+ // Regression for the 2026-07-03 production checkpoint invariant: after repeated distills,
+ // at most one active pack may exist per (user_id, namespace, pack_type), older packs must be
+ // archived (never deleted), and other users' active packs must be untouched.
+ it("repeated distills keep exactly one active pack per (user_id, namespace, pack_type)", async () => {
+ const OTHER_USER = "u-83a07d75";
+ const store: Record = {
+ memory_context_packs: [
+ { id: "other-rl", user_id: OTHER_USER, namespace: "real_life", pack_type: "master", status: "active" },
+ ],
+ audit_logs: [],
+ };
+ const client = makeClient(store);
+ for (const namespace of ["au", "real_life"]) {
+ await createContextPack(client, newPack(namespace) as any, bridgePrincipal, runtime);
+ await createContextPack(client, newPack(namespace) as any, bridgePrincipal, runtime);
+ }
+
+ const activeCounts = new Map();
+ for (const p of store.memory_context_packs) {
+ if (p.status !== "active") continue;
+ const key = `${p.user_id}/${p.namespace}/${p.pack_type}`;
+ activeCounts.set(key, (activeCounts.get(key) ?? 0) + 1);
+ }
+ for (const [key, count] of activeCounts) expect(count, `active packs for ${key}`).toBe(1);
+
+ // superseded packs are archived in place, never deleted
+ expect(store.memory_context_packs.filter((p) => p.user_id === USER && p.namespace === "au")).toHaveLength(2);
+ expect(store.memory_context_packs.filter((p) => p.user_id === USER && p.namespace === "real_life")).toHaveLength(2);
+
+ // the other user's active master is untouched by this user's distills
+ expect(store.memory_context_packs.find((p) => p.id === "other-rl")!.status).toBe("active");
+ });
});