V1 Web PWA · Journey

Journey: Arete create habit

A source-reconciled journey record for V1 Web PWA — shipped behavior, state boundaries, failure modes, and the automation evidence available today.

automated · 3 specs verdict: partial· 2026-05-29
9sections17 minread

On this page
Journey at a glance
ActorsFirst-habit userAdding-Nth-habit userCoach-suggested userRecovery-resuming user
Automation3 E2E spec(s) · verdict: partial· 2026-05-29

Journey flow#

Generated from the authored steps below — click a node to jump to that section.

flowchart TD s1["1. Land on home and pick the Arete entry"] s2["2. Arete hub renders"] s3["3. Pick the create entry point"] s4["4. Enter the habit creation wizard (HabitLoop…"] s5["5. Save the habit"] s6["6. Return to the polished /arete hub"] s7["7. First check-in"] s8["8. Streak begins"] s9["9. Return to home and confirm rail update"] s10["10. Coach-card observation (lagged)"] s1 --> s2 --> s3 --> s4 --> s5 --> s6 --> s7 --> s8 --> s9 --> s10 click s1 href "#1-land-on-home-and-pick-the-arete-entry" click s2 href "#2-arete-hub-renders" click s3 href "#3-pick-the-create-entry-point" click s4 href "#4-enter-the-habit-creation-wizard-habitloopwizard" click s5 href "#5-save-the-habit" click s6 href "#6-return-to-the-polished-arete-hub" click s7 href "#7-first-check-in" click s8 href "#8-streak-begins" click s9 href "#9-return-to-home-and-confirm-rail-update" click s10 href "#10-coach-card-observation-lagged" s10 --> v(["verdict: partial"]) click v href "../results/arete-create-habit.html" style v stroke:#d6a531,color:#d6a531

The cross-view flow from "user wants to add a new ritual" through "habit created, first check-in recorded, streak begins, home rail reflects it." The journey traverses both the polished Lilith room (/arete) and the deeper internal habit-system surface (/domains/arete/habits), because the polished hub's + New ritual link is the natural starting point while the multi-step creation flow lives in the internal namespace. Catches drift between the two namespaces, the local-vs-server persistence boundary in the internal habit system, and the streak-cannot-start case when the create-write never persists across surfaces. The polished /arete/habits/new proposal lane is also covered as the adjacent "held for coach review" path: its browser form fields are mapped to the same real BFF habit contract, but the proposed habit does not start a streak until coach review.

Personas#

  • First-habit user — signed in with no Arete intentions; the BFF-backed room renders the explicit data-arete-room-empty state and points to the habits page
  • Adding-Nth-habit user — already has 1–4 intentions; the polished + New ritual button is the natural entry; the deeper habit-system surface exists at /domains/arete/habits
  • Coach-suggested user — accepts or defers a Lilith coach-card proposal; both decisions persist through /v1/arete/coach/decision
  • Recovery-resuming user — broken streak; new habit should not be punished by past quiet days (see customer/04-arete/arete-streak.md promise copy)

Pre-conditions#

  • Signed-in session
  • getArete() reading /v1/arete/room into AreteData ({ intentions, weekStats, coachNote, eveningPrompt }); an unavailable feed returns an honest empty room rather than invented streak fixtures, per customer/04-arete/arete.md
  • The internal /domains/arete/habits surface is reachable; client component imports sampleHabits, sampleHabitStacks, sampleIdentityStatements, sampleHabitAnalytics per customer/04-arete/domains-arete-habits.md
  • Service worker active; offline behaviour deferred to shell/03-pwa-behavior.md

Steps#

1. Land on home and pick the Arete entry#

2. Arete hub renders#

page.tsx awaits getArete()<AreteRoom data={data} />. The room mounts LCustomerNav active="today" and LSubNav active="habits" with items Habits · Journal · Weekly review · Patterns.

  • Page server-blocks on getArete() (no loading.tsx under /arete)
  • Sub-nav highlights "Habits"
  • Domain masthead reads "Arete · habits", display title "The small daily rituals."
  • Existing intention rows, when present, render a "Done today" / "Pending" tag and seven-day cell bar; a fresh member instead sees the empty state
  • "+ New ritual" link is visible at the bottom of the intentions list and targets /domains/arete/habits?wizard=loop
  • Coach card visible on the right with data.coachNote, "Approve" / "Not this week" buttons
  • Verify: customer/04-arete/arete.md layout regions

