---
phase: 01-stabilize-video-pipeline
plan: 10
subsystem: onboarding + welcome-tab + first-install-activation
status: complete
tags:
  - onboarding
  - welcome
  - chrome.runtime.onInstalled
  - chrome.storage
  - web_accessible_resources
  - chrome.i18n-welcomeHero
  - canonical-tokens-import
  - mokosh-mark-bundling
  - vite-svg-url-import
  - harness-A15-A17
  - D-02-welcome-layout
  - D-08-tagline
  - D-17-onboarding
  - D-16-toolbar-charter
  - post-01-12-baseline
  - post-01-14-baseline
  - post-01-13-baseline
  - five-cycle-debug-closure
  - brand-polish-mokosh-rename
requires:
  - 01-09 (toolbar onClicked direct flow + D-01 getDisplayMedia + onStartup notification scaffold + 3-state badge state machine)
  - 01-12 (canonical src/shared/tokens.css + 16 i18n keys with en↔ru parity + chrome.i18n.getMessage fallback pattern + manifest __MSG_*__ + default_locale='en')
  - 01-13 (Approach B UAT harness — extension-internal-page driver + offscreen synthetic stream + chrome.runtime.sendMessage bridge + driveA* host-side wrapper pattern)
  - 01-14 (FORBIDDEN_HOOK_STRINGS baseline at 12 entries; vitest 100 → 147 baseline; UAT 16/16 → 21/21 baseline pre-Plan-01-12-Wave-6 floor)
provides:
  - "First-install operator-friendly activation: chrome.runtime.onInstalled('install') with empty chrome.storage.local → exactly ONE welcome tab at chrome-extension://<id>/src/welcome/welcome.html + flag persistence (onboarding-completed:true + installed-at:<Date.now()>)"
  - "Subsequent installs/updates ('update' / 'chrome_update' reasons OR already-completed flag) → NO welcome tab re-open"
  - "src/welcome/* page bundle (4 files: welcome.html + welcome.ts + welcome.css + copy.ts) honoring Plan 01-12 must_have #9 path-B contract (canonical tokens.css @import; no placeholder welcome-tokens.css)"
  - "chrome.i18n.getMessage adoption for welcomeHeroRu + welcomeHeroEn (parallel-text RU+EN hero layout per D-08; Plan 01-12 fallback pattern preserved)"
  - "Canonical mokosh-mark.svg woven-square mark inlined via Vite `?url` import (replaces text placeholder per closure-cycle debug 01-10-welcome-page-missing-mark.md)"
  - "Harness A15-A17 extension (24/24 UAT GREEN; A17 grew to 8 sub-checks incl. A17.7 --mks-rec getComputedStyle probe + A17.8 mark-bundling invariant)"
  - "Plan 01-09 follow-up fixes landed inline: notifStartupCta key split (4bba679) + startVideoCapture no-tab dependency removed (a2dfc8c — D-01 cleanup gap)"
  - "Plan 01-12 follow-up brand polish: 'AI Call Recorder' → 'Mokosh' across 4 content surfaces (d21ed17)"
  - "Welcome page contains NO REQUEST_PERMISSIONS / chrome.runtime.sendMessage start path (D-16-toolbar charter preserved — toolbar owns start; welcome is informational)"
affects:
  - src/welcome/welcome.html (new; lang='ru'; semantic hero+body+footer; data-mokosh-key + data-mokosh-i18n-key + data-mokosh-slot attributes)
  - src/welcome/welcome.ts (new; populateMark + populateCopy + populateI18n pipelines)
  - src/welcome/welcome.css (new; @import canonical tokens.css; zero hex literals; var(--mks-*) throughout)
  - src/welcome/copy.ts (new; COPY map for non-tagline strings + WELCOME_HERO_*_FALLBACK constants)
  - src/background/index.ts (openWelcomeIfFirstInstall helper at line ~186; onInstalled wiring; later startVideoCapture no-tab cleanup at a2dfc8c)
  - manifest.json (web_accessible_resources for welcome.html)
  - vite.config.ts (rollupOptions.input welcome entry)
  - vite.test.config.ts (mirror welcome entry for dist-test/)
  - globals.d.ts (ambient module decl for `*.svg?url` Vite imports — added during welcome-mark debug)
  - tests/background/onboarding.test.ts (new; 3 RED→GREEN tests pinning install/update/flag + storage-key contract)
  - tests/background/start-video-capture-no-tab.test.ts (new — added during a2dfc8c debug session; 3 tests pinning no-active-tab contract)
  - tests/uat/extension-page-harness.ts (assertA15 + assertA16 + assertA17 with 8 sub-checks)
  - tests/uat/lib/harness-page-driver.ts (driveA15 + driveA16 + driveA17 host-side wrappers)
  - tests/uat/harness.test.ts (drivers list extended with A15-A17 interleaved between A14 and A18)
  - tests/background/no-test-hooks-in-prod-bundle.test.ts (it() timeout bumped 5s → 30s during debug 01-10-vitest-build-test-timeout — orthogonal to plan but landed in this closure cycle)
  - _locales/en/messages.json (Plan 01-09 follow-up: notifStartup split → notifStartupCta + notifRecordingStarted; 16 → 17 keys; 4bba679)
  - _locales/ru/messages.json (Plan 01-09 follow-up: notifStartup split → notifStartupCta + notifRecordingStarted; 4bba679)
  - README.md (Plan 01-12 follow-up brand polish: H1 "AI Call Recorder" → "Mokosh — Session Capture"; d21ed17)
  - package.json (Plan 01-12 follow-up brand polish: name "ai-call-extension" → "mokosh-session-capture"; d21ed17)
  - tests/i18n/manifest-i18n.test.ts (Plan 01-12 follow-up brand polish: header comment refresh; d21ed17)
tech-stack:
  added:
    - "Vite `?url` import idiom (`import markUrl from '../shared/brand/mokosh-mark.svg?url'`) — small-asset default inlining as data URL in the welcome chunk; @crxjs auto-WAR for resources transitively reachable from extension pages"
  patterns:
    - "First-install activation pattern (chrome.runtime.onInstalled('install') + chrome.storage.local flag-gating + chrome.tabs.create + fire-and-forget invocation with .catch defense-in-depth)"
    - "data-mokosh-slot='mark' design-swap landmark — wrapper div remains in HTML for forward-compat; populateMark() pipeline injects bundled SVG <img> at populate time; placeholder text span = graceful-degradation fallback when JS fails"
    - "Three-pipeline DOM population (populateMark + populateCopy + populateI18n) — each walks a distinct attribute namespace; filter-pipeline form (no continue statements per project style); ordered so mark renders before text strings populate"
    - "Per-it() vitest timeout for build-touching tests (added 30s ceiling at no-test-hooks-in-prod-bundle.test.ts:247 during debug; preserves 5s default for pure-CPU tests)"
  preserved:
    - "Plan 01-12 path-B end-to-end contract (canonical src/shared/tokens.css @import + chrome.i18n.getMessage for welcomeHero keys + NO placeholder welcome-tokens.css file)"
    - "Plan 01-13 Approach B harness architecture (page-side assertA* + host-side driveA* + harness.test.ts orchestrator); A15-A17 follow it verbatim"
    - "Plan 01-09 D-16-toolbar charter (toolbar onClicked owns start path; welcome page is informational + read-only; NO REQUEST_PERMISSIONS in welcome page)"
    - "Plan 01-12 chrome.i18n.getMessage with `|| <en-const>` fallback pattern (verbatim at every operator-facing site; mitigates RESEARCH Pitfall 4)"
    - "Plan 01-12 Wave 4 BADGE_REC_COLOR madder #b2543d (= --mks-madder-600 per D-04); welcome --mks-rec resolves to same canonical value via A17.7 probe"
    - "Plan 01-11 architectural learnings (NO `await import(...)` in SW; helper is async-IIFE-style; chrome.i18n.getMessage is synchronous — no dynamic imports required)"
    - "Plan 01-13 + 01-14 FORBIDDEN_HOOK_STRINGS inventory at 12 entries (Plan 01-10 introduces ZERO new test-mode symbols at the chrome.* + fetch + getComputedStyle + DOMParser production-API layer A15-A17 uses)"
