Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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; } }
2 changes: 1 addition & 1 deletion components/pandora/MobileBottomNav.tsx
Original file line number Diff line number Diff line change
@@ -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 <nav className="pd-mobile-nav" aria-label="Mobile Pandora navigation">{mobileNavItems.map((item, index) => { const Icon = icons[index]; const active = item === activeNav || (item === "Dashboard" && activeNav === "Dashboard"); return <button type="button" key={item} aria-current={active ? "page" : undefined} onClick={() => onNavChange(item === "Feed" ? "Memory Feed" : item)}><span><Icon size={18} aria-hidden="true" />{item === "Queue" ? <i aria-label="Queue has attention items" /> : null}</span>{item}</button>; })}</nav>; }
export function MobileBottomNav({ activeNav, onNavChange }: { activeNav: string; onNavChange: (item: string) => void }) { return <nav className="pd-mobile-nav" aria-label="Mobile Pandora navigation">{mobileNavItems.map((item, index) => { const Icon = icons[index]; const active = item === activeNav || (item === "Dashboard" && activeNav === "Dashboard"); return <button type="button" key={item} aria-current={active ? "page" : undefined} onClick={() => onNavChange(item === "Feed" ? "Memory Feed" : item)}><span><Icon size={18} aria-hidden="true" />{item === "Queue" ? <i aria-hidden="true" /> : null}</span>{item}{item === "Queue" ? <span className="pd-sr-only">Queue has attention items</span> : null}</button>; })}</nav>; }
66 changes: 66 additions & 0 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
@@ -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`.
33 changes: 33 additions & 0 deletions tests/unit/retrieval-eval.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Row[]> = {
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<string, number>();
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");
});
});
Loading