3. Pick the create entry point#

Three valid entry points; each lands the user in a different state.

  • Entry A — + New ritual button on /arete — links to /domains/arete/habits?wizard=loop. Fixed 2026-06-23: /domains/arete/habits/page.tsx now reads searchParams and initializes AreteHabitSystem in loop_wizard, so the handoff lands in the wizard instead of dropping to the dashboard.
  • Entry B — Coach card "Approve" / "Not this week" — wired 2026-05-26 via AreteCoachActions client component (apps/oshun/web/src/components/lilith/AreteCoachActions.tsx). Approve shows the "queued for the next review" confirmation; Not-this-week shows "Set aside for now". Both decisions POST /v1/arete/coach/decision, read back through /v1/arete/room, and can be undone to pending.
  • Entry C — Each intention row's arrow is now a <Link> to /domains/arete/habits?habit=<label> so the per-habit detail surface can read it. The polished was a static <span> until 2026-05-26.
  • Entry D — Propose a habit on /arete/habits — links to /arete/habits/new, verifies the label/cadence/domain/why form contract in the polished browser surface, then posts those same values through authenticated real HTTP to POST /v1/arete/habits and reloads /arete/habits to prove the server-rendered page reads the BFF habit back. This is intentionally a proposal path, not the immediate Cue→Routine→Reward active-habit wizard.

4. Enter the habit creation wizard (HabitLoopWizard)#

Inside /domains/arete/habits, the dashboard exposes nine sub-views via the view state union: dashboard | loop_wizard | four_laws | stacking | identity | keystone | streaks | analytics | reminders | detail per customer/04-arete/domains-arete-habits.md.

  • Tap the "New Habit" entry in HabitDashboard, or land via /domains/arete/habits?wizard=loop
  • State transitions: view flips from dashboard to loop_wizard; <HabitLoopWizard onBack onCreate /> mounts
  • Wizard steps cover the shipped Atomic-Habits loop scaffolding: cue (CueType), routine description/duration/two-minute version, and reward (RewardType, immediate reward, long-term benefit)
  • Identity tie-in: from the same workspace, the final wizard step lists sampleIdentityStatements; the user can attach one identity statement to the new habit, the selected id is persisted in useAreteStore, and IdentityPanel reads it back under the matching identity per customer/04-arete/domains-arete-habits.md.
  • Confirm scope, ritual phrasing, frequency, reminder cadence
  • Back affordance: onBack returns to view === 'dashboard'
  • Verify: customer/04-arete/domains-arete-habits.md view branches and HabitLoopWizard behaviour

5. Save the habit#

Per customer/04-arete/domains-arete-habits.md the internal workspace now writes the same BFF habit-create contract that feeds the polished /arete room, then mirrors the BFF id into the local store for the same-browser dashboard/Home handoff.

  • Submit the wizard
  • Wizard builds a HabitData object from Cue/Routine/Reward, POSTs /v1/arete/habits, stores the returned habitId in the Arete Zustand store (localStorage["oshun.arete"]), closes, and returns to view === 'dashboard'. If an identity statement was selected, the outgoing whyItMatters summary includes it and the local habit also keeps identityStatements: [identityId].
  • New habit row appears in HabitDashboard
  • Browser wizard POSTs to /v1/arete/habits; the polished /arete hub reads the same habit back through /v1/arete/room, including from a second browser context with no Arete localStorage seeded
  • The polished /arete/habits/new proposal form's browser field contract covers label, cadence, domain source, and rationale; Playwright uses the same values in an authenticated real POST /v1/arete/habits, verifies the held-for-review response, confirms authenticated GET /v1/arete/habits reads the proposed habit back with streakDays: 0 and todayKept: false, and reloads /arete/habits to prove the page renders the live BFF row.
  • Telemetry: arete_habit_created fires after the browser wizard receives a successful BFF habitId, carrying the persisted id, label, cadence, source surface, cue type, duration, and identity-binding metadata.

6. Return to the polished /arete hub#