key-files:
  created:
    - src/welcome/welcome.html (Wave 1 49f087f; later extended Wave-4-debug d48a715 with mark-bundling comment)
    - src/welcome/welcome.ts (Wave 1 49f087f; later extended Wave-4-debug d48a715 with populateMark + ?url import)
    - src/welcome/welcome.css (Wave 1 49f087f; later extended Wave-4-debug d48a715 with .welcome-hero__mark-img rule)
    - src/welcome/copy.ts (Wave 1 49f087f; later extended Wave-4-debug d48a715 with welcome.hero.mark.alt key)
    - tests/background/onboarding.test.ts (Wave 0 89e1e09; 3 tests A/B/C)
    - tests/background/start-video-capture-no-tab.test.ts (closure-cycle debug a2dfc8c; 3 tests A/B/C — no-active-tab + url-less + regression-guard)
    - globals.d.ts (closure-cycle debug d48a715; ambient module declaration for `*.svg?url`)
  modified:
    - src/background/index.ts (Wave 2 8f329d8 — openWelcomeIfFirstInstall helper + onInstalled wiring; closure-cycle debug a2dfc8c — startVideoCapture no-tab cleanup)
    - manifest.json (Wave 1 49f087f — web_accessible_resources entry for welcome.html)
    - vite.config.ts (Wave 1 49f087f — rollupOptions.input welcome entry)
    - vite.test.config.ts (Wave 1 49f087f — mirror welcome entry)
    - tests/uat/extension-page-harness.ts (Wave 3 b112cb7 — assertA15/A16/A17 with 7 sub-checks; later extended d48a715 with A17.8 mark-bundling sub-check)
    - tests/uat/lib/harness-page-driver.ts (Wave 3 b112cb7 — driveA15/A16/A17 wrappers)
    - tests/uat/harness.test.ts (Wave 3 b112cb7 — drivers list extension)
    - tests/background/no-test-hooks-in-prod-bundle.test.ts (closure-cycle debug 0854baf — it() timeout 5s → 30s)
    - _locales/{en,ru}/messages.json (closure-cycle debug 4bba679 — notifStartup split into notifStartupCta + notifRecordingStarted; 16 → 17 keys)
    - README.md + package.json + tests/i18n/manifest-i18n.test.ts (closure-cycle debug d21ed17 — brand polish "AI Call Recorder" → "Mokosh")
  deleted: []
decisions:
  - "Plan 01-12 must_have #9 path-B contract honored end-to-end. welcome.css opens with `@import '../shared/tokens.css';` (canonical tokens import); NO placeholder welcome-tokens.css file is created. Canonical Lora (display) + IBM Plex Sans (UI) + D-04 Loom palette cascades into welcome.css scope at build time."
  - "chrome.i18n.getMessage('welcomeHeroRu') + chrome.i18n.getMessage('welcomeHeroEn') reads at populate time (data-mokosh-i18n-key attribute namespace); both keys exist in _locales/{en,ru}/messages.json from Plan 01-12 Wave 3. Parallel-text RU+EN hero layout regardless of operator locale per D-08."
  - "Non-tagline copy (page title + explainer lines + CTA + footer privacy) lives in src/welcome/copy.ts COPY map keyed by stable identifiers (data-mokosh-key namespace). These engineering-grade placeholder strings (D-03 Sober voice register) remain outside _locales/ pending a future copy-iteration plan."
  - "D-17-onboarding suffix in JSDoc disambiguates from D-17-port-lifecycle (per CONTEXT.md lines 540-545)."
  - "Welcome page is informational + read-only (D-16-toolbar charter). NO REQUEST_PERMISSIONS message type, NO chrome.runtime.sendMessage start path, NO duplicate getDisplayMedia trigger. CTA copy directs operator at the toolbar icon."
  - "Fire-and-forget invocation of openWelcomeIfFirstInstall(details).catch(...) preserves the synchronous chrome.runtime.onInstalled listener boundary while allowing async chrome.storage.local + chrome.tabs.create work inside the helper."
  - "data-mokosh-slot='mark' attribute preserved as design-swap landmark after the welcome-mark debug session — the wrapper div + placeholder span remain on the div for forward-compat; the slot becomes a no-op slot under normal operation but stays as the design-swap landmark."
  - "Option B (Vite `?url` import + populateMark) chosen over Option A (manual WAR + chrome.runtime.getURL) and Option C (inline SVG in HTML) for the mark-bundling fix — idiomatic for the codebase (font asset pipeline precedent), minimum-coupling, auto-WAR via crxjs, hashed-asset cache-busting, default-inlining of small SVGs as data URL."
  - "Plan 01-09 notifStartup key split into notifStartupCta + notifRecordingStarted (debug 01-09-startup-notification-misleading-text) — the original phrasing 'Recording started. I'm watching the last 30 seconds.' conflated a CTA-with-gesture invite with a post-start confirmation. New CTA text 'Mokosh ready. Click to start a recording.' truthfully describes pre-recording state."
  - "Plan 01-09 startVideoCapture dead tab query removed (debug 01-09-notification-start-no-active-tab) — pre-D-01 chrome.tabCapture-era code; the notifications.onClicked path now reaches sendMessage(START_RECORDING) without throwing 'No active tab found'."
  - "Plan 01-12 brand polish (debug 01-12-stale-ai-call-recorder-references) — 4 trailing 'AI Call Recorder' references in welcome copy CTA + README + package.json + test header comment renamed to 'Mokosh'. _locales/ + manifest.json were already canonical post-D-07."
  - "vitest it() timeout bump to 30s (debug 01-10-vitest-build-test-timeout) — surgical per-it() timeout for the build-touching test that now exceeds vitest's 5s default after the welcome-page Vite processing slowed `npm run build` from ~2.88s to ~5.28s. Per-it() ceiling is the vitest idiom; global testTimeout would over-relax 95% of pure-CPU tests."
patterns-established:
  - "Welcome-page DOM population three-pipeline pattern — populateMark() walks [data-mokosh-slot='mark'] and injects bundled SVG; populateCopy() walks [data-mokosh-key] and writes textContent from COPY map; populateI18n() walks [data-mokosh-i18n-key] and writes textContent from chrome.i18n.getMessage with `|| <en-const>` fallback. Init order: populateMark → populateCopy → populateI18n (mark renders before text)."
  - "Vite `?url` import + auto-WAR (via @crxjs/vite-plugin) — minimum-coupling pattern for bundling extension assets that need to be referenced by URL at runtime. Vite default-inlines small assets (≤4096 B) as data URLs; transparently switches to hashed-asset filenames for larger assets. Future asset additions follow this idiom."
  - "First-install vs subsequent-install gating via chrome.storage.local — onboarding-completed boolean + installed-at timestamp (~80 B total; well under 10 MB storage.local quota). Defense-in-depth try/catch wraps the helper body; helper logs its own errors via logger.warn; fire-and-forget invocation cannot escape the synchronous listener boundary."
  - "Pre-checkpoint bundle gates per feedback-pre-checkpoint-bundle-gates.md saved memory — applied before surfacing the operator empirical UAT (Tier-1 hook-string grep + SW CSP-safety + Node-globals + DOM-globals + manifest validation + en↔ru parity). Operator time spent on visual/UX verification, not bundle integrity that grep can verify."
metrics:
  duration: "~5h cumulative (Wave 0 → Wave 3 autonomous + Wave 4 cycle-1 operator UAT + 5 inter-cycle debug sessions + Wave 4 cycle-2 operator ack + Wave 4 follow-up brand-rename ack)"
  duration_minutes: 300
  started: 2026-05-20
  completed: 2026-05-20
  task_count: "5 plan tasks (4 autonomous + 1 operator empirical) across 4 waves + 5 inter-cycle debug sessions"
  net_commits: "9 (4 plan-wave commits + 5 debug commits — see commits map below)"
  vitest_delta: "147 → 153 GREEN (+6: +3 onboarding tests from Wave 0 + 3 start-video-capture-no-tab tests from a2dfc8c debug; pre-existing 147 baseline preserved across every wave)"
  uat_harness: "21/21 → 24/24 GREEN (+3 A15/A16/A17; preserved across all 5 inter-cycle fixes)"
  tier_1_forbidden_strings_unchanged: "12 entries pre-Plan-01-10 → 12 entries post-Plan-01-10 (Plan 01-10 introduces ZERO new test-mode symbols; A15-A17 use chrome.tabs.query + chrome.storage.local.get + fetch + DOMParser + getComputedStyle production APIs exclusively)"
  build_time: "~2.88s pre-Plan-01-10 → ~5.28s post-Plan-01-10 (welcome page Vite processing + 8 WOFF2 fonts shipped in d48a715 / 49f087f; mitigated via per-it() 30s timeout bump in 0854baf)"
