diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 1bf7b27..82e6ddb 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -206,6 +206,25 @@ Make FamilySync configurable, administrable, and maintainable for real multi-mem
**Plans**: TBD
**UI hint**: yes
+### Phase 13: Real Lint Gate (ESLint)
+
+**Goal**: The CI `fast-checks` lint step stops being a hollow no-op — a real, type-aware ESLint flat config (typescript-eslint `recommendedTypeChecked` + React/react-hooks for the PWA) plus Prettier are wired across both apps, all first-run violations are genuinely fixed, and `pnpm lint` / `pnpm format:check` actually fail on violations and gate PRs to main.
+**Mode:** standard
+**Depends on**: Phase 8 (Gitea CI shipped the lint gate slot wired to auto-activate once a package `lint` script lands; this phase makes it real and adds the format gate). Independent of Phases 9/10/11/12.
+**Requirements**: none (promoted from backlog 999.16; no formal REQ-IDs)
+**Success Criteria** (what must be TRUE):
+
+ 1. **SC-1:** `pnpm lint` exits non-zero on an introduced violation (today it exits 0); `pnpm format:check` exits non-zero on an unformatted file.
+ 2. **SC-2:** The CI `fast-checks` lint step blocks a PR to main on lint violations, and a new `format:check` step blocks on format violations.
+ 3. **SC-3:** The first real run's existing violations are resolved so the baseline gate ends green — `pnpm lint` AND `pnpm format:check` both exit 0 across both apps.
+
+**Decisions** (LOCKED, from 13-CONTEXT.md): D-13-01 `recommendedTypeChecked` via `projectService:true`; D-13-02 React/hooks plugins scoped to `apps/pwa` only; D-13-03 no strict presets; D-13-04 `--max-warnings 0`; D-13-05 fix all violations now; D-13-06 fixes address, never mask (justified suppressions only); D-13-07 Prettier standalone + `eslint-config-prettier`; D-13-08 isolated reformat commit; D-13-09 lint all TS/TSX incl. tests/e2e/config; D-13-10 `disableTypeChecked` override for non-project files.
+
+**Plans**: 3 plans
+- [ ] 13-01-PLAN.md — Install ESLint/Prettier deps + flat config + package scripts + prove the gate fails (SC-1)
+- [ ] 13-02-PLAN.md — Fix all first-run lint violations across both apps, green `pnpm lint` (D-13-05/06)
+- [ ] 13-03-PLAN.md — Prettier reformat (isolated commit) + CI format:check step + green baseline (SC-2, SC-3)
+
## Progress
| Phase | Milestone | Plans Complete | Status | Completed |
@@ -222,6 +241,7 @@ Make FamilySync configurable, administrable, and maintainable for real multi-mem
| 10. Admin Role & Settings | v1.1 | 0/? | Not started | - |
| 11. Per-Event Reminders | v1.1 | 0/? | Not started | - |
| 12. Initial Setup Wizard | v1.1 | 0/? | Not started | - |
+| 13. Real Lint Gate (ESLint) | v1.1 | 0/3 | Not started | - |
## Backlog
@@ -426,6 +446,8 @@ Plans:
**Context:** Raised during Phase 8 execution, 2026-06-11 — user noted the `--if-present` lint step "didn't fix the linter, just made it so it didn't have to exist to proceed" and wants a lint gate that actually fails. Tags: ci, lint, eslint, typescript-eslint, quality, gitea.
+> **Promoted into v1.1 Phase 13 (Real Lint Gate (ESLint)) — no formal REQ-IDs; scope expanded to include a Prettier `format:check` gate.** Backlog entry retained for history.
+
**Requirements:** TBD
**Plans:** 0 plans
diff --git a/.planning/phases/13-real-lint-gate-eslint/13-01-PLAN.md b/.planning/phases/13-real-lint-gate-eslint/13-01-PLAN.md
new file mode 100644
index 0000000..851917b
--- /dev/null
+++ b/.planning/phases/13-real-lint-gate-eslint/13-01-PLAN.md
@@ -0,0 +1,207 @@
+---
+phase: 13-real-lint-gate-eslint
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - package.json
+ - eslint.config.js
+ - .prettierrc
+ - .prettierignore
+ - apps/api/package.json
+ - apps/pwa/package.json
+ - pnpm-lock.yaml
+autonomous: true
+requirements: []
+must_haves:
+ truths:
+ - "ESLint flat config exists at repo root and loads without error (D-13-01: typescript-eslint recommendedTypeChecked via projectService:true; D-13-03: no strict/strictTypeChecked presets)"
+ - "React + react-hooks rules apply ONLY to apps/pwa/** globs; apps/api receives no React config (D-13-02)"
+ - "Every package lint invocation bakes --max-warnings 0 so any warning fails (D-13-04)"
+ - "Lint covers app src/, vitest tests, Playwright e2e specs, and config files (D-13-09); config + apps/api/tests files get a disableTypeChecked override so type-aware linting does not error on out-of-project files (D-13-10)"
+ - "Prettier is wired as a standalone gate (format / format:check scripts) with eslint-config-prettier/flat imported LAST in the flat config to disable conflicting formatting rules (D-13-07)"
+ - "Running `pnpm lint` on an introduced deliberate violation exits non-zero (SC-1); the throwaway file is deleted and never committed"
+ - "Running `pnpm format:check` on an introduced unformatted file exits non-zero (SC-1); the throwaway file is deleted and never committed"
+ artifacts:
+ - path: "eslint.config.js"
+ provides: "Root flat ESLint config: base type-aware block, pwa-react block, disableTypeChecked override block, prettier-last"
+ contains: "recommendedTypeChecked"
+ min_lines: 40
+ - path: ".prettierrc"
+ provides: "Prettier defaults matching existing single-quote codebase style"
+ contains: "singleQuote"
+ - path: ".prettierignore"
+ provides: "Prettier excludes (dist, node_modules, lockfile, generated migrations, html)"
+ - path: "package.json"
+ provides: "Root type:module + format/format:check scripts"
+ contains: "format:check"
+ - path: "apps/api/package.json"
+ provides: "api lint script mirroring typecheck idiom"
+ contains: "--max-warnings 0"
+ - path: "apps/pwa/package.json"
+ provides: "pwa lint script mirroring typecheck idiom"
+ contains: "--max-warnings 0"
+ key_links:
+ - from: "package.json (root lint script)"
+ to: "apps/api/package.json + apps/pwa/package.json lint scripts"
+ via: "pnpm -r --if-present lint"
+ pattern: "pnpm -r --if-present lint"
+ - from: "eslint.config.js"
+ to: "apps/api/tsconfig.json + apps/pwa/tsconfig.json + apps/pwa/tsconfig.e2e.json"
+ via: "parserOptions.projectService:true + tsconfigRootDir"
+ pattern: "projectService"
+---
+
+
+Install ESLint v9 + typescript-eslint + React/react-hooks + Prettier as root devDependencies, author the root flat `eslint.config.js`, add Prettier config, wire per-package `lint` scripts and root `format`/`format:check` scripts, and add `"type": "module"` to the root package.json. Then prove the gate actually fails (SC-1) with a throwaway-violation smoke test for BOTH `pnpm lint` and `pnpm format:check`, deleting the throwaway files (they must not be committed).
+
+This is the foundation wave: nothing fixes existing violations yet — `pnpm lint` is EXPECTED to report the codebase's real first-run violations at the end of this plan. That cleanup is Plan 02. This plan's success is: config loads, the gate fails on a deliberate violation, and the gate machinery is fully wired.
+
+Purpose: Replace the hollow no-op CI lint slot with a real, type-aware gate. Without this, `pnpm lint` exits 0 regardless of code quality.
+Output: eslint.config.js, .prettierrc, .prettierignore, root + per-package script changes, pnpm-lock.yaml updated. No source-code fixes.
+
+
+
+@$HOME/.claude/gsd-core/workflows/execute-plan.md
+@$HOME/.claude/gsd-core/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/13-real-lint-gate-eslint/13-CONTEXT.md
+@.planning/phases/13-real-lint-gate-eslint/13-RESEARCH.md
+@.planning/phases/13-real-lint-gate-eslint/13-PATTERNS.md
+@.planning/phases/13-real-lint-gate-eslint/13-VALIDATION.md
+
+# Files this plan reads/edits:
+@package.json
+@apps/api/package.json
+@apps/pwa/package.json
+@apps/api/tsconfig.json
+@apps/pwa/tsconfig.json
+@apps/pwa/tsconfig.e2e.json
+
+
+
+New files created in THIS plan:
+- `eslint.config.js` (root, ESM flat config)
+- `.prettierrc` (root)
+- `.prettierignore` (root)
+
+New scripts added in THIS plan:
+- root `package.json`: `format`, `format:check`, plus `"type": "module"`
+- `apps/api/package.json`: `lint`
+- `apps/pwa/package.json`: `lint`
+
+New devDependencies (root): eslint, @eslint/js, typescript-eslint, eslint-plugin-react, eslint-plugin-react-hooks, eslint-config-prettier, prettier.
+
+Downstream (later plans): Plan 02 fixes the violations this config surfaces; Plan 03 adds the CI `Format check` step and asserts the green baseline.
+
+
+
+
+
+ Task 1: Install lint/format devDependencies + author flat config + Prettier config
+ package.json, eslint.config.js, .prettierrc, .prettierignore, apps/api/package.json, apps/pwa/package.json, pnpm-lock.yaml
+
+ - 13-RESEARCH.md Pattern 1 (full flat-config skeleton, verbatim authority), Pitfalls 1/2/3/7, "Prettier Configuration" section, Standard Stack version table.
+ - 13-PATTERNS.md "eslint.config.js (root) — GREENFIELD" + the config-files override list + the typecheck→lint script-mirroring section.
+ - apps/api/tsconfig.json (confirms `exclude: ["tests"]` → apps/api/tests must go in the disableTypeChecked override), apps/pwa/tsconfig.json, apps/pwa/tsconfig.e2e.json.
+ - package.json (root) — existing `lint`/`typecheck` script idiom to mirror.
+
+
+ Install all lint/format packages as ROOT workspace devDependencies (single install so one eslint invocation sees every tsconfig for projectService) at the EXACT pinned versions from RESEARCH.md Standard Stack: eslint@9.39.4, @eslint/js@9.39.4, typescript-eslint@8.61.0, eslint-plugin-react@7.37.5, eslint-plugin-react-hooks@7.1.1, eslint-config-prettier@10.1.8, prettier@3.8.4. Use `pnpm add -D -w` from the repo root. DO NOT install eslint 10.x — it breaks eslint-plugin-react@7.37.5 at runtime (jsx-eslint/eslint-plugin-react#3977, "contextOrFilename.getFilename is not a function"); the 9.39.4 pin is non-negotiable per D-13-01 compatibility (Pitfall 1).
+
+ Add `"type": "module"` to the root package.json (PATTERNS.md: root has no CJS entry points, so this is safe and is the chosen route over a `.mjs` extension). This makes `eslint.config.js` parse as ESM.
+
+ Author `eslint.config.js` at repo root following RESEARCH.md Pattern 1 skeleton. Required structure, in order:
+ 1. Global `ignores` block: dist (`**/dist/**` — build output incl. apps/api/dist, apps/pwa/dist and the generated sw.js under dist), `**/node_modules/**`, generated Drizzle migrations (`apps/api/src/db/migrations/**`), `pnpm-lock.yaml`. (apps/api/dist must be ignored — grep already showed lint-able-looking JS there.)
+ 2. Base block — files `apps/**/*.{ts,tsx}`, extends `js.configs.recommended` + `tseslint.configs.recommendedTypeChecked` (D-13-01; NOT strict/strictTypeChecked per D-13-03), with `languageOptions.parserOptions.projectService: true` and `tsconfigRootDir: import.meta.dirname`. Add the `@typescript-eslint/no-unused-vars` rule override with `argsIgnorePattern: '^_'` + `varsIgnorePattern: '^_'` (RESEARCH no-unused-vars section).
+ 3. PWA-React block — files `apps/pwa/**/*.{ts,tsx}` ONLY (D-13-02), extends `reactPlugin.configs.flat.recommended` + `reactHooks.configs.flat.recommended`, `settings.react.version: 'detect'`. Explicitly turn OFF `react/prop-types` (TypeScript supersedes it in React 19 — RESEARCH Open Question 2, reduces first-run noise).
+ 4. disableTypeChecked override block (D-13-10) — files: `apps/api/drizzle.config.ts`, `apps/api/vitest.config.ts`, `apps/pwa/vite.config.ts`, `apps/pwa/vitest.config.ts`, `apps/pwa/playwright.config.ts`, AND `apps/api/tests/**/*.ts` (excluded from apps/api/tsconfig.json per Pitfall 2) — extends `tseslint.configs.disableTypeChecked`. Also add `eslint.config.js` itself if projectService complains about it.
+ 5. `eslint-config-prettier/flat` imported LAST in the array (D-13-07, Pitfall 7 — use the `/flat` import path, NOT bare `eslint-config-prettier`). Import the react plugin under the canonical name (no aliasing) so prettier-config can disable its formatting rules.
+ Do NOT use `--cache` anywhere — incompatible with type-aware linting (RESEARCH Performance / Anti-Patterns).
+
+ Author `.prettierrc` with RESEARCH.md defaults: `semi: true`, `singleQuote: true` (matches the existing single-quote codebase — avoids a needless massive diff), `tabWidth: 2`, `trailingComma: "all"`, `printWidth: 100`.
+ Author `.prettierignore`: `dist/`, `node_modules/`, `pnpm-lock.yaml`, `apps/api/src/db/migrations/`, `*.html`.
+
+ Add scripts:
+ - root package.json: `"format": "prettier --write ."`, `"format:check": "prettier --check ."`. Leave the existing root `"lint": "pnpm -r --if-present lint"` unchanged (it auto-activates once the package lint scripts below exist).
+ - apps/api/package.json: `"lint": "eslint src/ tests/ --max-warnings 0"` (mirror the existing `typecheck` script style: single command, directory-scoped, no wrappers). `--max-warnings 0` per D-13-04.
+ - apps/pwa/package.json: `"lint": "eslint src/ e2e/ --max-warnings 0"`. e2e/ is covered by tsconfig.e2e.json which projectService discovers automatically.
+
+
+ cd /home/luc/Projects/familysync && node -e "import('eslint').then(()=>console.log('eslint loads'))" && grep -q "recommendedTypeChecked" eslint.config.js && grep -q "projectService" eslint.config.js && grep -q "disableTypeChecked" eslint.config.js && grep -q "eslint-config-prettier/flat" eslint.config.js && grep -q "apps/pwa/\*\*" eslint.config.js && grep -q "apps/api/tests" eslint.config.js && grep -q '"type": *"module"' package.json && grep -q "singleQuote" .prettierrc && grep -q -- "--max-warnings 0" apps/api/package.json && grep -q -- "--max-warnings 0" apps/pwa/package.json && grep -q "format:check" package.json && echo ALL_CONFIG_PRESENT
+
+ eslint, typescript-eslint, react/react-hooks, eslint-config-prettier, prettier installed at pinned versions in root devDependencies (pnpm-lock.yaml updated). eslint.config.js exists with all five blocks in order (ignores → base recommendedTypeChecked → pwa-react → disableTypeChecked override incl. apps/api/tests → prettier-last). .prettierrc + .prettierignore exist. Root has `type: module` + format/format:check scripts. Both apps have a `lint` script with `--max-warnings 0`. The grep gate above prints ALL_CONFIG_PRESENT.
+
+
+
+ Task 2: Prove the gate fails (SC-1) — deliberate-violation smoke test for lint AND format, then delete throwaways
+ (throwaway, never committed) apps/api/src/_lint-gate-test.ts, (throwaway, never committed) _format-gate-test.ts
+
+ - 13-VALIDATION.md "Deliberate-Violation Test (ROADMAP success criterion 1)" + "Manual-Only Verifications" table.
+ - 13-RESEARCH.md "Deliberate-Violation Test" subsection (the exact throwaway file content).
+
+
+ Prove SC-1 — the gate is real, not a no-op. This is a one-time smoke proof; the throwaway files MUST NOT be committed.
+
+ Lint gate proof:
+ 1. Create `apps/api/src/_lint-gate-test.ts` containing a function that returns void and has an unhandled promise on its own line — `Promise.resolve(1)` with no await/void/.catch — which triggers `@typescript-eslint/no-floating-promises` under recommendedTypeChecked. (This file IS inside apps/api/src so it is in the tsconfig project and gets type-aware rules.)
+ 2. Run `pnpm lint` (or `pnpm --filter @familysync/api lint`). Capture the exit code. It MUST be non-zero AND the output MUST mention `no-floating-promises`. NOTE: the rest of the codebase's real violations will ALSO report here — that is expected at this wave; the proof is that this specific deliberate violation is detected and exit is non-zero.
+ 3. Delete `apps/api/src/_lint-gate-test.ts`.
+
+ Format gate proof:
+ 1. Create a temporary file at repo root `_format-gate-test.ts` with deliberately bad formatting that Prettier will reject — e.g. double quotes + no trailing semicolons + irregular indentation (opposite of the .prettierrc settings).
+ 2. Run `pnpm format:check`. Exit code MUST be non-zero and the output MUST list `_format-gate-test.ts`.
+ 3. Delete `_format-gate-test.ts`.
+
+ HARD RULE: both throwaway files are deleted in this task. Do NOT `git add` them. The SUMMARY must record both exit codes observed (non-zero) as evidence for SC-1. Do not attempt to make the whole `pnpm lint` exit 0 here — that is Plan 02's job.
+
+
+ cd /home/luc/Projects/familysync && test ! -e apps/api/src/_lint-gate-test.ts && test ! -e _format-gate-test.ts && ! git ls-files --error-unmatch apps/api/src/_lint-gate-test.ts 2>/dev/null && ! git ls-files --error-unmatch _format-gate-test.ts 2>/dev/null && echo THROWAWAYS_ABSENT_AND_UNTRACKED
+
+ SC-1 demonstrated: `pnpm lint` exited non-zero with `no-floating-promises` reported on the deliberate file, and `pnpm format:check` exited non-zero listing the unformatted throwaway. Both throwaway files are deleted and were never staged/committed. The verify gate prints THROWAWAYS_ABSENT_AND_UNTRACKED. Exit codes are recorded in the SUMMARY as SC-1 evidence.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| npm registry → repo devDependencies | New dev-tooling packages (eslint et al.) enter the supply chain at install time |
+
+This is a dev-tooling-only phase: no runtime, network, auth, or user-data surface is touched. STRIDE surface is limited to supply-chain (Tampering) at install time.
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Disposition | Mitigation Plan |
+|-----------|----------|-----------|-------------|-----------------|
+| T-13-SC | Tampering | pnpm devDependency installs (eslint, typescript-eslint, eslint-plugin-react, eslint-plugin-react-hooks, eslint-config-prettier, prettier) | accept | RESEARCH.md Package Legitimacy Audit ran the gate: all 7 packages map to canonical official GitHub repos with tens/hundreds of millions of weekly downloads; none flagged SLOP; no postinstall scripts; three SUS-flagged only on a too-new-release signal and explicitly approved (no human checkpoint required). Versions pinned exactly. |
+| T-13-01 | Tampering | flat config formatting-rule conflict | mitigate | Import `eslint-config-prettier/flat` LAST (D-13-07, Pitfall 7) so ESLint never re-reports formatting that Prettier owns — prevents a fix/format feedback loop. Verified by grep gate. |
+
+
+
+- `node -e "import('eslint')..."` resolves → eslint installed and importable.
+- All flat-config blocks present (grep gate in Task 1): recommendedTypeChecked, projectService, disableTypeChecked, eslint-config-prettier/flat, apps/pwa/** scoping, apps/api/tests override.
+- Both apps' lint scripts carry `--max-warnings 0`; root has format/format:check + type:module.
+- SC-1: deliberate lint violation → `pnpm lint` exits non-zero (no-floating-promises); deliberate unformatted file → `pnpm format:check` exits non-zero; both throwaways deleted and untracked.
+
+
+
+- ESLint flat config loads without error and is structured exactly per RESEARCH Pattern 1 (covers D-13-01/02/03/04/07/09/10).
+- Prettier config + scripts wired; root is `type: module`.
+- `pnpm lint` is now a REAL gate that fails on a deliberate violation (SC-1, lint half).
+- `pnpm format:check` fails on a deliberate unformatted file (SC-1, format half).
+- No throwaway test files committed.
+- (Out of scope here: making the whole repo lint/format-clean — that is Plans 02 and 03.)
+
+
+
diff --git a/.planning/phases/13-real-lint-gate-eslint/13-02-PLAN.md b/.planning/phases/13-real-lint-gate-eslint/13-02-PLAN.md
new file mode 100644
index 0000000..93302b0
--- /dev/null
+++ b/.planning/phases/13-real-lint-gate-eslint/13-02-PLAN.md
@@ -0,0 +1,180 @@
+---
+phase: 13-real-lint-gate-eslint
+plan: 02
+type: execute
+wave: 2
+depends_on:
+ - 13-01
+files_modified:
+ - apps/api/src/**
+ - apps/api/tests/**
+ - apps/pwa/src/**
+ - apps/pwa/e2e/**
+autonomous: true
+requirements: []
+must_haves:
+ truths:
+ - "Every first-run ESLint violation across both apps is resolved so `pnpm lint` exits 0 (D-13-05: fix all violations now; phase not done until green)"
+ - "Fixes ADDRESS the violation, never mask it: no blanket eslint-disable; no `void promise` used to silence a floating promise that should be awaited; every eslint-disable-next-line carries a justifying inline comment explaining why the rule is wrong HERE (D-13-06)"
+ - "Floating-promise findings on the push/outbox/reminder broker paths are reviewed as candidate bugs (real await missing?) BEFORE any `void` is applied; legitimate fire-and-forget setInterval ticks may use `void runX().catch(...)` only with that intent documented (D-13-06)"
+ - "ical.js no-unsafe-* findings in broker sync/expand are handled with targeted eslint-disable-next-line + a justification comment mirroring the EventForm.tsx:271-275 idiom (external-library weak-typing limitation, not a bug) (D-13-06)"
+ artifacts:
+ - path: "apps/api/src/broker/sync.ts"
+ provides: "ical.js unsafe-access findings resolved (narrowed or justified-suppressed)"
+ - path: "apps/pwa/src/components/EventForm.tsx"
+ provides: "no-explicit-any on the occurrence.recurrence cast handled via the pre-existing justified suppression"
+ key_links:
+ - from: "pnpm lint (root)"
+ to: "apps/api + apps/pwa lint scripts"
+ via: "exit code 0 after fixes"
+ pattern: "pnpm -r --if-present lint"
+---
+
+
+Run `pnpm lint` (real gate from Plan 01) and fix EVERY first-run violation across both apps until `pnpm lint` exits 0. Fixes must address violations, not mask them (D-13-06). Reformatting (Prettier) is explicitly NOT in this plan — Plan 03 owns the mechanical reformat as an isolated commit (D-13-08). Keep this plan's diff to genuine lint fixes so the reformat diff stays reviewable.
+
+Purpose: A lint gate that never passes is as useless as one that never fails. D-13-05 requires the codebase to actually be clean; real bugs (floating promises, misused promises, unsafe access) get genuinely fixed, not silenced.
+Output: lint-clean source across apps/api and apps/pwa. `pnpm lint` exits 0.
+
+
+
+@$HOME/.claude/gsd-core/workflows/execute-plan.md
+@$HOME/.claude/gsd-core/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/13-real-lint-gate-eslint/13-CONTEXT.md
+@.planning/phases/13-real-lint-gate-eslint/13-RESEARCH.md
+@.planning/phases/13-real-lint-gate-eslint/13-PATTERNS.md
+@.planning/phases/13-real-lint-gate-eslint/13-01-SUMMARY.md
+
+# The hot-spot source files (per RESEARCH First-Run Violations + PATTERNS):
+@apps/api/src/broker/sync.ts
+@apps/api/src/broker/expand.ts
+@apps/api/src/broker/outboxWorker.ts
+@apps/api/src/broker/poller.ts
+@apps/api/src/broker/reminderScheduler.ts
+@apps/pwa/src/components/EventForm.tsx
+
+
+
+No NEW files. This plan MODIFIES existing source to remove lint violations. New eslint-disable-next-line comments (each with a justification) may be added to broker files and EventForm.tsx. The deliverable is a state change: `pnpm lint` exit 0.
+
+Downstream: Plan 03 reformats everything with Prettier (isolated commit) and asserts the green baseline + CI format step.
+
+
+
+Read 13-01-SUMMARY.md FIRST. It contains the per-rule violation inventory `pnpm lint` produced in Plan 01. Use that inventory as the authoritative worklist — do not rediscover it from scratch. The classes RESEARCH anticipated (and how to fix each) are below; the SUMMARY tells you which actually fired and where.
+
+
+
+
+
+ Task 1: Fix apps/api violations (broker async paths + ical.js unsafe access + api tests)
+ apps/api/src/**/*.ts (notably broker/sync.ts, broker/expand.ts, broker/outboxWorker.ts, broker/poller.ts, broker/reminderScheduler.ts, routes/**), apps/api/tests/**/*.ts
+
+ - 13-01-SUMMARY.md — the violation inventory (authoritative worklist).
+ - 13-RESEARCH.md "First-Run Violations: Expected Findings and Correct Fixes" (no-unsafe-* family, no-floating-promises, no-misused-promises, require-await, no-unused-vars) + Pitfalls 4/5.
+ - 13-PATTERNS.md "apps/api/src/broker/*.ts" section (the existing setInterval+.catch pattern, the `void runX().catch(...)` legitimate form, the ical.js suppression idiom) + "eslint-disable-next-line with justification comment" shared pattern.
+
+
+ Iterate `pnpm --filter @familysync/api lint` until it exits 0. Fix by rule class, applying RESEARCH's right-fix-vs-mask guidance:
+
+ no-floating-promises / no-misused-promises (broker workers, route handlers): The three workers (outboxWorker.ts, poller.ts, reminderScheduler.ts) already use `setInterval(() => { runX().catch((err: unknown) => {...}) })`. If the rule fires on the `.catch()` return value, prepend `void`: `void runX().catch(...)` — legitimate fire-and-forget within a self-scheduling setInterval (Pitfall 4 / PATTERNS). HARD CONSTRAINT (D-13-06): before applying `void` to ANY promise on the push/outbox/reminder dispatch paths (e.g. dispatchEventChange, reminder fan-out, web-push send), review whether the promise should actually be `await`ed — a missing await there is a real error-swallowing bug, not noise. Only `void` a promise that is genuinely fire-and-forget, and only with that intent stated in a comment. For event-listener / addEventListener async callbacks (Pitfall 5), wrap as `() => { void asyncHandler() }` or extract a named function.
+
+ no-unsafe-* family (broker/sync.ts, broker/expand.ts): ical.js `getFirstPropertyValue()` returns `any`. Prefer narrowing via typeof/instanceof or an `ICAL.Time` cast where the value is then validated. Where narrowing is impractical because the library lacks types, use a targeted `// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access` (or the specific rule) WITH a justification comment in the EventForm.tsx:271-275 idiom — multi-line comment block stating "ical.js getFirstPropertyValue returns 'any'; caller immediately validates", then the disable line, then the code line, no blank lines between. sync.ts already has an explanatory comment at line 99 — only the disable line is the addition there.
+
+ require-await: remove `async` if the function has no await and returns a non-Promise, OR add the intended await. no-unused-vars: prefix intentionally-unused params/vars with `_` (the config already ignores `^_`); otherwise delete the dead binding.
+
+ apps/api/tests/**: these are linted with non-type-aware rules (disableTypeChecked override from Plan 01). Fix any no-unused-vars / syntactic violations there too. Three test files already carry `eslint-disable-next-line @typescript-eslint/ban-ts-comment` — leave those (already justified by intent of the @ts-expect-error usage; add a one-line justification comment if absent).
+
+ Do NOT run Prettier and do NOT reformat. Keep edits surgical — only touch lines that resolve a violation.
+
+
+ cd /home/luc/Projects/familysync && pnpm --filter @familysync/api lint; test $? -eq 0 && echo API_LINT_GREEN
+
+ `pnpm --filter @familysync/api lint` exits 0. Every broker floating/misused-promise finding was reviewed for a real missing-await bug before being voided; any `void` applied is genuine fire-and-forget with a documented intent. ical.js unsafe-access findings are either narrowed or carry a justified eslint-disable-next-line comment. No blanket disables; no reformatting. The verify gate prints API_LINT_GREEN.
+
+
+
+ Task 2: Fix apps/pwa violations (React/hooks rules + explicit-any + e2e specs)
+ apps/pwa/src/**/*.{ts,tsx} (notably components/EventForm.tsx, main.tsx, ErrorBoundary.tsx, sw.ts), apps/pwa/e2e/**/*.ts
+
+ - 13-01-SUMMARY.md — the violation inventory (pwa half).
+ - 13-RESEARCH.md First-Run Violations (react/display-name, no-explicit-any EventForm.tsx:275 case) + Pitfalls 6 (sw.ts webworker lib) and 8 (React import in main.tsx/ErrorBoundary.tsx is legit — rule should not fire).
+ - 13-PATTERNS.md "apps/pwa/src/components/EventForm.tsx" section — the suppression at lines 271-275 is PRE-EXISTING and already justified; verify it sits on the line immediately before the cast and that no other `as any` exists in the file.
+
+
+ Iterate `pnpm --filter @familysync/pwa lint` until it exits 0. By rule class:
+
+ no-explicit-any (EventForm.tsx ~line 275): the `(occurrence as any)?.recurrence` cast already has a justified `// eslint-disable-next-line @typescript-eslint/no-explicit-any` immediately above it (PATTERNS). Verify the disable comment is correctly positioned and that the rule no longer fires; do NOT add a second suppression. If any OTHER `as any` exists, narrow it with a proper type guard rather than suppressing.
+
+ react/display-name: add a displayName or convert anonymous memo/forwardRef components to named function expressions (`memo(function Foo(){...})`). react/prop-types: should be OFF already (Plan 01 disabled it for React 19) — if it still fires, confirm the off-rule landed in the pwa-react block. React namespace usage in main.tsx (``) and ErrorBoundary.tsx (`extends React.Component`) is legitimate (Pitfall 8) — `react/react-in-jsx-scope` is already disabled by flat.recommended; do not delete the React import.
+
+ react-hooks/exhaustive-deps: for each finding, ADD the genuinely-missing dependency (the real fix). Only suppress with `// eslint-disable-next-line react-hooks/exhaustive-deps` + a justification if adding the dep would cause an intentional one-shot effect to re-run — and state that reason (D-13-06). Do not blanket-disable hooks rules.
+
+ sw.ts (Pitfall 6): if type-aware rules error on ServiceWorker globals due to the `webworker` lib reference, prefer keeping it in-project; only if it genuinely cannot resolve, add sw.ts to a narrow disableTypeChecked override in eslint.config.js with a comment — but try narrowing first.
+
+ apps/pwa/e2e/**: covered by tsconfig.e2e.json (type-aware). Fix no-unused-vars / no-floating-promises in specs (Playwright actions are awaited; a missing await on a locator action is a real flake bug — fix it, don't void it).
+
+ Do NOT run Prettier and do NOT reformat. Surgical edits only.
+
+
+ cd /home/luc/Projects/familysync && pnpm --filter @familysync/pwa lint; test $? -eq 0 && echo PWA_LINT_GREEN
+
+ `pnpm --filter @familysync/pwa lint` exits 0. EventForm.tsx uses the single pre-existing justified suppression (no duplicate). exhaustive-deps findings got the real missing dependency where correct, suppressed only with a stated reason. The React import stays in main.tsx/ErrorBoundary.tsx. e2e missing-await findings fixed, not voided. No reformatting. The verify gate prints PWA_LINT_GREEN.
+
+
+
+ Task 3: Assert whole-repo lint green + typecheck/tests unbroken by the fixes
+ (verification only — no new edits expected)
+
+ - 13-VALIDATION.md "Sampling Rate" + Vitest passes-while-tsc-fails note (run tsc --noEmit, not just vitest).
+
+
+ Final guard for the wave. Run `pnpm lint` (root, both apps) and confirm exit 0. Then confirm the lint fixes did not break compilation or tests: run `pnpm typecheck` (tsc --noEmit for both apps — esbuild/vitest can stay green on type errors, so tsc is the real check) and `pnpm test` (apps/api) + `pnpm --filter @familysync/pwa test`. If any fix introduced a type error or test failure, correct it here (a fix that breaks the build is not a fix). Do NOT reformat — Plan 03 owns that.
+
+
+ cd /home/luc/Projects/familysync && pnpm lint && pnpm typecheck && pnpm --filter @familysync/api test && pnpm --filter @familysync/pwa test && echo LINT_TYPECHECK_TESTS_GREEN
+
+ `pnpm lint` exits 0 across both apps; `pnpm typecheck` clean (both apps, incl. tsconfig.e2e.json); api + pwa test suites pass. No reformatting performed in this plan. The verify gate prints LINT_TYPECHECK_TESTS_GREEN.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| external library (ical.js) → broker code | Untyped `any` values cross into sync/expand; unsafe access is the lint signal |
+| async dispatch (push/outbox/reminder) → unhandled rejection | A floating/misused promise here can silently swallow a security-relevant error |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Disposition | Mitigation Plan |
+|-----------|----------|-----------|-------------|-----------------|
+| T-13-02 | Tampering | masking a real bug with eslint-disable / `void` | mitigate | D-13-06 enforced per-task: no blanket disables; every suppression carries a justification comment; floating promises on push/outbox/reminder paths are reviewed for a missing-await bug BEFORE any `void`. Reviewer checks the diff for un-justified disables. |
+| T-13-03 | Information Disclosure | floating promise swallowing errors (broker dispatch) | mitigate | `no-floating-promises` (type-aware) surfaces every unhandled promise; each is either awaited/`.catch()`-handled (real fix) or documented fire-and-forget — never silently voided. |
+| T-13-04 | Tampering | no-unsafe-* on ical.js-derived data | accept | ical.js is a trusted Mozilla-maintained library with weak types; values are validated at the call site. Suppressions are targeted + justified, not blanket. |
+
+
+
+- `pnpm --filter @familysync/api lint` exit 0; `pnpm --filter @familysync/pwa lint` exit 0; root `pnpm lint` exit 0.
+- `pnpm typecheck` clean (both apps), `pnpm test` (api) + pwa test green — fixes did not regress build/tests.
+- Diff review: no blanket `eslint-disable` (file-level); every `eslint-disable-next-line` has an adjacent justification comment; no `void` on a push/outbox/reminder promise without a documented fire-and-forget rationale.
+
+
+
+- `pnpm lint` exits 0 across both apps (D-13-05).
+- All fixes address violations, none mask them (D-13-06): justified suppressions only, real bugs genuinely fixed.
+- typecheck + tests still green.
+- No Prettier reformatting in this plan (reserved for Plan 03, D-13-08).
+
+
+
diff --git a/.planning/phases/13-real-lint-gate-eslint/13-03-PLAN.md b/.planning/phases/13-real-lint-gate-eslint/13-03-PLAN.md
new file mode 100644
index 0000000..0c12764
--- /dev/null
+++ b/.planning/phases/13-real-lint-gate-eslint/13-03-PLAN.md
@@ -0,0 +1,163 @@
+---
+phase: 13-real-lint-gate-eslint
+plan: 03
+type: execute
+wave: 3
+depends_on:
+ - 13-02
+files_modified:
+ - .gitea/workflows/ci.yml
+ - apps/api/src/**
+ - apps/pwa/src/**
+ - apps/pwa/e2e/**
+ - apps/api/tests/**
+autonomous: true
+requirements: []
+must_haves:
+ truths:
+ - "All files are reformatted by Prettier as ONE isolated mechanical commit, separate from Plan 02's logic fixes, for reviewability (D-13-08)"
+ - "CI fast-checks job gains a `Format check` step running `pnpm format:check`, inserted after Lint and before Typecheck; the stale no-op lint comment is removed (SC-2)"
+ - "Baseline gate ends green: `pnpm lint` exits 0 AND `pnpm format:check` exits 0 across both apps (SC-3)"
+ - "CI lint step now genuinely blocks a PR to main on violations (no longer the --if-present no-op), and the new format:check step blocks on format violations (SC-2)"
+ artifacts:
+ - path: ".gitea/workflows/ci.yml"
+ provides: "fast-checks job with a Format check step after Lint; stale no-op comment removed"
+ contains: "format:check"
+ key_links:
+ - from: ".gitea/workflows/ci.yml fast-checks job"
+ to: "root package.json lint + format:check scripts"
+ via: "pnpm lint / pnpm format:check steps"
+ pattern: "pnpm format:check"
+---
+
+
+Close the phase: (1) run Prettier across the whole repo as a single isolated reformat commit (D-13-08), (2) add the `Format check` CI step to the fast-checks job and remove the now-false no-op lint comment (SC-2), (3) assert the green baseline — `pnpm lint` AND `pnpm format:check` both exit 0 across both apps (SC-3). This is the last wave; after it, the lint+format gate is real, green, and wired into CI.
+
+Purpose: SC-3 requires the first real run's baseline to end green; SC-2 requires the CI gate to actually block PRs to main. The reformat is isolated from logic fixes (Plan 02) so the large mechanical diff is reviewable on its own.
+Output: reformatted tree (isolated commit), updated ci.yml, green lint + format baseline.
+
+
+
+@$HOME/.claude/gsd-core/workflows/execute-plan.md
+@$HOME/.claude/gsd-core/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/13-real-lint-gate-eslint/13-CONTEXT.md
+@.planning/phases/13-real-lint-gate-eslint/13-RESEARCH.md
+@.planning/phases/13-real-lint-gate-eslint/13-PATTERNS.md
+@.planning/phases/13-real-lint-gate-eslint/13-02-SUMMARY.md
+@.gitea/workflows/ci.yml
+
+
+
+No NEW files. MODIFIES:
+- The whole source tree (Prettier reformat — isolated commit).
+- `.gitea/workflows/ci.yml` — adds the `Format check` step; removes the stale 3-line no-op lint comment (ci.yml lines 28-30).
+
+This completes the phase's full artifact set across all three plans:
+- New config files (Plan 01): eslint.config.js, .prettierrc, .prettierignore
+- New scripts (Plan 01): root format/format:check + type:module; apps/api & apps/pwa lint
+- New CI step (Plan 03): Format check
+- State: lint-clean (Plan 02) + format-clean (Plan 03) baseline, green in CI.
+
+
+
+
+
+ Task 1: Prettier reformat — single isolated mechanical commit (D-13-08)
+ apps/api/src/**, apps/api/tests/**, apps/pwa/src/**, apps/pwa/e2e/**, root config files Prettier owns
+
+ - 13-RESEARCH.md "Prettier Configuration" (the .prettierrc settings + .prettierignore exclusions, so you know what gets formatted and what is skipped).
+ - 13-CONTEXT.md D-13-08 (isolate the reformat commit from logic fixes for reviewability).
+
+
+ Run `pnpm format` (`prettier --write .`) at the repo root. This reformats every file Prettier owns, respecting `.prettierignore` (dist/, node_modules/, pnpm-lock.yaml, apps/api/src/db/migrations/, *.html are skipped). Because `.prettierrc` sets `singleQuote: true` to match the existing codebase, the diff should be whitespace/wrapping/trailing-comma churn, not a mass quote flip.
+
+ This reformat MUST be its own commit, isolated from Plan 02's lint fixes (D-13-08) — do not mix any logic change into it. After formatting, run `pnpm lint` to confirm the reformat did NOT introduce any lint violation (eslint-config-prettier means ESLint does not fight Prettier, so this should stay green from Plan 02). If `pnpm lint` is non-zero after reformat, a formatting/lint conflict exists — investigate the eslint-config-prettier placement (must be last) before proceeding; do not blanket-disable.
+
+ Record `git diff --stat` (file count + line churn) in the SUMMARY as evidence of the mechanical reformat.
+
+
+ cd /home/luc/Projects/familysync && pnpm format:check && pnpm lint && echo FORMAT_AND_LINT_GREEN_AFTER_REFORMAT
+
+ `pnpm format` ran; the whole tree is Prettier-clean (`pnpm format:check` exits 0) and still lint-clean (`pnpm lint` exits 0 — no eslint/prettier conflict). The reformat is staged as its own isolated commit with no logic changes mixed in. `git diff --stat` recorded in SUMMARY. The verify gate prints FORMAT_AND_LINT_GREEN_AFTER_REFORMAT.
+
+
+
+ Task 2: Add CI Format check step + remove stale no-op lint comment (SC-2)
+ .gitea/workflows/ci.yml
+
+ - .gitea/workflows/ci.yml — the fast-checks job, the existing `Lint` (lines 31-32) and `Typecheck` (lines 34-35) steps, and the stale no-op comment at lines 28-30.
+ - 13-PATTERNS.md ".gitea/workflows/ci.yml — adding a Format check step" (exact step shape + the comment to remove) + the "CI step insertion" shared pattern (`- name: Verb noun` / ` run: pnpm