The polished room and the internal habit system remain separate surfaces, but the created habit now shares a BFF-backed source of truth.

  • Navigate to /arete via shell-nav "Today" or direct URL
  • Intention rows render — the just-created habit appears from /v1/arete/room
  • The row's detail link points back into /domains/arete/habits?habit=...
  • Verify: customer/04-arete/arete.md Data & contracts — /arete reads real member habits and week stats from /v1/arete/room

7. First check-in#

  • On /arete, locate the new habit row read back through /v1/arete/room
  • In the polished hub, the action is a detail link to /domains/arete/habits?habit=<label>; it is not a check-in control. The check-in affordance therefore lives in the internal surface today
  • On /domains/arete/habits dashboard, tap the per-habit toggle for the wizard-created habit
  • The toggle POSTs /v1/arete/habits/:habitId/check-in with status: "done", applies the BFF streak.currentDays / completedToday result to useAreteStore, and changes the dashboard toggle to the rest-day amend affordance
  • Verify: customer/04-arete/domains-arete-habits.md Interactions → "Per-habit toggle"

8. Streak begins#

  • After the first check-in, the habit's streak transitions from 0 to 1 in the dashboard/local continuity store from the BFF response
  • Return to /arete; the intention row shows Done today, 1-day streak, and the weekly KPI reads 1 / 7 completed across 1 ritual
  • Open /arete in a second browser context using the same real member session but no oshun.arete local continuity store; the intention row still shows Done today, 1-day streak, and 1 / 7 via the fresh authenticated /v1/arete/room BFF read
  • Navigate to /arete/streak (direct URL; the per-view file customer/04-arete/arete-streak.md flags that /arete intention rows show a streak eyebrow but no href is wired)
  • The phone-shell AreteStreak surface reads the live /v1/arete/streak rollup: a fresh member renders an honest 0 days, and the real first check-in path reads back 1 day on /arete/streak
  • The 42-cell heatmap on /arete/streak marks today's cell with today state (ink fill with 2px accent border) — the per-view file flags that today-state colour is ink with accent border
  • Promise copy "Two rest days a week are part of the practice" remains visible — humane recovery requirement per customer/04-arete/arete-streak.md

9. Return to home and confirm rail update#

  • Click shell-nav Home (or g h)
  • HomeAretePracticeSection rail now includes the new locally created habit as the active continuation commitment, even when the Arete Home BFF endpoint is unavailable
  • The primary continuation CTA targets the Arete habits lane (verify against customer/02-home-discovery/home.md Arete practice section)
  • After the first check-in, the live Home Arete rail reads the local continuity store back as 1d streak • done today
  • HomeFootingSummaryStrip ([data-kpi-grid]) reflects the same local Arete check-in by adding the pending same-day completion to the live Home practice-streak metric; legacy KpiGrid remains preview-only per customer/02-home-discovery/home.md
  • Verify: customer/02-home-discovery/home.md Arete practice section

10. Coach-card observation (lagged)#

The Sunday letter on /arete/coaching proposes adjustments based on patterns Lilith named. New habits should not trigger an adjustment until at least one week of data is in.

  • Navigate to /arete/coaching (direct URL — the per-view file customer/04-arete/arete-coaching.md flags that the coach card from /arete is not wired to here)
  • The letter does not yet mention the new habit (week-old data requirement)
  • On the next Sunday boundary, Lilith may surface an adjustment in the letter

Post-conditions#

  • A new habit row exists in the BFF habit store, is mirrored into the local continuity store, and is visible on both the internal dashboard and polished Arete room
  • Streak invariant: first check-in produces streak = 1 (not 0, not 2)
  • Home HomeAretePracticeSection rail reflects the locally created habit as the current continuation commitment for the same browser profile
  • No double-write: navigating between /arete and /domains/arete/habits and back does not create duplicate habits
  • Telemetry: arete_habit_created is verified for the BFF-backed wizard create. The broader sequence remains candidate instrumentation: arete_hub_viewedarete_habit_create_startedarete_habit_checkin_firstarete_streak_started

