Slice: V1/features.md lines 1807–2815. Date: 2026-06-11. Method: static source
reading + grep only; every claim below was verified against current code
(delegation chains followed; comments/tests not trusted as proof).
Context honored: C4/C5/C14/D5/E2/E4 landed as described in
V1_SPEC_GROUND_TRUTH_AUDIT_2026-06-10.md. The findings below are what remains
AFTER those fixes — chiefly, the fixes are real at the route/store they touched
but do not reach the surfaces the spec names, and several engines built for this
slice have zero live consumers.
Not findings (recorded deferrals / deploy): Psyche
voice/avatar/translation/conferencing realtime (honest notBuilt capability
surface, D5 default); the LLM composer without OSHUN_LLM_* creds (DEPLOY);
/lilith design-system showcase fixtures (spec explicitly says it is not the
canonical crisis surface); D1-covered /domains/* surfaces.
1. The assistant's Iris memory bridge is never bootstrapped — C4's wiring is dead at runtime#
Severity: P0-STRUCT
Evidence:
apps/oshun/bff/src/routes/assistant.ts:208-215— engine constructed withmemoryBridge(C4), sessions created at:395, messages processed at:531. No call tobootstrapMemoryorcloseMemoryanywhere in the route (or anywhere inapps/oshun/bff/src— grep returns zero hits outside the lib).libs/oshun/shell-assistant/src/assistant-engine.ts:269-280—bootstrapMemory()is "Required once per session beforeprocessMessageif the bridge is enabled";:384and:597gaterecordUserTurn/recordAssistantTurnonthis.memoryBridge?.hasSession(sessionId).libs/oshun/shell-assistant/src/iris-memory-bridge.ts:407-409—hasSessionis only true afterbootstrapSession, which onlybootstrapMemorycalls.
Spec promise: features.md 1816 (memory-state indicators reflecting real posture), 1843–1867 (session memory, promotion), 2038–2049 (inference/promotion).
What the code actually does: hasSession() is false for every session the
BFF creates, so no member turn is ever recorded into Iris, the session's memory
indicator is never reconciled with real Iris consent, session memory is never
closed/sealed, and the promote-at-close path (closeMemory) never runs. The
bridge object exists; the runtime never activates it. The assistant still has no
memory.
Fix sketch: In the sessions POST handler,
await assistantEngine.bootstrapMemory(session.id) after createSession; in
the DELETE handler, await assistantEngine.closeMemory(sessionId) before
endSession. Surface the bootstrap's continuity indicator in the session
response.
2. Memory recall is never consulted — the recall-resolution pipeline and its sibling engines are orphans#
Severity: P0-STRUCT
Evidence:
recallRelevantMemory(libs/oshun/shell-assistant/src/iris-memory-bridge.ts:837) has zero callers outside its own file (repo-wide grep).RecallPipeline(libs/oshun/memory-iris/src/recall/pipeline.ts:128),resolveIrisMemoryWriteConflict(conflict-resolution.ts:53),filterIrisPrivacyAwareRecall/evaluateIrisPrivacyStorageGate/cascadeIrisSensitiveConsentRevocation(privacy-suppression.ts:254-384),InferenceEngine(inference/policy.ts:66),InspectionStateMachine(admin-inspection/state-machine.ts:100),ActorHandleNamespace(multi-actor/namespace.ts:36),assessIrisProfileMemoryDecay(retention-compaction.ts:65) — all have zero consumers inapps/(repo-wide grep per symbol).
Spec promise: features.md 1993–2036 (recall-resolution algorithm with scope filter, suppression gate, sensitive-category gate, tenant boundary, relevance scoring, per-surface budgets, surfaceRationale), 1884–1896 (conflict resolution surfaced to the user), 1898–1913 (privacy suppression), 2137–2171 (admin inspection state machine), 2086–2105 (multi-actor masking).
What the code actually does: memory (when it exists at all — see #1/#3) is stored and reviewable, but never recalled into a turn. No response is ever shaped by remembered context; no rationale ("you mentioned this last Sunday") can ever surface; conflict detection, decay, suppression-at-recall, inference thresholds, and admin inspection are tested library code wired to nothing.
Fix sketch: In processMessage, call memoryBridge.recallRelevantMemory
(≤12-entry budget) and feed results into the response formatter and the C14
composer's grounding facts. Route /v1/iris/adapter/search through
RecallPipeline + filterIrisPrivacyAwareRecall. Mount
InspectionStateMachine behind the admin iris reads.
3. No client can grant memory consent — the C4 consent route has zero consumers, so durable memory is unreachable for every member#
Severity: P0-STRUCT
Evidence:
POST /v1/iris/adapter/consents(apps/oshun/bff/src/routes/iris.ts:437— "the only path to granted") — repo-wide grep overapps/oshun/web/srcandapps/oshun/mobile/srcfinds no caller. The only iris calls any client makes are two GETs:apps/oshun/web/src/app/profile/memory/memory-state.ts:144-147(review,consents).- Consents start empty (
iris-memory-adapters.ts:467-508honest empty seed) and the canonical engine suppresses durable writes until granted (iris-memory-bridge.tsplanWrite/remember contract).
Spec promise: features.md 1846–1849 (profile memory "opt-in… stored with consent record"), 2057–2085 (customer memory UX flows).
What the code actually does: the consent flip exists only as an HTTP route
and its tests. The /profile/memory page renders the six category consents as
forever-"pending" and offers no control that POSTs the decision. Combined with
#1 (no writes) and #2 (no recall), the consent-gated durable memory feature is a
closed loop no member can enter: nothing can consent → nothing writes → nothing
recalls.
Fix sketch: Add grant/deny controls on /profile/memory (and the assistant
dock consent prompt) that POST /v1/iris/adapter/consents?role=settings and
re-hydrate. Mirror in mobile.
4. /profile/memory mutations are localStorage theater — "permanently deleted (step-up enforced)" facts resurrect from the server#
Severity: P0-HONESTY
Evidence:
apps/oshun/web/src/app/profile/memory/ProfileMemoryControls.tsx:369-498— forget-fact, full-clear, category-clear, scope-clear, pause, and export all mutate the client view-model via lib functions (hardDeleteIrisUserMemoryFact,clearIrisUserMemoryCategory, …) andcommit()→ localStorage (:173). Success toasts:"Memory fact permanently deleted (step-up enforced)"(:378).- The ONLY network calls on the surface are the two hydration GETs
(
memory-state.ts:144-147).DELETE /v1/iris/adapter/memory/:id(routes/iris.ts:629) has zero web/mobile callers. - Hydration overwrite:
ProfileMemoryControls.tsx:156re-fetchesfetchRealProfileMemoryStateon mount, replacing local state with the server record.
Spec promise: features.md 1875–1877 ("deletions are tombstoned and propagated; no silent re-creation"), 1911–1918 (edit/clear/opt-out/export/delete/pause), 2068–2076 (forget flow with step-up, full clear with 7-day grace).
What the code actually does: a member who "permanently deletes" a remembered
fact deletes a localStorage copy; the server's Iris store is untouched and the
fact reappears on the next visit. Pause ("off-the-record") likewise never
reaches the BFF — the server would keep writing memory while the user believes
writes are paused (features.md 1918). The step-up gate is also theater: the
challenge answer (iris-step-up) is printed next to the input (:1082) and
checked client-side (:264).
Fix sketch: Wire forget → DELETE /v1/iris/adapter/memory/:id,
category/scope clear → bulk-delete criteria, pause → a server-side per-user
write-suppression flag the iris adapter honors, then re-hydrate. Replace
string-challenge step-up with the real auth step-up flow.
5. /profile/safety shows every member a fabricated crisis record for a fictional user ("oshun-member-renata")#
Severity: P0-HONESTY
Evidence:
apps/oshun/web/src/app/profile/safety/ProfileSafetyJourney.tsx:42-85—buildProfileSafetySeedStatehardcodes a 2026-05-08acute-griefcrisis frame forsubjectUserId: 'oshun-member-renata', with a fabricated redacted excerpt ("[redacted: grief disclosure during Tara session]") and resources (us-988, …). Rendered under the header "This page reflects what the audit platform sees about your most recent crisis frame" (:206-208). State persists to localStorage; the page never fetches anything.apps/oshun/mobile/src/safety/MobileProfileSafetyJourney.tsx:50-67— identical Renata seed on mobile.- The REAL crisis-frame store exists server-side
(
apps/oshun/bff/src/safety/crisis-frame-runtime.ts:41-64, durable snapshot) and is never read by either surface.
Spec promise: features.md 2656–2659 ("Visible audit: a 'what happened' surface in profile/safety shows the user the audit-platform-visible record of the frame… the user owns the record of what occurred").
What the code actually does: every member — including those who have never
had a crisis frame — sees the same invented incident presented as their own
audit record; their stillness/re-entry/check-in interactions manipulate a
fiction in localStorage. This is the exact "Renata" fixture pattern C4 purged
from /profile/memory, surviving on the safety surface, where honesty matters
most.
Fix sketch: Add GET /v1/safety/crisis-frames/mine over the crisis-frame
store (+ incident records); render honest empty state ("no crisis frames on your
account") when none; drive the recovery-journey state machines from the real
frame, server-persisted.
6. The mobile assistant never reaches the BFF — no Lilith crisis policy, no memory, no server persona, no composer#
Severity: P0-SEC
Evidence:
apps/oshun/mobile/src/components/MobileAssistantSheet.tsx:777,788,844— every reply comes frombuildMobileAssistantReply(context, prompt, personaId).apps/oshun/mobile/src/assistant/mobile-assistant-model.ts:372-440— that function is a client-side keyword matcher over canned navigation strings. Grep forfetch(/api calls acrossapps/oshun/mobile/src/assistant+ the sheet: zero. Grep forcrisis|lilith|safetyin the sheet: zero.
Spec promise: features.md 1813–1815 ("Universal invocation points across customer web, customer mobile, and admin web"), 2342–2366 (crisis-aware behavior on every surface, no-bypass), C5's "EVERY member turn".
What the code actually does: a member who types a suicidal-ideation message into the mobile assistant gets a canned reply like "The durable answer is Library…". The 13-rule crisis catalog, operator crisis frames, Iris memory, the E4 server persona, and the C14 composer are all bypassed because mobile turns never leave the device. C5's guarantee holds only on customer web.
Fix sketch: Point the mobile sheet at
POST /v1/assistant/sessions(/…/message) via oshun-bff-client (the network
layer already exists for handoff), keeping the local templates only as the
offline/error notice path — never as the reply for a successfully-sent turn.
7. AssistantPanel fabricates tool execution — fake progress bars report "complete" for actions that never ran#
Severity: P0-HONESTY
Evidence:
apps/oshun/web/src/components/assistant/AssistantPanel.tsx:1539-1576 — any
message containing "start"/"action"/"session"/"check-in" spawns a ToolAction
card ("Starting Tara session...", "Running daily check-in...") whose progress is
progress += webRandomUnitInterval() * 30 + 10 on a 400 ms timer
(// Simulate progress, :1561) until status: 'complete'. No request is
made; nothing is started or run.
Spec promise: features.md 1813–1827 (assistant experience; safe fallback — not fabricated activity), Quality bar: no result-faking on live paths.
What the code actually does: the member sees a tool run to 100% and report complete when no check-in, session, or action occurred anywhere.
Fix sketch: Delete the keyword-spawned ToolAction simulation; render tool
cards only from real response cards/navigateTo outcomes returned by the BFF.
8. Failed session-history fetch silently substitutes four fabricated sessions#
Severity: P0-HONESTY
Evidence:
apps/oshun/web/src/components/assistant/AssistantPanel.tsx:651-663
(SIMULATED_SESSION_HISTORY, SESSION_TOPICS "Evening meditation routine", …)
served by the catch at :1885-1887
(// Fallback to simulated session history).
Spec promise: features.md 1816 (turn history, transcripts are real member
data); the panel's own send-path comment (:1654-1657) states the correct rule
— fabricating is forbidden — then the history path violates it.
What the code actually does: when GET /v1/assistant/sessions fails, the
member is shown four invented past conversations with invented topics as their
own history.
Fix sketch: Replace the fallback with an honest "history unavailable right now" state (the send path already has the right pattern).
9. Assistant-route Nisaba/Metis adapters are inline fixtures that fabricate per-member progress — while real adapters sit unused on app.domainAdapters#
Severity: P0-HONESTY
Evidence:
apps/oshun/bff/src/routes/assistant.ts:115-170—nisaba.getContinueReadingreturns hardcoded "Tablet Fragment 12 … progressPercent: 62" for every user;metis.getContinueLearningreturns "Calculus I … progressPercent: 34, nextStep: 'Lesson 4 queued…'" for every user; search/recommendations come frombuildNisabaSearchHits/METIS_SEARCH_SEEDSkeyword fixtures.apps/oshun/bff/src/adapters/domain-service-adapters.ts:827-897— the REALNisabaApiAdapter/MetisApiAdapter(withgetDailyPassage,getContinueLearning,searchCatalog, circuit breakers, fail-closed unavailability) exist onapp.domainAdapters(:897-898) and are used at:59-113for the other four domains — Nisaba and Metis alone are bypassed.
Spec promise: features.md 1816–1822 (context handoff, cross-domain carry-over over real domain data; safe fallback when a subsystem is unavailable — not fabricated data).
What the code actually does: the assistant tells every member they are 34%
through Calculus I and mid-way through "Tablet Fragment 12" regardless of
reality, on the live reply path. Two sources of truth: the assistant's Metis
answers can contradict the real /v1/metis surfaces.
Fix sketch: Replace the inline objects with adapters.nisaba.* /
adapters.metis.* delegation exactly as the other four domains do (the
interfaces already align); let the engine's safe-fallback path handle
unavailability.
10. Persona governance is five disjoint ID universes — policy, lifecycle, selection, and rendering never meet#
Severity: P0-STRUCT
Evidence:
- Consumer selection (E4): launch roster IDs —
lotus-mindful,rhea-steady,veritas-compass, … (libs/oshun/persona-registry/src/launch-roster.ts:64+; served byroutes/personas-consumer.ts). - Web assistant panel's own switcher:
oshun-navigator,tara-guide,stoic-scholar,zen-guide,sufi_heart, … (apps/oshun/web/src/design-system/assistant-persona-switching.ts:81-145), persisted client-side and sent in apersonaHandoffbody field the BFF route never reads (AssistantPanel.tsx:1444-1451,1596-1601vsroutes/assistant.ts:444-449body type). The panel never calls/v1/personas/select(grep:personasClientused only bycomponents/personas/PersonaPicker.tsx). - Admin lifecycle store:
persona-zen-guide/persona-stoic-guide/persona-sufi-guide(apps/oshun/bff/src/admin/persona-lifecycle-store.ts:90-92) — disjoint from the roster, so an admin retire/deprecate can never affect what members can select. - Lilith policy packs:
tara-contemplative-guide/veritas-stoic-scholaronly (adapters/lilith-persona-policy-adapters.ts:39,66-69) —checkPersonaTopic('lotus-mindful', …)returns "No Lilith persona policy pack was found" (:526-533); no roster persona has a policy pack. - Psyche runtime default persona:
Guide One(adapters/psyche-embodiment-adapters.ts:285+).
Spec promise: features.md 2459–2519 (single canonical seven-role taxonomy "referenced verbatim" by the registry, the assistant handoff surface, and every Lilith suite), 2599–2629 (one lifecycle governing every persona), 2788–2791 (customer selection from the governed registry).
What the code actually does: the persona a member selects (roster) has no Lilith policy, no lifecycle governance, and is not the persona the assistant panel displays/switches; the persona the panel switches is never told to the server; the personas admins govern don't exist for members. E4 made the selection server-side, but the selection floats free of policy, lifecycle, and the rendering surface.
Fix sketch: Make LAUNCH_ROSTER persona IDs the single ID space: key Lilith
policy packs and the admin lifecycle store by roster IDs; replace the panel's
assistant-persona-switching catalog with /v1/personas browse +
/v1/personas/select; drop the ignored personaHandoff body field.
11. The Lilith tone-band catalog and its enforcement functions have zero consumers#
Severity: P0-STRUCT
Evidence:
libs/oshun/persona-policy-lilith/src/tone-bands/enforcement.ts:79-160 exports
resolveEffectiveToneBand, enforceIsisGenerationDispatchTone,
enforceLivingScenesConductorTone, enforceShareControlTone; repo-wide grep
finds no caller outside the lib and its tests. Same for the band-transition
graph (transitions.ts) and checkPublishLineage/emitLineageBinding
(sacred-symbols/lineage-binding.ts).
Spec promise: features.md 2520–2570 (tone band catalog, band transitions, per-band capability caps: "Bands cap the ceiling"), 2552 table (max motion / audacity / share / generation tier), tests at 2680-2683 ("every band's caps enforced in Isis, Living Scenes, share controls"), 2571–2597 (lineage policy enforced at composition time).
What the code actually does: generation dispatch, share controls, and scene conduction run without any tone-band ceiling; the catalog is a versioned artifact enforcing nothing. The "contemplative-strict" promises (audacity 0, reduced-motion, redacted-intent share) are not applied anywhere a member's request passes through.
Fix sketch: Call enforceIsisGenerationDispatchTone in the Isis generation
admit path, enforceShareControlTone in the share routes, and
enforceLivingScenesConductorTone in the living-scenes conductor; derive the
active band from surface + persona role.
12. Psyche dialogue surface (WS + HTTP) has zero clients, and records member turns with no Lilith policy, no crisis-frame check, and client-assignable "assistant" role#
Severity: P1
Evidence:
- Zero consumers: grep for
psycheacrossapps/oshun/web/srcandapps/oshun/mobile/srcreturns nothing (no page, no client, no WS connect). - No policy:
apps/oshun/bff/src/psyche/realtime-route.ts:83-99androutes/psyche.tsturn POST record content straight into the durable store — noanalyzeMessageSafety, noisCrisisFrameActiveForUser, no consent gate; turns persist via durable snapshot (dialogue-session-store.ts:184). - Role spoofing:
realtime-route.ts:85-88acceptsrole: 'assistant' | 'system'from the client, writing assistant-attributed turns into the member's transcript. PsycheSessionBridge(libs/oshun/shell-assistant/src/psyche-session-bridge.ts:179) — the piece that would connect the assistant engine to Psyche sessions — has zero app consumers.
Spec promise: features.md 2191–2196 (session envelope binds persona, Lilith policy version, Iris memory scope), 2284–2289 (crisis-frame entry mid-session → instant policy enforcement, memory-write suspension), 1854–1856 (no memory writes during crisis frame).
What the code actually does: D5's transport transports for nobody (a member-facing realtime leg with no member surface), and if anything ever connects, it is a policy-free durable recorder of member utterances. The assistant engine and the psyche session store remain two unlinked conversation systems.
Fix sketch: Run every inbound turn through analyzeMessageSafety + the
crisis-frame check (mirror the assistant route's interception); reject
client-supplied non-user roles on the WS; wire a web surface (assistant panel
session transport or /psyche page) — or explicitly record the no-client status
as a deferral.
13. Psyche adapter session creation fabricates a live "connected" runtime — example.com join URLs, invented latencies and confidence scores#
Severity: P1
Evidence: apps/oshun/bff/src/adapters/psyche-embodiment-adapters.ts —
buildConference returns connectionState: 'connected',
joinUrl: 'https://example.com/psyche/…', latencyMs: 130 (:605-617);
participants get connectionQuality: 'excellent', latencyMs: 48/62,
isSpeaking: true (:504-528); buildTranslation reports a 'streaming'
websocket with avgConfidence: 0.91 (:620-646); disclosure metadata claims
watermarkApplied (:660); persona seeds carry invented
evaluationScore: 0.94 / validationScore: 0.93 (:297,323). All reachable
via POST /v1/psyche/adapter/sessions (routes/psyche.ts:432).
Spec promise: features.md 2236–2256 (latency budgets and quality thresholds are measured), 2270–2281 (diagnostics: per-event timing, provider attribution). D5's own bar: "honest health… instead of fabricating transports".
What the code actually does: the capability/health routes honestly say the
realtime transports are not built (:155-169), but the session-creation route
on the same adapter answers with a fully fabricated connected conference +
streaming translation + speaking AI participant. The two routes contradict each
other; the session route fails the "did it really produce this?" test.
Fix sketch: Make plan-session/sessions fail-closed for modalities whose
transports are notBuilt (mirror the capability surface); return only the
text-session envelope that is real; delete the invented metrics.
14. Crisis-frame suppression of Iris memory is client-asserted, and crisis frames don't suppress generation/Tara surfaces#
Severity: P1
Evidence:
apps/oshun/bff/src/routes/iris.ts:545-578—lilithCrisisFrame/safetyCriticalContexton/v1/iris/adapter/searchare query params the caller supplies;:740-741,781-782— same onplan-write/rememberbodies. The server never injectsisCrisisFrameActiveForUser(userId)into these calls; the canonical adapter'smatchesCrisisRecallPolicyfilter therefore only fires if the client chooses to say so.- Server-side frame checks exist in exactly four places
(
grep isCrisisFrameActiveForUser): assistant route, telegram, reminders, payments (domain-stubs). No Isis generation route, Tara route, or living-scenes route consults the frame.
Spec promise: features.md 1854–1856 ("during a Lilith crisis frame, no new memory writes; existing memory suppressed from recall"), 2004–2005 ("Crisis-frame suppression overrides all other markers"), 2351–2354 (halt synthesis — no ritual continuation), 2648–2650 (Living Scenes/briefings/Isis generation remain disabled during stillness).
What the code actually does: during an operator-armed crisis frame, memory
writes and recalls through /v1/iris/adapter/* proceed normally unless the
client volunteers the flag, and generation/ritual surfaces continue serving.
Enforcement-by-honor-system.
Fix sketch: In the iris search/plan-write/remember handlers, compute
lilithCrisisFrame server-side via isCrisisFrameActiveForUser and OR it with
the client hint; add the same check to the Isis generation admit path and Tara
session-start.
15. /profile/persona voice page is built against a contract the E2 route doesn't serve — dead previews, dead "switch", undefined provider#
Severity: P1
Evidence:
- Page expects
{ id, providerId, previewUrl, approvalState }per voice (apps/oshun/web/src/app/profile/persona/page.tsx:31-43) and rendersProvider · {voice.providerId} · {voice.approvalState}(:132),<audio src={voice.previewUrl}>(:173-178), andvoice.id === data.activeVoiceId(:101). - The E2 route returns
{ voiceId, label, description, familyLabel, premiumCast }(apps/oshun/bff/src/routes/personas-consumer.ts:117-125). - The page's doc comment promises "Voice switching is a POST that lives in the
BFF voice module" (
:5-6) — no such POST exists; the "○ switch" affordance (:157) is a non-interactive<span>.
Spec promise: features.md 2738–2752 (voice profile schema: vendor, consent record, naturalness baseline; per-profile catalog), 2788–2791 (customer persona selection with per-persona disclosure copy).
What the code actually does: every row shows "Provider · undefined ·
undefined", an audio element with src={undefined}, an active marker that can
never match (voice.id is undefined), and a switch control that does nothing.
E2 unified the data source but the page was never migrated to it.
Fix sketch: Render the route's actual DTO
(voiceId/familyLabel/premiumCast), drop the fabricated provider/preview
fields (or add real ones to the registry), and make "switch" POST
/v1/personas/select with the persona behind the voice.
16. Voice and avatar persona packs never affect the actual voice or embodiment — stored config, default browser voice#
Severity: P1
Evidence:
AssistantPanel.tsx:1829-1840—speakTextuses a bareSpeechSynthesisUtterance(rate 1.0, pitch 1.0, novoiceassignment). The member'sactiveVoiceId(from/v1/persona/voice/ active persona) is read nowhere in the speech path.- The assistant identity pins
voicePackId: null, avatarPackId: null(routes/assistant.ts:188-189); avatar mode in the panel is entitlement-gated CSS embodiment with no pack binding (:1103-1136).
Spec promise: features.md 2690–2697 (packs are governed assets gating premium voiced/avatar experiences), 2738–2752 (voice profile contracts), 1828–1830 (avatar assistant mode with entitlement gating + synchronized disclosure).
What the code actually does: selecting a voice/persona changes a stored record and a list highlight; every spoken reply uses the OS default voice and the avatar surface is identical for all personas. Persona packs are config-without-effect on every surface that exists today.
Fix sketch: Map roster personas to speechSynthesis.getVoices() profiles
(locale + gender + rate/pitch per pack) as the V1 voice binding; thread
activePersona into the panel's tone/avatar theme; record the real TTS-provider
binding as deploy-bound.
17. ContinuationToken protocol: real route, zero emitters, zero readers#
Severity: P1
Evidence: routes/iris.ts:307-416 implements write/read/resume/notices over
ContinuityProtocol (@oshun/memory-iris/continuity/protocol). Grep for
continuation-token across apps/oshun/web/src + apps/oshun/mobile/src: zero
hits. No surface (Tara, Nisaba, Metis, home shell) writes checkpoints or renders
continuation cards from it. (The separate C16 /v1/iris/mobile-handoff blob
channel IS consumed — it is not the spec's per-surface checkpoint protocol.)
Spec promise: features.md 2105–2135 ("every surface emits ContinuationToken updates at natural checkpoints… the home surface renders continuation cards from them").
What the code actually does: dead route; cross-device continuity for reading/study/ritual posture does not function.
Fix sketch: Emit tokens from the web Tara player / Nisaba reader / Metis
lesson at checkpoint events; render GET /v1/iris/continuation-tokens as home
continuation cards.
18. Iris export fabricates a bundle that is never written; DSAR data-export omits memory entirely#
Severity: P1
Evidence:
adapters/iris-memory-adapters.ts:377-402—exportMemoriesreturnssuccess: true,filePath: '/exports/…'(no file is ever written),sizeBytes: memoryCount * 384,checksum: '${exportId}-checksum-v1'— all invented. Served live byPOST /v1/iris/adapter/export(routes/iris.ts:665).- The web export flow doesn't use it:
ProfileMemoryControls.tsx:485exports the client view-model as a data URI (buildProfileMemoryDataUri) and labels DSAR exports "signed and ready to download" (:497) with no signature. apps/oshun/bff/src/data-export/bundle-builder.ts— grep for iris/memory/assistant: zero; the account data-export bundle contains no memory.
Spec promise: features.md 1915 ("Export: full memory bundle export (JSON), with audit chain"), 2080–2082 ("signed JSON with audit chain; downloadable from profile/data-export; available to DSAR fulfillment").
What the code actually does: the server endpoint manufactures an export manifest pointing at a file that doesn't exist; the UI exports a different (client-side) object and calls it signed; DSAR fulfillment never sees memory.
Fix sketch: Make exportMemories build the real in-memory JSON bundle
(entries + consents + audit refs) and return it inline (or via the existing
data-export bundle store); add an iris section to bundle-builder.ts; drop the
"signed" copy until a signature exists.
19. Lilith enforcement reaches exactly one surface — the spec's other wiring points are unwired#
Severity: P1
Evidence: analyzeMessageSafety is called from exactly one live path:
routes/assistant.ts:485,609 (grep). Sophia's answer composer/synthesizer
(apps/oshun/bff/src/sophia/*) contains no lilith/crisis reference; Metis tutor
routes, Tara generation paths, and Telegram inbound message handling run no
claim/tone/crisis analysis (telegram checks only the operator frame flag,
routes/telegram.ts:19,263). validateDraftResponse (output-side validation)
is exposed on /v1/lilith/adapter/evaluate-interaction but invoked by no
generation pipeline.
Spec promise: features.md 2444–2449 ("Wiring points: Tara teacher personas… ritual script generation, meditation script narration, voice rendering, avatar rendering, Veritas explainer generation, Sophia grounded answers, Metis tutor responses, support copilot"), 2296–2302 ("the policy fires inline across whichever surface the user is on").
What the code actually does: C5 put the catalog in the assistant's line of fire and nowhere else. A crisis disclosure typed into a Sophia question, a Metis tutor exchange, or a Telegram message gets no per-signal response; generated contemplative content is never tone-checked before serving.
Fix sketch: Run analyzeMessageSafety on Sophia question intake and
Telegram inbound text; run validateDraftResponse (plus the tone rubric) on
Sophia/Metis composed outputs before send; reuse the assistant route's
interception envelope.
20. Memory consent has three disconnected sources of truth; the assistant's memory indicator reflects none of the real ones#
Severity: P1
Evidence:
- (a) Profile preferences:
AssistantControlsSection.tsxwritespreferences.assistant.memory.*; the BFF session create derivesmemoryConsentGrantedfrom that (routes/assistant.ts:417-419viabuildOshunAssistantBehaviorProfile), and session continuity's memory indicator comes from it (assistant-metis-handoff.ts:53-56). - (b) Iris consent records:
setBffIrisConsentstore (iris-memory-adapters.ts:412) — the thing that actually gates durable writes. - (c)
/profile/memorycategory consents: localStorage projection (#3/#4). - The reconciler (bridge
bootstrapSessioncontinuity indicator) is never invoked (#1).
Spec promise: features.md 1816 (memory-state indicator), 1846–1849 (consent record governs profile memory); E2's own one-source-of-truth bar.
What the code actually does: a member can toggle "Approved memory" in assistant controls and see the shell indicator say memory is on, while Iris consent is ungranted and every durable write would be suppressed — and vice versa.
Fix sketch: Make the Iris consent store authoritative: assistant-controls
toggle POSTs /v1/iris/adapter/consents; session create reads consent from the
iris adapter (or via bootstrapMemory's continuity) instead of preferences.
21. Mobile has no memory-controls flow at all#
Severity: P1
Evidence: find apps/oshun/mobile/src -iname "*memory*" → only
components/AssistantMemoryState.tsx (an indicator with no /v1/iris binding).
No mobile screen lists/edits/forgets memory; no consent surface.
Spec promise: features.md 2049–2052 ("The customer-facing memory surface
lives at apps/oshun/web/src/app/profile/memory/ and the equivalent mobile
flow").
Fix sketch: Mobile memory screen over the same /v1/iris/adapter/review +
/consents reads and the (to-be-wired, #4) mutation routes.
22. C14's retrieval/synthesized disclosure labels and the E4 persona envelope are rendered by no client#
Severity: P2
Evidence: the route emits responseMode, retrievalText
(routes/assistant.ts:611-621) and activePersona (:580-587); grep across
web + mobile finds no consumer of any of the three (the only responseMode hits
are an unrelated drone-cinematics module). The panel's parse type
(AssistantPanel.tsx:1577-1590) doesn't even include the fields.
Spec promise: features.md 1816-1818 (disclosure state, persona identity indicators).
Fix sketch: Render a small "synthesized/grounded" register chip + the server persona label in the panel from the existing fields.
23. Lilith adapter still runs on the frozen clock and fabricates voice-quality verdicts#
Severity: P2
Evidence: adapters/lilith-persona-policy-adapters.ts:38 —
DEFAULT_NOW = '2026-03-22T15:00:00.000Z' stamps health lastChecked forever
(the C4 fix removed only the iris twin). createVoiceSafetyAssessment
(:449-488) hardcodes clarity: 0.94, naturalness: 0.92, consistency: 0.9,
watermark: { verified: true } unconditionally, certified: !abuseSignal —
quality scores and watermark verification for audio it never analyzed, served
via /v1/lilith/adapter/* voice-safety reads.
Spec promise: features.md 2368–2384 (voice-quality policy: naturalness floor measured per profile, audio integrity: required watermark validated).
Fix sketch: Live clock like the iris adapter; return
{ assessed: false, reason: 'no-audio-analysis-provider' } fail-loud instead of
invented scores until a real scorer exists.
24. Assistant sessions and transcripts are process-memory only#
Severity: P2
Evidence: routes/assistant.ts:51 module-singleton engine;
assistant-engine.ts sessionStore is an in-memory map with no snapshot sink
(unlike psyche's wireDurableDialogueSessions and iris's
wireDurableIrisMemory).
Spec promise: features.md 1816 (persistent context, transcripts), 2200-2204 (reconnect with durable session ID).
Fix sketch: Add a durable-snapshot sink for assistant sessions mirroring
dialogue-session-store.ts.
25. Iris/Lilith/Psyche health endpoints fabricate per-check latencies#
Severity: P2
Evidence: iris-memory-adapters.ts:255-281 (latencyMs: 9/7/12/5/6/11
constants); psyche adapter at least measures its stores
(psyche-embodiment-adapters.ts:132 "D5: honest health… measure"), lilith
health is constant-frozen (#23).
Spec promise: honest health surfaces (D5's own bar).
Fix sketch: Measure the store call (psyche pattern) or drop the latency field.
26. /welcome/test-assistant test harness ships as an app route#
Severity: P2
Evidence: apps/oshun/web/src/app/welcome/test-assistant/page.tsx — a
"TestSection" playground for the panel, reachable in production routing.
Fix sketch: Move under a dev-only flag or delete (the panel has real entry points everywhere).
27. UX cohesion: three persona surfaces, two memory surfaces, and a safety surface that don't tell one story#
Severity: UX
Evidence/observations:
- Persona:
/personas(PersonaPicker, roster IDs, server-backed) vs the assistant panel's persona switcher (different persona set, client-only) vs/profile/persona("voices", broken DTO #15). A member can "activate" three different personas in three places and no surface reflects the others. - Memory: the assistant dock/panel memory indicator (preferences-driven) vs
/profile/memory(Iris-hydrated + localStorage mutations). The spec's pause toggle "visible in the assistant dock and the profile/memory header" (features.md 2076-2078) exists only on profile/memory, only locally — pausing there changes nothing in the dock or the engine. - Safety:
/profile/safetynarrates a crisis journey (stillness, re-entry, check-ins) that has no connection to the real crisis frame the assistant route enforces — a member who actually trips the crisis catalog sees the assistant's plain-language response, then finds a profile/safety page describing a different, fictional incident. - Psyche: a whole runtime with no page; nothing in the product ever says "realtime session".
Fix sketch: One persona entry point (panel switcher = /v1/personas data);
one memory-consent authority surfaced identically in dock + profile;
profile/safety driven by the real frame store; remove or surface Psyche.
Severity counts#
| Severity | Count | Findings |
|---|---|---|
| P0-SEC | 1 | #6 |
| P0-HONESTY | 5 | #4, #5, #7, #8, #9 |
| P0-STRUCT | 5 | #1, #2, #3, #10, #11 |
| P1 | 10 | #12, #13, #14, #15, #16, #17, #18, #19, #20, #21 |
| P2 | 5 | #22, #23, #24, #25, #26 |
| UX | 1 | #27 |
| Total | 27 |
Cross-cutting summary: the 06-10 headline fixes are real but each stops one hop short of the spec's surface. C4 built consent-gated durable memory that nothing can write to, consent to, or recall from (#1–#3); C5 put crisis policy on one route while mobile, Sophia, Metis, Telegram, and Psyche turns bypass it (#6, #12, #19); D5 built a transport no client connects to (#12); E2/E4 made persona selection server-side inside a five-way persona ID schism (#10, #15, #16). The Renata-fixture pattern C4 purged from /profile/memory survives verbatim at /profile/safety (#5), and the tone-band/recall/inference/inspection engines remain fully built, fully tested, and fully unwired (#2, #11).