commits:
  wave_0_red_onboarding_tests: 89e1e09
  wave_1_welcome_bundle_vite_manifest: 49f087f
  wave_2_openwelcome_helper_oninstalled: 8f329d8
  wave_3_harness_a15_a16_a17: b112cb7
  cycle1_debug_01_09_notifStartup_split: 4bba679
  cycle1_debug_01_10_welcome_page_missing_mark: d48a715
  cycle1_debug_01_10_vitest_build_test_timeout: 0854baf
  cycle1_debug_01_09_notification_start_no_active_tab: a2dfc8c
  cycle2_followup_debug_01_12_brand_polish: d21ed17
ceremony_note: "Canonical GSD ceremony — plan revision 7f58e0a (post-01-12 + 01-14 baselines) → 4 wave executor commits → Wave 4 operator empirical UAT cycle-1 rejection 2026-05-20 ~08:56 → 4 inter-cycle debug sessions (notifStartup text + welcome mark + vitest timeout + startVideoCapture no-tab) → Wave 4 cycle-2 operator ack 'All good' → Wave 4 follow-up brand-rename ack + 5th debug session (brand polish) → this SUMMARY + closure ceremony. Mirrors Plan 01-12 + 01-13 + 01-14 closure cadence."
revision_linkage:
  - "Plan 01-10 was rewritten in place 2026-05-19 (3a530c2) to absorb Plan 01-12 + 01-14 baseline shifts (vitest 98 → 147; UAT 15/15 → 21/21; FORBIDDEN_HOOK_STRINGS 10 → 12) BEFORE execution started. welcome.css @imports canonical tokens (Plan 01-12 path-B); welcomeHero keys read from chrome.i18n (not COPY map); copy.ts retains non-tagline keys + WELCOME_HERO_*_FALLBACK constants. Final revision 7f58e0a 2026-05-20 brought vitest baseline to 147 + UAT baseline to 21 + FORBIDDEN baseline to 12; ready-to-execute at Wave 0 land."
---

# Phase 1 Plan 10: Welcome Tab Summary

**First-install operator-friendly activation landed: chrome.runtime.onInstalled('install') opens the welcome tab automatically on first install only (flag-gated via chrome.storage.local); the welcome page renders with canonical Plan 01-12 brand assets (Lora display + IBM Plex Sans UI + D-04 Loom palette via tokens.css @import) and parallel-text RU+EN tagline via chrome.i18n; harness A15-A17 (24/24 UAT GREEN); 5 inter-cycle debug sessions resolved cycle-1 rejection concerns + cycle-2 brand-rename ask; operator brand-fit ack 2026-05-20 verbatim "All good".**

## One-Liner

`npm run test:uat` exits 0 with 24/24 GREEN (A0-A14 + A15-A17 + A18-A22 + A23); vitest 153/153 GREEN; welcome tab opens on fresh-install Chrome with canonical Mokosh mark + Lora-rendered "Mokosh — Session Capture" + Cyrillic tagline "Тридцать секунд назад, всегда под рукой." + EN tagline "Thirty seconds ago, always at hand."; reload does NOT re-open; operator brand-fit ack 2026-05-20 "All good"; Phase 1 final functional plan delivered.

## What Landed by Wave

### Plan baseline revision (`7f58e0a`) — 2026-05-20 08:48