Failure modes to verify#

  • Formerly inert "+ New ritual" control on /arete — fixed 2026-05-26 via href="/domains/arete/habits?wizard=loop" on the LBtn. If the link regresses, the polished room cannot start the create flow.
  • Formerly inert coach-card decisions — fixed 2026-05-26 via the AreteCoachActions client component (5-test suite). Decisions POST /v1/arete/coach/decision and read back through /v1/arete/room.
  • Wizard Create was a no-op — fixed 2026-06-23. Create Habit now builds a habit, POSTs it to /v1/arete/habits, persists the returned BFF id to useAreteStore, returns to the dashboard, renders the new row, and reads back on the polished /arete hub.
  • Home rail ignores the wizard-created habit — fixed 2026-06-23. HomeAretePracticeSection passes useAreteStore.habits into buildHomeAretePracticeModel; the model treats active local habits as Home commitments, promotes the newest local habit into the continuation card, and routes the primary CTA to the Arete habits lane. This is same-browser local read-back only, not a BFF/cross-device sync claim.
  • First check-in stayed local-only for wizard-created habits — fixed 2026-06-23. Persisted wizard habits now POST /v1/arete/habits/:habitId/check-in, apply the BFF streak/today result to the dashboard and useAreteStore, read back as Done today / 1-day streak / 1 / 7 on /arete, and surface 1d streak • done today on the live Home Arete rail. The live Home footing strip also adds the pending same-day local Arete completion to its practice-streak metric while profile sync is still pending.
  • Local-only sample-habit toggles in /domains/arete/habitssampleHabits check-ins mutate component state only because their fixture ids do not exist in the member's BFF habit store; Playwright now asserts the seeded toggle flips aria-pressed, restores on a second click, emits no /v1/arete/habits/:habitId/check-in POST, and creates no persisted local habit.
  • Namespace drift between /arete and /domains/arete/habits for newly created habits — fixed 2026-06-23. The wizard writes /v1/arete/habits and the polished room reads /v1/arete/room; seeded sample rows remain local to the internal workspace.
  • Formerly inert /arete/streak "Adjust" and "Begin" controls — both are wired as navigation links (/arete/plan?intent=adjust and /tara/sit/when-the-mind-wanders); no direct streak-mutation write exists on the streak surface
  • LSubNav items lacked href — fixed 2026-05-26. AreteRoom's sub-nav items now carry hrefs: Habits → /domains/arete/habits, Journal → /domains/arete/journal, Weekly review → /arete/review, Patterns → /arete/patterns.
  • No loading.tsx or error.tsx on /arete — per-view file flags this; a failing getArete() falls through to the nearest ancestor boundary (or global error)
  • Streak increment race for wizard-created habits — fixed 2026-06-23. The persisted-habit toggle writes the BFF check-in first, then /arete reads the same check-in via /v1/arete/room. Seeded sample habits remain internal-only and are not represented on /arete.
  • Polished proposal form field contract was not tied to the real BFF habit contract — fixed 2026-06-27. /arete/habitsPropose a habit now has Playwright coverage that drives /arete/habits/new, verifies the browser field semantics, posts the same payload through authenticated real HTTP, reads the proposed habit back through authenticated GET /v1/arete/habits, and reloads the browser page to prove live server-rendered read-back.

