# Zync OS Mobile (Plan B) Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: executed via run-plan harness (codex seats). Steps use checkbox (`- [ ]`) syntax for tracking. Audience: AI coding agents first.

**Goal:** Mobile OS shell (home/dock/drawer/full-screen apps/recents/shade) + module migration sweep so every in-scope module opens windowed (desktop) and full-screen (mobile).

**Architecture:** Extends the LANDED Plan A desktop shell (`apps/zync-app/src/os/**`). New `MobileShell` + mobile store share registry/url-sync/persistence/schema with desktop. Spec is source of truth: `docs/specs/2026-07-11-zync-os-mobile-design.md` (SHIPPED, 9 codex review rounds) — every task below POINTS to its spec section; the spec pins the contracts, this plan pins files/waves/acceptance.

**Tech Stack:** React + zustand + TanStack Query, Hono (zync-api), Workbox SW, Playwright (+WebKit projects), zod.

**Design doctrine:** `.claude/skills/zc-ui-ux-designer/` (SKILL.md + references/mobile.md + desktop.md) — REQUIRED READING for every UI task; vision judge grades against it. Specs/plans never restate physics.

---

## Wave Plan

| Wave | Tasks | Files touched (primary) | Safe to parallelize? |
|------|-------|------------------------|----------------------|
| 1 | T1 schema-unify, T2 blocker-plane, T4 manifest, T5 audit, T6 skill-align, T7 sw-isolation, T8 sheet+dialog | disjoint: packages/modules+api route / packages/ui shell + os/guards.ts / packages/modules manifest.ts+registry-os.ts / docs artifact / mobile.md / sw.ts / packages/ui dialog | ✅ no overlap (T1 owns modules/shell-layout-schema.ts, T4 owns manifest.ts — different files) |
| 2 | T3 mutation-sweep, T9 shell-mode, T10 mobile-store, T11 url-sync | app-wide codemod / shell-mode.ts+migration+settings / os/mobile/mobile-shell-store.ts / url-sync.ts+OsShell.tsx | ✅ T3 codemod excludes src/os/** (T9-T11 territory); enforced in task |
| 3 | T12 surfaces-core, T13 surfaces-panels | os/mobile/{MobileShell,MobileHome,MobileAppIcon,MobileDock,AppDrawer,MobileAppFrame,MobileNavigationBar}.tsx / os/mobile/{RecentApps,NotificationShade,MobileTopStrip}.tsx + command-center variant | ✅ disjoint files |
| 4 | T14 verify-scaffold, T15 boot-wiring | playwright configs+tests / main.tsx+coach marks+telemetry | ✅ no overlap |
| 5 | T16–T25 cluster sweep (10 clusters) | disjoint per cluster: bindings/<module>.ts + module route dirs + tests | ✅ manifest.ts frozen (T4 wrote all metadata) |
| 6 | T26 final gate | none (verification) | single task |

## Decision gates

Run-level: `g1` binds `base_branch` = `master` (Plan A landed there; verify `a37d90d1` is ancestor of origin/master at pre-flight). No task-level gates: migrations follow standard forward-only flow; mobile-OS-default mirrors the user-decided Plan A pattern (OS default + Legacy checkbox).

---

### Task 1: Layout schema unification + persistence (mobile slot)

**Wave:** 1 · **Blocks:** T9, T10, T11 · **Blocked by:** —
**Files:** Create `packages/modules/src/shell-layout-schema.ts`; Modify `apps/zync-api/src/routes/shell-layout.ts`, `apps/zync-app/src/os/shell-layout-schema.ts` (→ re-export), `apps/zync-app/src/os/OsShell.tsx` (mirror keys).
**Contract:** spec §6 (read in full). Seams: shared zod source (envelope `{v:1, writer, committedAt, data}` kept; per-device strict `data` schemas — desktop = landed fields + `onboarding`; mobile = `{homeIcons, dock, widgets, onboarding?}`); route widens to `device ∈ {'desktop','mobile'}` (unknown → 400); localStorage mirrors principal-keyed `zync.shell.<tenantId>.<userId>.<deviceClass>` with legacy `zync.shell.layout` read-once migration + delete.
**Behavior:** ends server/client schema drift; desktop behavior unchanged (409 rule, commit points, reset).
**Acceptance:** `pnpm --filter zync-api test -- shell-layout` PASS (incl. device=mobile round-trip + onboarding accepted) + existing desktop suites green.
- [ ] Write failing route tests (mobile device, onboarding field, unknown device 400)
- [ ] Implement shared schema + route + client re-export + mirror keys
- [ ] Run acceptance; commit

### Task 2: canDiscard blocker signal plane

**Wave:** 1 · **Blocks:** T3, T10, T12, T13 · **Blocked by:** —
**Files:** Create `packages/ui/src/shell/blocker-context.ts`, `packages/ui/src/shell/use-shell-blocker.ts`, `packages/ui/src/shell/use-app-mutation.ts`, `apps/zync-app/src/os/guards.ts`; Modify `apps/zync-app/src/os/window-router.tsx` (provider), shared `Form`/dialog layer in `packages/ui`.
**Contract:** spec §3 signal plane (read in full). Seams: `ShellBlockerContext` (null default, defined in ui, provided by app), `registerBlocker(reason) → release`, `useShellBlocker(reason, active)`, `useAppMutation` (useMutation signature + auto blocker), `canDiscard(instanceId) → {ok} | {ok:false, reason}`, precedence mutation > dirty > keep-alive.
**Acceptance:** `pnpm --filter zync-app test -- guards` PASS (unit matrix: register/release/unmount-cleanup/precedence/no-op-outside-provider).
- [ ] Write failing unit matrix; implement; run; commit

### Task 3: Mutation codemod sweep + ESLint gate

**Wave:** 2 · **Blocks:** — · **Blocked by:** T2
**Files:** Modify ~121 files under `apps/zync-app/src/**` EXCLUDING `src/os/**` (waves 2 own it; os/ files use `useAppMutation` from the start); ESLint config.
**Contract:** spec §3 mutation bullet. Mechanical: `useMutation` → `useAppMutation` (import + identifier only, options untouched); `no-restricted-imports` rule bans `useMutation` from `@tanstack/react-query` in `apps/zync-app/src/**` (wrapper exempt).
**Acceptance:** `grep -rn "useMutation" apps/zync-app/src --include=*.tsx --include=*.ts | grep -v use-app-mutation` → 0 lines; lint green; typecheck green.
- [ ] Codemod; run acceptance + full typecheck/lint/test; commit

### Task 4: Manifest metadata (single owner) + ownership invariant

**Wave:** 1 · **Blocks:** T5-verify, T10, T11, T16–T25 · **Blocked by:** —
**Files:** Modify `packages/modules/src/manifest.ts`, `apps/zync-app/src/os/registry-os.ts` (+ its invariant test file).
**Contract:** spec §2 (read in full — the NORMATIVE ownership table + `mobile` field shape + `UNOWNED_ROUTE_PREFIXES` + redirect-alias rule). ALL `os` + `mobile` metadata for the sweep written HERE; removes `/profile/notifications` from notifications prefixes; both files then FROZEN for the plan.
**Acceptance:** ownership completeness vitest green (every top-level prefix owned-by-exactly-one or allowlisted; redirect-only exempt); typecheck green.
- [ ] Write invariant test; add all metadata per table; run; commit

### Task 5: Audit task (route registry + violations + dirty editors)

**Wave:** 1 · **Blocks:** T16–T25 partition · **Blocked by:** —
**Files:** Create `docs/plans/audit/zync-os-mobile-sweep-audit.md` (artifact only, no src changes).
**Contract:** spec §7(a). Verify §2 table against live route registry (conflict → ESCALATE, never re-decide); per-module violation counts (`location.state`, `document.title`, `window.history`, classic-chrome assumptions); ad-hoc dirty-editor work-list (spec §3); cluster partition for T16–T25.
**Acceptance:** artifact exists with per-cluster file lists + violation counts; zero unresolved table conflicts.
- [ ] Run greps/registry read; write artifact; commit

### Task 6: Skill alignment (mobile.md)

**Wave:** 1 · **Blocks:** T12, T13 · **Blocked by:** —
**Files:** Modify `.claude/skills/zc-ui-ux-designer/references/mobile.md`.
**Contract:** spec §7(a2): registry presentation is fullscreen-only; 'sheet' language applies to dialog adaptation only. Doctrine and spec must not disagree.
**Acceptance:** grep mobile.md for registry-level sheet claims → 0.
- [ ] Amend; verify; commit

### Task 7: SW cache isolation (BLOCKER fix)

**Wave:** 1 · **Blocks:** T26 · **Blocked by:** —
**Files:** Modify `apps/zync-app/src/sw.ts`, `apps/zync-app/src/hooks/useServiceWorker.ts` (purge message), logout flow call site; Create e2e spec under `apps/zync-app/tests/`.
**Contract:** spec §8 (read in full): `/api/*` NetworkOnly; authed files NetworkOnly; image/font CacheFirst ONLY same-origin `/assets/ /icons/ /fonts/` as `static-assets-v2`; activate deletes `api-cache`+`files-cache`+`static-assets-v1`; `{type:'purge-user-caches'}` with MessageChannel ack.
**Acceptance:** cross-user offline e2e PASS (API + PDF + protected image none served post-logout).
- [ ] Failing e2e; implement; run; commit

### Task 8: BottomSheet + presentation context + dialog adapter

**Wave:** 1 · **Blocks:** T12, T13 · **Blocked by:** —
**Files:** Create `packages/ui/src/sheet/BottomSheet.tsx`, `packages/ui/src/shell/presentation-context.ts`; Modify `packages/ui` Dialog/Modal; annotate destructive call sites (grep-derived, complete).
**Contract:** spec §4 (BottomSheet in ui) + §7(c): `presentation?: 'sheet'|'modal'|'auto'`; `'auto'` = sheet when `ShellPresentationContext === 'mobile-frame'`; destructive confirms annotated `presentation="modal"`, non-destructive untouched. Physics per mobile.md detents.
**Acceptance:** adapter integration fixture PASS incl. modal passthrough; destructive-annotation grep list committed in test.
- [ ] Failing fixture; implement; annotate; run; commit

### Task 9: Shell mode × device class

**Wave:** 2 · **Blocks:** T12, T15 · **Blocked by:** T1
**Files:** Modify `apps/zync-app/src/os/shell-mode.ts`; Create migration `00XX_ui_shell_device.sql` (next free number, `user_preferences.ui_shell_device` CHECK-constrained default `'auto'`); Modify preferences PATCH route + Interface Settings UI.
**Contract:** spec §1 (read in full). Delete the `<768 || coarsePointerOnly → classic` early-return; same predicate becomes the class fork; precedence unchanged; no hot-swap on resize (toast + Reload); `shell_boundary_crossed` counter.
**Acceptance:** shell-mode unit matrix PASS (class × pref × override × force grid).
- [ ] Failing matrix; implement + migration; run; commit

### Task 10: Mobile shell store

**Wave:** 2 · **Blocks:** T12, T13 · **Blocked by:** T1, T2, T4
**Files:** Create `apps/zync-app/src/os/mobile/mobile-shell-store.ts` (+ test).
**Contract:** spec §3 (read in full): `MobileApp`/`MobileShellState` shapes verbatim; actions; ONE instance per moduleId; cap 6 + LRU-unmount through `canDiscard`; cap-exhaustion reject + toast + `app_cap_hit`; panels max-one-true.
**Acceptance:** store unit matrix PASS (MRU, cap+guards+exhaustion, panels exclusivity).
- [ ] Failing matrix; implement; run; commit

### Task 11: url-sync mobile branch + history state

**Wave:** 2 · **Blocks:** T12 · **Blocked by:** T1, T4
**Files:** Modify `apps/zync-app/src/os/url-sync.ts`, `apps/zync-app/src/os/OsShell.tsx` (desktopState migration), `apps/zync-app/src/os/shell-mode.ts` (`/home` alias rewrite — coordinate: T9 same wave DIFFERENT lines; if conflict risk flagged by audit, fold alias rewrite into T9).
**Contract:** spec §5 (read in full): `zync`-wrapped union gains `surface`; `/home` push/replace dedup; popstate matrix; cold-entry replaceState (no synthesized home entry); deep-link + push-tap resolution.
**Acceptance:** url-sync unit matrix PASS (home dedup, back-from-home, per-app pop, surface popstate, cold entry).
- [ ] Failing matrix; implement; run; commit

### Task 12: Mobile surfaces — shell core

**Wave:** 3 · **Blocks:** T14 · **Blocked by:** T6, T8, T9, T10, T11
**Files:** Create `apps/zync-app/src/os/mobile/{MobileShell,MobileHome,MobileAppIcon,MobileDock,AppDrawer,MobileAppFrame,MobileNavigationBar}.tsx`.
**Contract:** spec §4 surface contracts (read in full) + mobile.md physics (REQUIRED READING). MobileAppFrame hosts WindowRouter, provides `ShellPresentationContext='mobile-frame'` + blocker provider; nav bar Back/Home/Recents semantics per §5.
**Acceptance:** behavioral probes PASS (open from icon/drawer/dock, back semantics, drawer search); axe clean.
- [ ] Implement against probes; run; commit

### Task 13: Mobile surfaces — panels

**Wave:** 3 · **Blocks:** T14 · **Blocked by:** T6, T8, T10
**Files:** Create `apps/zync-app/src/os/mobile/{RecentApps,NotificationShade,MobileTopStrip}.tsx`; Modify command-center panel (full-screen variant), toast adapter (top position variant).
**Contract:** spec §4 (recents cards + guarded dismiss; shade sections top→bottom incl. timer chip + quick settings; command center = SAME implementation, presentation variant; toasts top).
**Acceptance:** probes PASS (recents dismiss above/below threshold + guarded confirm sheet, shade pull detents, mark-all); axe clean.
- [ ] Implement against probes; run; commit

### Task 14: Verification scaffold (mobile projects)

**Wave:** 4 · **Blocks:** T16–T26 · **Blocked by:** T12, T13
**Files:** Modify playwright config (+6 mobile projects incl. 2 WebKit); Create gesture probes, visual baselines (4 Chromium projects), slopgate glob additions, PWA named checks (manifest zod test, SW update e2e, offline fallback e2e).
**Contract:** spec §Verification program (read in full).
**Acceptance:** full mobile suite runs green on the shell built so far; baselines recorded.
- [ ] Scaffold; record baselines; run; commit

### Task 15: Boot wiring + lazy split + usability

**Wave:** 4 · **Blocks:** T26 · **Blocked by:** T9
**Files:** Modify `apps/zync-app/src/main.tsx` (MobileShell lazy chunk), coach marks (3 steps, flag in mobile payload `onboarding`), shade escape hatches, telemetry counters, diagnostic copy.
**Contract:** spec §1 boot + §Usability. Build-artifact assertion: no WM code in mobile chunk / no mobile chunk in desktop session.
**Acceptance:** build assertion script green; coach-marks e2e PASS.
- [ ] Implement; run; commit

### Tasks 16–25: Module migration sweep (one task per cluster)

**Wave:** 5 · **Blocks:** T26 · **Blocked by:** T4, T5, T14 (clusters partitioned by T5's artifact; 10 clusters: projects · calendar · time_management(+`/time/approvals`) · invoices(+receipts,payments) · expenses(+vendors) · crm · marketing(+proposals,contracts) · kb · reports(+analytics) · contractor_payouts + settings-extras(integrations,profile,billing)).
**Files per cluster (disjoint):** Create `apps/zync-app/src/os/bindings/<module>.ts`; Modify that cluster's route dirs only; Create playwright spec.
**Contract:** spec §7 DONE definition (4 items, read in full) — bindings (`titleFromRoute`, badge/widget ONLY where query exists), violations fixed (zero `location.state`/`window.history`/`document.title`, dirty editors wired via `useShellBlocker`), playwright windowed+fullscreen core flow, vision keyframes at wave close.
**Acceptance per cluster:** cluster playwright spec PASS (desktop + mobile projects) + violation greps 0 + classic suites green.
- [ ] Per cluster: bindings → fix violations → spec → run → commit

### Task 26: Final gate

**Wave:** 6 · **Blocked by:** ALL
**Contract:** spec §Acceptance criteria 1–12 walked one by one; vision judge batch (codex models, never Claude); iOS manual checklist artifact recorded for the user to execute; fault-injection clean.
**Acceptance:** all 12 criteria checked with evidence links in run log.
- [ ] Walk criteria; record; commit

---

## Self-review notes

- Spec coverage: §1→T9/T15, §2→T4/T5, §3→T2/T3/T10, §4→T8/T12/T13, §5→T11, §6→T1, §7→T5/T6/T8/T16-25, §8→T7, verification→T14/T26, usability→T15. No gaps.
- Same-wave file overlap: T9/T11 both name `shell-mode.ts` — T11 carries an explicit fold-into-T9 escape hatch; all other pairs disjoint.
- Seam consistency: `useAppMutation`/`useShellBlocker`/`canDiscard`/`ShellPresentationContext` names identical across T2/T3/T8/T10/T12/T16-25 and the spec.
- No task bodies: contracts point to spec sections (single source of truth), plan pins files/waves/acceptance only.