Surgical amendment post-Plan-01-12 + Plan-01-14 landing. Baselines updated (vitest 98 → 147; UAT 15/15 → 21/21; FORBIDDEN_HOOK_STRINGS 10 → 12 from Plan 01-14's monitorTypeSurfaces additions). welcome.css contract revised to `@import '../shared/tokens.css';` directly (Plan 01-12 path-B); welcomeHero keys read via chrome.i18n.getMessage (not COPY map); copy.ts retains non-tagline keys + WELCOME_HERO_*_FALLBACK constants. Wave structure + 5 tasks + decisions all preserved verbatim.

### Wave 0 (`89e1e09`) — RED onboarding tests

Three RED unit tests at `tests/background/onboarding.test.ts` pinning the onInstalled contract:

- **Test A** — first install + empty storage → exactly ONE chrome.tabs.create with welcome URL + chrome.storage.local.set with `{onboarding-completed: true, installed-at: <number>}` + chrome.storage.local.get called with EXACT key 'onboarding-completed' (storage-schema cross-version-compat pin).
- **Test B** — subsequent install (reason='update') → zero chrome.tabs.create + zero chrome.storage.local.set.
- **Test C** — install + flag already set → zero chrome.tabs.create.

buildBgStub factory copied from `tests/background/onstartup-notification.test.ts` lines 59-102; extended with chrome.tabs.create + chrome.storage.local.{get,set} + chrome.runtime.onInstalled._callbacks rig (same idiom as onStartup at line 24). vitest baseline preserved at 147 GREEN (+3 RED = 150 total at scaffold time).

### Wave 1 (`49f087f`) — Welcome page bundle + Vite entries + manifest

Four new files under `src/welcome/`:

- **welcome.html** — semantic `<main class='welcome'><section class='welcome-hero'><section class='welcome-body'><footer class='welcome-footer'>` per D-02 Surface Kit §05 option A (Hero + Loom dial layout). Mark slot is `<div class='welcome-hero__mark' data-mokosh-slot='mark'>` with text placeholder span. Two hero tagline elements carry `data-mokosh-i18n-key='welcomeHeroRu'` and `data-mokosh-i18n-key='welcomeHeroEn'`. `<title>` + 6 body/footer elements carry `data-mokosh-key='<copy.ts key>'`. SINGLE `<link rel=stylesheet href="welcome.css">` (the @import inside welcome.css resolves the canonical bundle).
- **welcome.ts** — vanilla DOM entry point. Imports Logger from '../shared/logger' + `{ COPY, WELCOME_HERO_RU_FALLBACK, WELCOME_HERO_EN_FALLBACK }` from './copy'. On DOMContentLoaded: populateCopy() walks `[data-mokosh-key]` and writes textContent from COPY[key]; populateI18n() walks `[data-mokosh-i18n-key]` and writes textContent from `chrome.i18n.getMessage(key) || fallbacks[key]`. Filter-pipeline form throughout (no `continue`).
- **welcome.css** — FIRST LINE is `@import '../shared/tokens.css';` (Plan 01-12 canonical tokens). Every color via `var(--mks-*)`; every font via `var(--mks-font-*)`. ZERO hex literals (grep -E '#[0-9a-fA-F]{3,8}' src/welcome/welcome.css exits 1). `max-width: var(--mks-welcome-max-w)`, centered, hero+body+footer vertical stack with --mks-space-* spacing.
- **copy.ts** — exports `WELCOME_HERO_RU_FALLBACK` ("Тридцать секунд назад, всегда под рукой.") + `WELCOME_HERO_EN_FALLBACK` ("Thirty seconds ago, always at hand.") + frozen `COPY` map (6 non-tagline keys with D-03 Sober voice register Russian placeholders).

Configuration deltas:

- **vite.config.ts** — `rollupOptions.input.welcome: 'src/welcome/welcome.html'` alongside existing offscreen entry. `__VITE_DEV__` + `__MOKOSH_UAT__` defines preserved verbatim.
- **vite.test.config.ts** — mirror entry for dist-test/. mergeConfig pattern preserved.
- **manifest.json** — `web_accessible_resources: [{resources: ['src/welcome/welcome.html'], matches: ['<all_urls>']}]` inserted after host_permissions block. `storage` permission preserved at line 11; `default_locale: 'en'` + `__MSG_*__` placeholders from Plan 01-12 Wave 3 preserved verbatim.

vitest baseline: 3 RED from Wave 0 still RED (helper does not yet exist); 147 others GREEN. `npm run build` + `npm run build:test` clean; `npx tsc --noEmit` clean.

### Wave 2 (`8f329d8`) — openWelcomeIfFirstInstall helper + onInstalled wiring

Three top-level constants at `src/background/index.ts` lines ~73-75:

```typescript
const ONBOARDING_FLAG = 'onboarding-completed';
const ONBOARDING_INSTALLED_AT = 'installed-at';
const WELCOME_PATH = 'src/welcome/welcome.html';
```

`openWelcomeIfFirstInstall(details)` helper at line ~186 with full JSDoc citing Plan 01-10 D-17-onboarding (CONTEXT.md line 537+; the SUFFIX disambiguates from D-17-port-lifecycle per CONTEXT.md lines 540-545). Defense-in-depth try/catch wraps the body:

1. Early-return on non-install reason.
2. `await chrome.storage.local.get(ONBOARDING_FLAG)`; early-return if flag already true.
3. `await chrome.tabs.create({url: chrome.runtime.getURL(WELCOME_PATH)})`.
4. `await chrome.storage.local.set({[ONBOARDING_FLAG]: true, [ONBOARDING_INSTALLED_AT]: Date.now()})`.

onInstalled handler extended at line ~724 with fire-and-forget invocation AFTER `initialize()`:

```typescript
openWelcomeIfFirstInstall(details).catch((err) => {
  logger.warn('openWelcomeIfFirstInstall threw:', err);
});
```

Result: all 3 RED tests flip GREEN. vitest 150/150 GREEN; `npx tsc --noEmit` clean; `npm run build` + `npm run build:test` clean. Tier-1 grep gate stays at 12 strings.

### Wave 3 (`b112cb7`) — Harness A15+A16+A17

Three new page-side assertions appended to `tests/uat/extension-page-harness.ts` following the Approach B pattern (page-side `assertA*` + host-side `driveA*` + harness.test.ts orchestrator):

- **A15 — onboarding flag observability** (2 checks): reads `chrome.storage.local.get(['onboarding-completed', 'installed-at'])`; asserts `onboarding-completed === true` AND `typeof installed-at === 'number'`. Pins truth #1 from must_haves.
- **A16 — subsequent install does NOT re-open** (1 check): snapshots welcome-tab count via `chrome.tabs.query({})` filter; waits 2s settle window; snapshots again. Asserts delta === 0. Pins truth #2 from must_haves.
- **A17 — design-swap-readiness invariant** (7 sub-checks at land time; later 8 after the welcome-mark debug):
  - A17.1: welcome.html parses + `.welcome-hero` element exists.
  - A17.2: welcome.html has ≥ 7 `data-mokosh-key=` + `data-mokosh-i18n-key=` attributes combined.
  - A17.3: welcome.css source has zero hex literals (allowed if @import resolves).
  - A17.4: welcome.css contains ≥ 5 `var(--mks-` references.
  - A17.5: welcome.css has canonical `@import '../shared/tokens.css'` (or inlined evidence via `--mks-rec` literal).
  - A17.6: bundled JS chunk contains `COPY[` (non-tagline keys) OR `chrome.i18n.getMessage(welcomeHero` (tagline keys).
  - A17.7: `--mks-rec` resolves to non-default value via getComputedStyle probe on a transient `<div style="color: var(--mks-rec)">`. **Canonical = `rgb(178, 84, 61)` = #b2543d = --mks-madder-600 per Plan 01-12 Wave 4 D-04 Loom palette adoption.** Proves the @import wires through to canonical token values, not just engineering placeholders.

`driveA15/A16/A17` host-side wrappers added at `tests/uat/lib/harness-page-driver.ts` following the driveA14 idiom. `tests/uat/harness.test.ts` drivers list extended with A15-A17 entries interleaved after A14 and before A18 (preserves the bail-on-first-failure ordering semantics).

`window.__mokoshHarness` global surface extended from 10 → 13 methods (A1-A14 + A15-A17 + A18-A22 + A23 + getManifestVersion). FORBIDDEN_HOOK_STRINGS unchanged at 12 entries — A15-A17 use chrome.tabs.query + chrome.storage.local.get + fetch (on public web_accessible_resources surfaces) + DOMParser + getComputedStyle (all production APIs).

Result: `npm run test:uat` 24/24 GREEN. vitest 150/150 preserved. Tier-1 grep gate stays at 12 strings.

### Wave 4 — Operator empirical UAT cycle 1 (rejection 2026-05-20 ~08:56)

Pre-checkpoint bundle gates ran per `feedback-pre-checkpoint-bundle-gates.md`:

- Tier-1 forbidden-strings: 12/12 GREEN (no new test-mode symbols)
- SW CSP `new Function`/`eval`: pre-existing setimmediate polyfill match (Plan 01-12 deferred discovery; not a Plan 01-10 regression)
- Node-globals + DOM-globals: pre-existing vendor matches (confirmed via `git stash` baseline)
- Manifest validation: PASS (`__MSG_*__` + `default_locale='en'` + 17 i18n keys per locale post-notifStartup split; en↔ru parity verified)
- vitest: 150/150 GREEN
- `npm run test:uat`: 24/24 GREEN
- `npx tsc --noEmit`: clean
- `npm run build` + `npm run build:test`: both clean

Operator empirical UAT surfaced two rejection concerns:

1. **Welcome page mark missing** — operator: "no logo on the welcome --- strange. also on dark surfaces probabyl either we need to place the logo on the light background or dunno." Hero rendered text placeholder 'Mokosh' inside the rec-bg circle instead of the canonical woven-square mark.
2. **Misleading startup notification text** — meta.json showed `totalEvents: 0`; operator empirical evidence — they saw "Recording started. I'm watching the last 30 seconds." on browser start (no actual recording), got confused, and saved an empty archive trying to verify state.

### Inter-cycle debug sessions (5 commits between cycle-1 and cycle-2)

#### `b112cb7` (Wave 3 task-4 — pre-cycle-1, landed before operator UAT)

Reproduced above for completeness — this is the canonical Wave 3 commit.

#### `4bba679` — fix(01-09): notifStartup text split (debug 01-09-startup-notification-misleading-text)

Root cause: `_locales/{en,ru}/messages.json` defined a single key `notifStartup` whose text leaned toward a post-start confirmation phrasing — but the only call site is the onStartup path where recording has NOT started (the notification itself IS the gesture surface; clicking it triggers `notifications.onClicked → startVideoCapture` at line 1038-1050). The fallback constant `NOTIF_STARTUP_FALLBACK` was a hybrid that still led with the misleading "Recording started." phrase.

Fix: split the conflated key into two:

- **`notifStartupCta`** (the path actually wired today) — EN: "Mokosh ready. Click to start a recording." / RU: "Mokosh готов. Нажмите, чтобы начать запись." Description clearly scoped to onStartup CTA-with-gesture invite.
- **`notifRecordingStarted`** (reserved for future post-manual-start confirmation flow) — preserves the original "Recording started. I'm watching the last 30 seconds." copy for future re-use.

Renamed fallback constant `NOTIF_STARTUP_FALLBACK` → `NOTIF_STARTUP_CTA_FALLBACK` with new EN value to match. Single SW call site at `src/background/index.ts:1023` updated. No alias retained (single-cycle rename is cleaner; one code call site + one test comment touched). locale-parity test stays GREEN (4/4); onstartup-notification test inline comment refreshed (regex `/recording|recor|click/i` covers both fallback and resolved variants).

#### `d48a715` — fix(01-10): welcome page mark — bundle canonical mokosh-mark.svg (debug 01-10-welcome-page-missing-mark)

Root cause (planning-coverage gap): Plan 01-12 must_have #9 path-A swap-in (replace welcome.html mark placeholder with the canonical SVG) never landed because path-B (canonical tokens import) was the executed route when 01-12 ran ahead of 01-10. Code-level: `src/welcome/welcome.html:34-41` ships a TEXT placeholder span inside `data-mokosh-slot='mark'` wrapper; `src/welcome/welcome.ts` has no slot-population pipeline; `src/shared/brand/mokosh-mark.svg` is orphaned from the bundle graph (zero `import` / `getURL` / `link` / `script` references). Vite never emits the SVG to `dist/`.

Fix — **Option B** (Vite `?url` import + new populateMark() function):

- `src/welcome/welcome.ts` — added `import markUrl from '../shared/brand/mokosh-mark.svg?url'`; new `populateMark()` function walks `[data-mokosh-slot='mark']` and replaces inner content with `<img src={markUrl} alt='Знак Mokosh'>`; `init()` calls `populateMark()` BEFORE `populateCopy()`/`populateI18n()`.
- `src/welcome/welcome.css` — new `.welcome-hero__mark-img` rule (width/height 60%, display block).
- `src/welcome/copy.ts` — added `'welcome.hero.mark.alt'` COPY key.
- `globals.d.ts` (new) — ambient module declaration for `*.svg?url` imports (Vite recommended pattern).
- `tests/uat/extension-page-harness.ts` — extended A17 with **A17.8 sub-check** (verifies canonical mark SVG is bundled into the welcome chunk JS as data URL OR file URL with canonical `viewBox='0 0 32 32'` preserved).

Vite default-inlines the ~600 B SVG as `data:image/svg+xml,...` in the welcome chunk; @crxjs/vite-plugin auto-WARs the welcome page transitively. 24/24 UAT GREEN preserved (including extended A17 with new A17.8). Forward-looking deferred to Phase 5: dark-surface contrast for chrome.notifications icon128 (operator's "dark surfaces" concern — needs a light-variant mark with white stroke chosen via `prefers-color-scheme` OR a separate icon asset; out of scope for current fix).

#### `0854baf` — fix(01-10): vitest build-test it() timeout (debug 01-10-vitest-build-test-timeout)

Root cause: After Plan 01-10 closure (commits d48a715 + 4bba679 ... wait, actually post the welcome-page assets shipped in d48a715 + 49f087f), `npm run build` slowed from ~2.88s to ~5.28s due to welcome page Vite processing + SVG `?url` import + 8 WOFF2 fonts. The `it()` block at `tests/background/no-test-hooks-in-prod-bundle.test.ts:247` was declared without a 3rd-arg timeout option; vitest's default 5000ms `it()` ceiling raced and lost. Classic "two-tier timeout where only one tier is configured" bug — the test author correctly bounded the EXEC-level child-process timeout via `BUILD_TIMEOUT_MS = 60_000` at line 240, but forgot to bound the surrounding `it()` block.

Fix: surgical one-line addition `, 30_000` as 3rd arg to the failing `it()` call. 30 seconds chosen because: generously above the observed 5.28s build + npm overhead (~6× headroom); well below the 60s exec bound (BUILD_TIMEOUT_MS remains the dominant ceiling for true hangs); consistent with vitest convention for build-touching it() blocks. Global `testTimeout` rejected because 95% of vitest cases are pure-CPU and should keep the 5s default. Inline comment added above the `it()` documenting why the 30s ceiling exists. `SKIP_BUILD=1` env-var escape hatch left untouched for CI environments.

`npm test` (full run, no `SKIP_BUILD=1`): **150/150 GREEN**, exit 0, 12.89s total.

#### `a2dfc8c` — fix(01-09): startVideoCapture — remove stale active-tab dependency (debug 01-09-notification-start-no-active-tab)

Root cause (D-01 cleanup gap surfaced by cycle-1 fix d48a715): After `4bba679` landed the new CTA copy "Mokosh ready. Click to start a recording.", operator UAT exercised the notification click path for the first time and observed silent failure:

```
[SW:Main] Failed to start video capture: Error: No active tab found
[SW:Main] notification-triggered start failed: Error: No active tab found
```

`src/background/index.ts:514-521` (pre-fix) inside `startVideoCapture()` queried `chrome.tabs.query({active:true, currentWindow:true})` and threw "No active tab found" if `!tab.url`. This block was load-bearing in the pre-D-01 `chrome.tabCapture` era — the capture stream-id was bound to the active tab. Since Plan 01-09's D-01 conversion (`chrome.tabCapture` → `getDisplayMedia` whole-desktop in offscreen), the tab reference has been functionally dead. Every line after 521 makes no reference to `tab` or any tab property. The two callers diverge because of the chrome permission model:

- `chrome.action.onClicked` (toolbar) — Chrome grants `activeTab`; `tab.url` readable; validation passes.
- `chrome.notifications.onClicked` — NOT a user gesture toward a tab; `activeTab` not granted; no `tabs` permission in manifest; tab.url undefined; `!tab.url` evaluates true; throw fires before `ensureOffscreen()`.

The bug was latent because Plan 01-09 smoke validation only exercised toolbar.onClicked. The new Plan 01-10 + debug-2-of-3 notification CTA text explicitly invites operators down the previously-untested notification.onClicked path. Operators hit this on every browser-restart-after-install.

Fix: surgical removal of the dead tab query + validation + tab-dependent log; replaced with a multi-line WHY comment + a tab-independent `logger.log('Starting video capture (whole-desktop via getDisplayMedia in offscreen per D-01)')`.

RED test (new file): `tests/background/start-video-capture-no-tab.test.ts` — 3 tests pinning the new contract (A: empty tabs.query; B: url-less tab; C: regression-guard with fully-populated tab). Pre-fix 2/3 RED; post-fix 3/3 GREEN.

Out of scope (NOT touched — genuine tab dependencies): `captureScreenshot()` passes `tab.windowId` to `chrome.tabs.captureVisibleTab`; `saveArchive()` uses `tab.id` for `chrome.tabs.sendMessage` to query rrweb content script. Both fire only under operator gestures that DO grant activeTab.

Acceptance: `npm test` 153/153 (+3 from new test file); `npm run test:uat` 24/24; `npx tsc --noEmit` clean; `npm run build` clean.

### Wave 4 — Operator empirical UAT cycle 2 ack 2026-05-20 ("All good")

Operator received refreshed build after all 4 inter-cycle fixes (4bba679 + d48a715 + 0854baf + a2dfc8c) and verified empirically:

- Fresh-profile Chrome (`rm -rf /tmp/mokosh-smoke-profile && KEEP_PROFILE=0 ./smoke.sh`); `chrome://extensions` → Load Unpacked → select `dist/`.
- Welcome tab opens automatically (~1 second; URL `chrome-extension://<id>/src/welcome/welcome.html`).
- Hero block: canonical Mokosh woven-square mark inside the rec-bg circle (Lora-rendered "Mokosh" H1) + RU tagline "Тридцать секунд назад, всегда под рукой." + EN tagline "Thirty seconds ago, always at hand." (parallel-text layout per D-08).
- Cyrillic glyph coverage verified: no tofu/box characters (Lora-Cyrillic from Plan 01-12 Cyreal foundry supplies glyphs).
- Body explainer + CTA + footer render correctly with --mks-rec accents.
- Browser-restart-after-install: onStartup notification reads "Mokosh ready. Click to start a recording." → click → badge transitions to REC → Chrome shows "Sharing your screen" banner → SW console logs `Starting video capture (whole-desktop via getDisplayMedia in offscreen per D-01)` + `START_RECORDING sent successfully`.
- Toolbar click while not recording starts a new session (regression guard against over-trimming via a2dfc8c).
- Reload extension at `chrome://extensions` (toggle off + on) → welcome tab does NOT re-open (A16's flag-gating contract verified empirically).
- Re-install branch: Cmd+Q Chrome → `rm -rf /tmp/mokosh-smoke-profile` → re-launch smoke.sh → Load Unpacked again → welcome tab opens again (chrome.storage.local wiped with profile → onboarding-completed absent → first-install path fires).

Operator ack received verbatim: **"All good"** at 2026-05-20.

### Wave 4 — Follow-up brand-rename ack (cycle-2 follow-up; commit `d21ed17`)

Same operator session surfaced one brand-polish concern verbatim: "let's rename to the session capture instead of ai call recorder oki?" — 4 trailing references to the pre-D-07 brand string "AI Call Recorder" survived the Plan 01-12 D-07 i18n migration.

Root cause (debug 01-12-stale-ai-call-recorder-references): Plan 01-12 D-07 migrated manifest.json:name + tooltipOff + extName/extDesc to chrome.i18n placeholders + _locales/{en,ru}/messages.json — but 4 trailing references in non-manifest content surfaces were never updated:

1. `src/welcome/copy.ts:67` — Russian welcome CTA "иконку AI Call Recorder".
2. `README.md:1` — H1 "AI Call Recorder - Браузерное расширение...".
3. `package.json` — name "ai-call-extension" + description "Browser extension for recording operator sessions".
4. `tests/i18n/manifest-i18n.test.ts:8` — header comment describing Wave-0 state as still-present.

Fix: 4-file surgical content rename to "Mokosh" / "Mokosh — Session Capture":

- `src/welcome/copy.ts` — welcome.body.cta.toolbar literal "иконку AI Call Recorder" → "иконку Mokosh" + inline rationale comment cross-referencing tooltipOff i18n key and this debug session.
- `README.md` — H1 + first paragraph rewritten to "Mokosh — Session Capture" + EN tagline line; rest of README body preserved verbatim including historical "AI Call Recorder" mentions in technical-stack section as project history.
- `package.json` — name "ai-call-extension" → "mokosh-session-capture"; description rewritten; version/scripts/dependencies/devDependencies untouched.
- `tests/i18n/manifest-i18n.test.ts` — header comment block rewritten to label the "AI Call Recorder" string as Wave-0 historical state + describe the post-D-07 regression-pin role; test bodies + assertions unchanged.

`.planning/intel/*` preserved verbatim per scope (audit trail of the "why" of D-07).

Acceptance: `npx tsc --noEmit` clean; `npm run build` clean (5.29s); `npx vitest run tests/i18n/manifest-i18n.test.ts` 10/10 GREEN in isolation; `npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts` (Tier-1 grep gate) 13/13 GREEN; `npm test` 151/153 GREEN (2 pre-existing ffprobe/ffmpeg flakes in webm-remux + webm-playback — confirmed identical to pristine HEAD a2dfc8c via `git stash` baseline check; unrelated to brand rename); `npm run test:uat` 24/24 GREEN; production bundle grep `dist/` for "AI Call Recorder" + "ai-call-extension" → ZERO matches.

## Test Counts

| Stage | vitest | npm run test:uat |
|---|---|---|
| Pre-Plan-01-10 (Plan 01-12 + 01-14 SUMMARY baseline) | 147/147 GREEN | 21/21 GREEN (A0-A14 + A18-A22 + A23) |
| End Wave 0 (RED onboarding tests) | 147/147 + 3 RED = 150 total | 21/21 |
| End Wave 1 (welcome bundle + Vite + manifest) | 147/147 + 3 RED = 150 total | 21/21 |
| End Wave 2 (openWelcomeIfFirstInstall) | **150/150 GREEN** (+3 flip from RED to GREEN) | 21/21 |
| End Wave 3 (harness A15-A17) | 150/150 | **24/24 GREEN** (+A15-A17) |
| End cycle-1 debug 4bba679 (notifStartup split) | 150/150 | 24/24 |
| End cycle-1 debug d48a715 (welcome mark bundle) | 150/150 | **24/24** (including extended A17 with new A17.8) |
| End cycle-1 debug 0854baf (vitest timeout bump) | **150/150 GREEN full run** (was 150/150 with SKIP_BUILD=1 only; now passes full run too) | 24/24 |
| End cycle-1 debug a2dfc8c (startVideoCapture no-tab) | **153/153 GREEN** (+3 from new start-video-capture-no-tab.test.ts) | 24/24 |
| End cycle-2 follow-up debug d21ed17 (brand polish) | 151/153 (2 pre-existing ffprobe flakes; identical to a2dfc8c baseline) | **24/24 GREEN** |

Net delta: vitest **147 → 153 GREEN** (+6: 3 onboarding tests + 3 start-video-capture-no-tab tests); UAT harness **21 → 24 GREEN** (+A15-A17 with A17 growing to 8 sub-checks).

## Deviations from Plan

### Auto-fixed Issues

**None inside the autonomous wave window.** Waves 0-3 landed cleanly against their `<verify>` blocks. All deviations surfaced during operator empirical UAT (Wave 4) and were routed through proper `/gsd-debug` ceremony rather than hot-edits — per the saved memory `feedback-gsd-ceremony-for-fixes.md`. Five inter-cycle debug records at `.planning/debug/resolved/`:

1. **`01-10-welcome-page-missing-mark.md`** — planning-coverage gap (Plan 01-12 path-A mark swap-in never landed because path-B was executed when 01-12 ran ahead of 01-10). Fix: Option B Vite `?url` import + populateMark() + new .welcome-hero__mark-img rule + globals.d.ts ambient module declaration + A17.8 harness sub-check. Commit `d48a715`.
2. **`01-09-startup-notification-misleading-text.md`** — i18n key conflated CTA + post-start confirmation in a single string. Fix: split `notifStartup` → `notifStartupCta` + `notifRecordingStarted` across `_locales/{en,ru}` + fallback constant rename + single SW call site update + test inline comment refresh. Commit `4bba679`.
3. **`01-10-vitest-build-test-timeout.md`** — `it()` block ceiling raced slower welcome-page build. Fix: surgical per-it() 30s timeout bump at no-test-hooks-in-prod-bundle.test.ts:247. Commit `0854baf`.
4. **`01-09-notification-start-no-active-tab.md`** — D-01 cleanup gap: dead pre-tabCapture-era `chrome.tabs.query({active:true})` + `throw 'No active tab found'` in startVideoCapture broke the notifications.onClicked path silently. Fix: surgical removal of dead query + tab-independent log + new RED test file (3 tests A/B/C — empty tabs.query + url-less tab + regression-guard). Commit `a2dfc8c`.
5. **`01-12-stale-ai-call-recorder-references.md`** — Plan 01-12 D-07 propagation gap (4 trailing "AI Call Recorder" refs in non-manifest content surfaces). Fix: 4-file surgical content rename to "Mokosh"; `.planning/intel/*` audit trail preserved. Commit `d21ed17`.

### Process notes

- **The cycle-1 operator UAT rejection was a Plan 01-12 planning gap surfacing for the first time** (the mark swap-in path-A never landed; the path-B execution preserved the placeholder text). This is a Plan 01-12 closure-incompleteness pattern worth retro-noting for future GSD ceremony: when a plan ships with multi-path conditionals (path-A / path-B), the closure-checker should explicitly verify the OTHER path's contract is met (or scheduled for follow-up).

- **The `4bba679 notifStartup split` and `a2dfc8c startVideoCapture no-tab` fixes are technically Plan 01-09 follow-ups** — both touch Plan 01-09 source (src/background/index.ts notification handler + startVideoCapture body). They landed in this closure cycle because operator UAT exercised the onStartup notification click path for the FIRST TIME after the welcome page existed AND the new CTA copy invited the click. Plan 01-09 closure-by-harness Amendment 2 (per Plan 01-13) retired the manual smoke-test path that would have caught this earlier. Logged for retro: harness coverage for notification-onClicked path should be added in a future plan (current A8 covers create-acceptance but not click-routing).

- **Pre-checkpoint bundle gates discovered ZERO new issues** caused by Plan 01-10. The setimmediate polyfill `new Function` in SW chunk was confirmed pre-existing from Plan 01-12 deferred-items.md; the JSZip `eval` + ts-ebml `Buffer.*` matches in vendor bundles were also pre-existing. The Tier-1 hook-string grep gate (the production-relevant Plan-01-10-scoped check) stayed at 12 strings throughout.

### Benign positive deviations

**1. A17 sub-check count grew 7 → 8 during the welcome-mark debug**
- **Found during:** d48a715 fix-and-verify
- **Issue:** Plan baseline specified A17 with 7 sub-checks (A17.1-A17.7). After welcome-mark fix, an A17.8 sub-check was added to lock the mark-bundling invariant going forward.
- **Action taken:** None (still vacuously GREEN: A17.8 verifies the inlined `data:image/svg+xml,...` data URL OR file URL with canonical `viewBox='0 0 32 32'` preserved in the welcome chunk JS).
- **Impact:** Net positive — harness now defends both the canonical-tokens @import (A17.5/A17.7) AND the canonical-mark SVG bundling (A17.8) invariants.

**2. vitest count grew 150 → 153 during the startVideoCapture no-tab debug**
- **Found during:** a2dfc8c TDD
- **Issue:** Plan baseline anticipated 150 GREEN at closure (147 pre-plan + 3 onboarding). Debug session added 3 RED→GREEN tests at `tests/background/start-video-capture-no-tab.test.ts`.
- **Action taken:** None (these tests pin a contract that prevents future Plan-01-09 regressions on the notification-click start path).
- **Impact:** Net positive — Plan 01-09 contract coverage expanded with concrete contract pinning; baseline now 153 GREEN.

**3. _locales/{en,ru}/messages.json key count grew 16 → 17 during the notifStartup debug**
- **Found during:** 4bba679 fix design
- **Issue:** Plan 01-12 Wave 3 baseline was 16 keys; closure-cycle debug split notifStartup into notifStartupCta + notifRecordingStarted (net +1 key per locale; total 17 keys per locale).
- **Action taken:** None — locale-parity test (en↔ru symmetric) stays GREEN at 4/4.
- **Impact:** Net positive — operator-facing key names follow a discoverable pattern (`notifStartupCta` = action invite; `notifRecordingStarted` = state confirmation; future i18n contributors get unambiguous semantic hints from the key alone).

### Authentication gates

None encountered. The plan's `<verify>` chain (`npm run build`, `npx tsc --noEmit`, `npm test`, `npm run test:uat`) is fully autonomous (Chrome launches headlessly via Puppeteer; no operator interaction needed except for the explicit Wave 4 operator empirical checkpoint).

## TDD Gate Compliance

This plan was structured with Wave 0 RED → Wave 2 GREEN gate sequence. Git log verification:

- **RED gate:** `89e1e09 test(01-10): wave-0 task-1 — RED onboarding tests` — present.
- **GREEN gate:** `8f329d8 feat(01-10): wave-2 task-3 — openWelcomeIfFirstInstall helper + onInstalled wiring (D-17-onboarding) — 3 RED → GREEN` — present.
- **REFACTOR gate:** Not separately landed (no behavior-preserving cleanup needed beyond Wave 2).

A second RED→GREEN cycle landed during the `a2dfc8c` debug session:
- RED→GREEN combined commit `a2dfc8c fix(01-09): startVideoCapture — remove stale active-tab dependency (D-01 cleanup gap)` — landed RED test file (start-video-capture-no-tab.test.ts) + GREEN fix to startVideoCapture as a single atomic commit per debug-session convention.

## Architectural Notes Worth Carrying Forward

- **First-install activation pattern.** `chrome.runtime.onInstalled('install')` + `chrome.storage.local` flag-gating + `chrome.tabs.create` fire-and-forget invocation with `.catch` defense-in-depth is now the canonical pattern for any future install-time activation flow (e.g., post-update changelog, opt-in feature prompts). The pattern survives chrome.runtime.InstalledDetails reason variance (install / update / chrome_update / shared_module_update) cleanly via early-return.

- **`data-mokosh-slot` as design-swap landmark.** Even though the mark-bundling fix injected the SVG directly via `populateMark()`, the `data-mokosh-slot='mark'` wrapper attribute remains on the div for forward-compat. Future design swaps (e.g., a different mark variant, an animated mark, a designer-driven SVG iteration) can hook into this slot via the populateMark() pipeline without touching welcome.html structure. The slot infrastructure is the canonical extension point.

- **Vite `?url` import + auto-WAR is the minimum-coupling asset bundling idiom.** Compared to manual web_accessible_resources mutation + `chrome.runtime.getURL` (Option A) or inline SVG in HTML (Option C), the `?url` import: (1) avoids manifest.json edit; (2) cache-busts via hashed filenames automatically; (3) default-inlines small assets (≤4096 B) as data URLs to skip extra HTTP requests; (4) transparently switches to file URLs for larger assets; (5) leverages @crxjs/vite-plugin's auto-WAR for transitively-reachable resources from extension pages. Future asset additions (icons, brand SVGs, audio cues) follow this idiom.

- **Per-it() timeout is the vitest idiom for slow IO.** Build-touching tests (running `npm run build` inside the test) routinely declare 10s+ ceilings. Global `testTimeout` would over-relax 95% of pure-CPU tests that should keep the 5s default (catches accidental hangs fast). The pattern: bound the EXEC-level child-process timeout via a constant near the top of the file AND bound the surrounding `it()` block with the same magnitude.

- **Plan 01-09 + Plan 01-10 coupling surfaced an under-covered notification.onClicked path.** A8 (Bug A canonical regression-rewind) covers `chrome.notifications.create` acceptance; no harness assertion covers the `chrome.notifications.onClicked` → `startVideoCapture` routing. The `a2dfc8c` debug session pinned the contract at the unit-test layer (start-video-capture-no-tab.test.ts) but a UAT-harness-level assertion (e.g., A24 simulating notification click → badge transitions to REC) would close the loop. Logged for future plan in Phase 5 hardening.

- **i18n key naming conveys semantic intent.** The notifStartup → notifStartupCta + notifRecordingStarted split is a microcosm of the pattern: action-invite vs state-confirmation keys should be named distinctly. Future i18n contributors get unambiguous semantic hints from the key alone; the `.description` field reinforces this with explicit scoping ("Per Phase 1 always-on charter: recording does NOT auto-start").

- **Plan 01-12 path-B contract verified empirically.** A17.7 getComputedStyle probe on `--mks-rec` resolves to `rgb(178, 84, 61)` (= #b2543d = --mks-madder-600) — proves the canonical tokens.css @import wires through to canonical token values at runtime, not just engineering placeholders. This is the strongest invariant the harness can express for cross-file CSS variable resolution.

## Self-Check: PASSED

**File-existence verification:**
- `src/welcome/welcome.html` — FOUND
- `src/welcome/welcome.ts` — FOUND
- `src/welcome/welcome.css` — FOUND
- `src/welcome/copy.ts` — FOUND
- `src/welcome/welcome-tokens.css` — **NOT PRESENT** (Plan 01-12 path-B contract: no placeholder file created)
- `globals.d.ts` — FOUND (closure-cycle debug d48a715; ambient module decl for `*.svg?url`)
- `tests/background/onboarding.test.ts` — FOUND (Wave 0; 3 tests)
- `tests/background/start-video-capture-no-tab.test.ts` — FOUND (closure-cycle debug a2dfc8c; 3 tests)
- `tests/uat/extension-page-harness.ts` — modified (assertA15/A16/A17 with 8 sub-checks)
- `tests/uat/lib/harness-page-driver.ts` — modified (driveA15/A16/A17)
- `tests/uat/harness.test.ts` — modified (drivers list with A15-A17 interleaved)
- `src/background/index.ts` — modified (openWelcomeIfFirstInstall + onInstalled wiring; later startVideoCapture cleanup at a2dfc8c)
- `manifest.json` — modified (web_accessible_resources entry; default_locale + __MSG_*__ preserved)
- `vite.config.ts` + `vite.test.config.ts` — both modified (welcome rollup entry; __VITE_DEV__ + __MOKOSH_UAT__ defines preserved verbatim)
- `_locales/{en,ru}/messages.json` — modified (closure-cycle debug 4bba679; 16 → 17 keys)
- `README.md` + `package.json` + `tests/i18n/manifest-i18n.test.ts` — modified (closure-cycle debug d21ed17; brand polish)

**Commit-existence verification:**
- `7f58e0a` — plan baseline revision (post-01-12 + 01-14) — FOUND
- `89e1e09` — Wave 0 RED onboarding tests — FOUND
- `49f087f` — Wave 1 welcome bundle + Vite + manifest — FOUND
- `8f329d8` — Wave 2 openWelcomeIfFirstInstall + onInstalled wiring — FOUND
- `b112cb7` — Wave 3 harness A15+A16+A17 — FOUND
- `4bba679` — cycle-1 debug 01-09 notifStartup split — FOUND
- `d48a715` — cycle-1 debug 01-10 welcome page mark — FOUND
- `0854baf` — cycle-1 debug 01-10 vitest build-test timeout — FOUND
- `a2dfc8c` — cycle-1 debug 01-09 startVideoCapture no-tab — FOUND
- `d21ed17` — cycle-2 follow-up debug 01-12 brand polish — FOUND

**Gate evidence:**
- `npm test` → 153/153 GREEN (verified post-a2dfc8c; +6 from Plan 01-10 baseline of 147)
- `npm run test:uat` → 24/24 GREEN (verified post-b112cb7 + d48a715 + all 5 closure-cycle commits)
- `npx tsc --noEmit` → clean (verified across every wave + every debug commit)
- `npm run build` + `npm run build:test` → both clean
- Production bundle `dist/` grep for `AI Call Recorder` + `ai-call-extension` → ZERO matches (post-d21ed17)
- Tier-1 FORBIDDEN_HOOK_STRINGS inventory: 12 entries (unchanged from Plan 01-14 baseline)
- A17.7 getComputedStyle probe: `rgb(178, 84, 61)` (= #b2543d = --mks-madder-600 canonical resolution verified)
- A17.8 mark-bundling sub-check: `data:image/svg+xml,...` data URL with canonical `viewBox='0 0 32 32'` preserved in welcome chunk JS
- Operator brand-fit ack 2026-05-20: verbatim "All good" on welcome page + onStartup notification flow + brand-rename follow-up

## Known Limitations / Followups

- **`tabs` permission gap** (Phase 5 candidate, also surfaced in Plan 01-13) — `captureScreenshot()` + `saveArchive()` retain genuine `chrome.tabs.query({active:true})` dependencies; both fire under operator gestures that DO grant activeTab, so the bug is functionally absent. A future Plan adding `tabs` to the manifest permission set would let `notifications.onClicked` + future no-gesture paths read tab metadata, but widens the permission surface — not required for current behavior.
- **Dark-surface contrast for chrome.notifications icon128** (Phase 5 candidate, surfaced via 01-10-welcome-page-missing-mark debug operator forward-looking concern) — the canonical mark has a dark ink stroke (`stroke='#181b2a'`). On the welcome hero's rec-orange circle background, this is HIGH CONTRAST and is the correct design. On OTHER surfaces (e.g., system-dark-mode notification panel), a light-variant of the mark (white stroke) chosen via `prefers-color-scheme` OR a separate icon asset is required. Out of scope for current plan.
- **Notification.onClicked harness coverage** (Phase 5 candidate, surfaced via 01-09-notification-start-no-active-tab debug retro) — A8 covers `chrome.notifications.create` acceptance; no harness assertion covers `chrome.notifications.onClicked` → `startVideoCapture` routing. A future Plan adding e.g. A24 (notification click → badge transitions to REC) would close the regression-rewind loop at the UAT layer; current contract is unit-test-pinned via `start-video-capture-no-tab.test.ts`.
- **Plan 01-09 over-tested keys vs under-tested branches** — the notifStartup conflation (debug 01-09-startup-notification-misleading-text) surfaced an i18n-key-naming pattern lesson: keys that conflate two semantic intents (CTA-with-gesture vs state-confirmation) are a code-smell. Future i18n key naming reviews should split conflated keys at design time, not at operator-UAT time.
- **setimmediate polyfill `new Function` in SW chunk** — pre-existing across Phase 1 history (verified via `git checkout main`); not a Plan 01-10 regression. Logged at `.planning/phases/01-stabilize-video-pipeline/deferred-items.md` for Phase 5 hardening per Plan 01-12 Wave 7 pre-checkpoint disclosure.
- **`.planning/intel/*` audit trail preservation** — the brand-polish debug deliberately preserved all `.planning/intel/*` historical references to "AI Call Recorder" because those documents are audit trail (D-07 override decision rationale + design-system + brand-identity + classifications + handoff). Future content sweeps should follow the same scope-discipline: code surfaces + operator-facing copy + npm metadata get renamed; planning intel stays verbatim as historical record.

## Bridge to Phase 1 Closure

Phase 1 functional contract was **CLOSED via Plan 01-13's harness PASS** (2026-05-19; 15/15 GREEN). Phase 1 design/brand contract was **CLOSED via Plan 01-12's operator brand-fit ack** (2026-05-20 verbatim "all good"; 21/21 UAT GREEN). Plan 01-10 (welcome tab) was the **LAST remaining Phase 1 functional plan** — now CLOSED via this SUMMARY + operator cycle-2 ack 2026-05-20 verbatim "All good" + cycle-2 follow-up brand-rename ack via commit `d21ed17`.

**Phase 1 final-closure marker flip pending** (REQUIREMENTS / ROADMAP / STATE markers): this SUMMARY + the paired `docs(01-10): state + roadmap + requirements` commit unblocks Phase 1 final-closure. Plan count after closure: 14/14 (01-01 through 01-14 inclusive; Plan 01-11 closed as spike-pivot in `ba5474c`). Phase 1 progress: 14/14 plans = 100% functional + brand contract; remaining marker work is `/gsd-verify-work 1` (optional) + cross-phase requirements flip + STATE.md `status: executing` → `complete` (or equivalent terminal status for Phase 1 only — Phase 2 onwards still in `not started`).

**Phase 2 (DOM + event-capture privacy) inherits:**
- `src/welcome/` page bundle pattern (welcome.html + welcome.ts + welcome.css + copy.ts; data-mokosh-key + data-mokosh-i18n-key + data-mokosh-slot attribute namespaces) — applicable for any future operator-facing extension page (settings, changelog, opt-in prompts).
- `chrome.runtime.onInstalled` flag-gated first-install activation pattern — applicable for opt-in features.
- A15-A17 harness contracts as regression-rewind templates for any future operator-facing surface.
- Vite `?url` import + auto-WAR idiom for bundling extension assets.

## Files Modified Summary

| Category | Files | Lines (approx) | Wave / Commit |
|---|---|---|---|
| Welcome page bundle | 4 new files (welcome.html + welcome.ts + welcome.css + copy.ts) | ~250 LoC | Wave 1 (`49f087f`) + Wave-4-debug (`d48a715`) |
| Onboarding SW wiring | src/background/index.ts (helper + onInstalled extend) + later startVideoCapture cleanup at `a2dfc8c` | ~50 LoC net | Wave 2 (`8f329d8`) + closure debug (`a2dfc8c`) |
| Manifest + Vite config | manifest.json (WAR entry) + vite.config.ts (rollup entry) + vite.test.config.ts (mirror) | ~10 LoC | Wave 1 (`49f087f`) |
| Onboarding unit tests | tests/background/onboarding.test.ts + start-video-capture-no-tab.test.ts | ~200 LoC | Wave 0 (`89e1e09`) + closure debug (`a2dfc8c`) |
| Harness A15-A17 | extension-page-harness.ts + harness-page-driver.ts + harness.test.ts | ~250 LoC | Wave 3 (`b112cb7`) + A17.8 extension at `d48a715` |
| Closure-cycle debug — i18n split | _locales/{en,ru}/messages.json + src/background/index.ts + onstartup-notification.test.ts | ~20 LoC | `4bba679` |
| Closure-cycle debug — vitest timeout | tests/background/no-test-hooks-in-prod-bundle.test.ts (it() ceiling) | ~10 LoC | `0854baf` |
| Closure-cycle debug — globals decl | globals.d.ts | ~5 LoC | `d48a715` |
| Closure-cycle debug — brand polish | src/welcome/copy.ts + README.md + package.json + tests/i18n/manifest-i18n.test.ts | ~15 LoC | `d21ed17` |
| **Total** | **14 files** (4 new + 10 modified) | **~810 LoC net** | **9 commits across 4 waves + 5 closure-cycle debug sessions** |

---

*Phase: 01-stabilize-video-pipeline*
*Plan: 10*
*Completed: 2026-05-20*
*Operator cycle-2 ack: 2026-05-20 verbatim "All good" + cycle-2 follow-up brand-rename ack via `d21ed17`*
*Phase 1 functional plans: 14/14 complete (01-01..01-09 + 01-11 spike + 01-12 + 01-13 + 01-14 + 01-10)*