E2E coverage#

  • apps/oshun/web/e2e/arete-create-habit.spec.ts — the habit dashboard /domains/arete/habits (the HabitLoopWizard opens via "New Habit" and Back returns to the dashboard; /domains/arete/habits?wizard=loop lands directly in the wizard; Cue→Routine→Reward POSTs the real BFF habit-create route, persists the returned BFF habit id locally, emits browser telemetry event arete_habit_created with the persisted habit id and habit-loop metadata, renders the new row, persists a selected sampleIdentityStatements id locally, asserts the outgoing BFF whyItMatters identity line, and reads the created habit back in IdentityPanel, POSTs the first check-in to /v1/arete/habits/:habitId/check-in, applies the returned streak locally, reads back Done today / 1-day streak / 1 / 7 on polished /arete, opens a second browser context with the same real member session and no oshun.arete local store to prove /arete rehydrates the checked-in habit from /v1/arete/room, reads back 1d streak • done today on Home Arete, and reads the checked-in habit's live overall rollup back on /arete/streak as 1 day; the same return-to-Home path asserts HomeFootingSummaryStrip ([data-kpi-grid]) increments the practice-streak metric from the browser's current profile streak plus the pending local Arete check-in; the seeded per-habit toggle is still driven as a fixture-only boundary — a click inverts its aria-pressed state, a second click restores it, no BFF check-in POST leaves the browser, and no persisted local habit is created); the polished /arete/habits proposal lane (Propose a habit/arete/habits/new field contract → same payload real BFF POST → authenticated BFF GET read-back with zero streak/no kept-today state → browser /arete/habits live row read-back); and the /arete/streak surface (no-punish promise copy, live zero-streak count + gently-kept lede for a fresh member, 42-cell six-week heatmap with one accessible today marker at cell 41, legend, and the wired Adjust/arete/plan?intent=adjust / Begin/tara/sit/when-the-mind-wanders resumption affordances).
  • apps/oshun/web/e2e/arete-full-journey.spec.ts — drives the older broad Arete path, including the controlled accepted-response "held for coach review" acknowledgement plus coach Approve / "Not this week" decision + undo. The proposal form's real-BFF read-back now lives in arete-create-habit.spec.ts.
  • apps/oshun/web/e2e/arete-habits-bff.spec.ts — the genuine server habit-create engine, over real HTTP against the running BFF: POST /v1/arete/habits persistence + GET round-trip, and the 422 habit_label_too_short validation rejection. The same spec now proves the live check-in loop and guardrails: create/check-in 401 auth gates, invalid check-in status 422, owner-only check-in with non-enumerating 404 for another member, no-store mutation responses, durable done read-back into todayKept/streakDays, and same-day amendment to an explicit rest day (skipgrace) with the list returning to todayKept: false.
  • Coverage depth: partial — see coverage.md.
  • Uncovered: cross-device check-in sync is now covered for the polished /arete BFF read model, and /arete/streak now reads the live /v1/arete/streak rollup for both fresh-zero and first-check-in states. Home's Arete continuation remains intentionally same-browser local continuity, and legacy KpiGrid habits-this-week coverage is not asserted because KpiGrid is not part of the live Home surface. Remaining partial-depth items are the older manual checklist gaps for reminder-cadence/scope confirmation, lagged Sunday coaching observation, and the /arete missing loading.tsx / error.tsx boundary. The polished-hub intention rows are also exercised by arete-full-journey.spec.ts.

Per-view files touched by this journey#

Cross-references#

  • Feature spec: V1/features.md#arete
  • Architecture: V1/ARCHITECTURE.md#customer-web--appsoshunweb
  • Related journeys:
  • Shell:
  • Component sources:
    • apps/oshun/web/src/app/arete/page.tsx — polished hub entry
    • apps/oshun/web/src/components/lilith/rooms.tsxAreteRoom
    • apps/oshun/web/src/components/lilith/arete.tsxAretePlan, AreteStreak, AreteGoal, AreteCoaching
    • apps/oshun/web/src/components/domains/arete/AreteHabitSystem.tsx — internal workspace + nine subviews
    • apps/oshun/web/src/lib/lilith-data/arete.tsgetArete()
    • apps/oshun/web/src/lib/arete/arete-simulation-data.tssampleHabits
    • apps/oshun/web/src/lib/arete/arete-types.tsHabitData, IdentityStatement, CueType, RewardType, etc.

Open questions / known gaps#

  • Confirm whether /arete and /domains/arete/habits share a store in V1 for created habits — the wizard POSTs /v1/arete/habits and /arete reads back through /v1/arete/room; internal sample rows remain local
  • Connect the browser HabitLoopWizard to the actual habit-create endpoint (POST /v1/arete/habits)
  • Document whether the internal namespace's local-store wizard is meant to graduate into the polished /arete/habits/new BFF-backed create flow: it now shares the same BFF create route while retaining the richer local Cue/Routine/Reward metadata for same-browser Home continuity
  • Wire coach-card Approve / Not this week decisions to the BFF — decisions POST /v1/arete/coach/decision and read back through /v1/arete/room
  • Confirm whether the streak increment is a synchronous client mutation or a BFF round-trip — persisted wizard habits POST /v1/arete/habits/:habitId/check-in; seeded sample rows remain local-only
  • Document the identity-statement → habit binding contract: a wizard-created habit may attach one IdentityStatement id from sampleIdentityStatements; the selected statement is included in the BFF whyItMatters summary, and the id is stored in the local Arete continuity store and shown by IdentityPanel. The current BFF habit route does not expose a first-class identity column.