---
phase: 01-stabilize-video-pipeline
plan: 10
type: execute
wave: 4
depends_on:
  - 01-09
  - 01-13
  - 01-14
  - 01-12
files_modified:
  - manifest.json
  - vite.config.ts
  - vite.test.config.ts
  - src/welcome/welcome.html
  - src/welcome/welcome.ts
  - src/welcome/welcome.css
  - src/welcome/copy.ts
  - src/background/index.ts
  - tests/background/onboarding.test.ts
  - tests/uat/extension-page-harness.ts
  - tests/uat/extension-page-harness.html
  - tests/uat/lib/harness-page-driver.ts
  - tests/uat/harness.test.ts
autonomous: false
requirements:
  - REQ-video-ring-buffer
tags:
  - onboarding
  - welcome
  - chrome.runtime.onInstalled
  - chrome.storage
  - web_accessible_resources
  - canonical-tokens-import
  - css-variables
  - chrome.i18n-welcomeHero
  - D-17-onboarding
  - D-02-welcome-layout
  - D-08-tagline
  - harness-A15-A17
  - post-01-12-baseline
  - post-01-14-baseline

must_haves:
  truths:
    - "On first install (chrome.runtime.onInstalled with reason 'install') AND chrome.storage.local.get('onboarding-completed') returns nothing, exactly ONE welcome tab opens automatically pointing at chrome.runtime.getURL('src/welcome/welcome.html'). After the tab opens, chrome.storage.local.set is called with {'onboarding-completed': true, 'installed-at': <Date.now()>}."
    - "On subsequent installs (reason 'update' or 'chrome_update') OR when 'onboarding-completed' is already true, NO welcome tab opens (chrome.tabs.create is NOT called)."
    - "The welcome page renders a hero (.welcome-hero slot) containing a placeholder mark, the D-08 tagline string (Russian primary + English subtitle), 2-3 explainer lines, a primary instruction pointing the operator at the toolbar icon (NOT a Start button — toolbar onClicked owns the start path per Plan 01-09 D-16-toolbar), and a privacy/safety footer."
    - "The welcome page contains NO hardcoded hex color values in welcome.css — every color is sourced from a --mks-* CSS custom property. welcome.css opens with a one-liner `@import '../shared/tokens.css';` (Plan 01-12 must_have #9 path-B contract honored: Plan 01-12 landed FIRST; canonical src/shared/tokens.css is import-ready; NO placeholder welcome-tokens.css is created)."
    - "The D-08 hero tagline strings render via chrome.i18n.getMessage('welcomeHeroRu') and chrome.i18n.getMessage('welcomeHeroEn') reads at populate time — the canonical keys exist in _locales/{en,ru}/messages.json from Plan 01-12 Wave 3. A `|| <en-const>` fallback per Plan 01-12's chrome.i18n.getMessage fallback pattern is included verbatim at each call site. src/welcome/copy.ts retains the COPY map ONLY for non-tagline keys (page title + explainer lines + CTA + privacy footer) which Plan 01-12 did NOT migrate to _locales/."
    - "Typography uses font-family: var(--mks-font-display, <system fallback>) and var(--mks-font-ui, <system fallback>) — the canonical tokens.css from Plan 01-12 supplies Lora (display) + IBM Plex Sans (UI) via self-hosted @font-face rules; system fallback chain is preserved as graceful degradation."
    - "The welcome page references NO REQUEST_PERMISSIONS message type, NO chrome.runtime.sendMessage start path, NO duplicate getDisplayMedia trigger. Its CTA is informational — points operator at the toolbar icon (01-09 owns toolbar onClicked → picker)."
    - "Onboarding unit test covers BOTH first-install AND subsequent-install paths AND the storage-key contract via three synthesized onInstalled events + chrome.storage.local.get mock setups."
    - "UAT harness extends with three new assertions (A15, A16, A17). A15 = onboarding flag observability (chrome.storage.local 'onboarding-completed' is true AND 'installed-at' is a number AT LEAST ONCE during this session). A16 = subsequent-install does NOT re-open (post-A15 settle observation: no new welcome tabs spontaneously appear; chrome.tabs.query count delta is 0 over 2-second window). A17 = design-swap-readiness invariant: welcome.html parses; .welcome-hero slot exists; welcome.css contains the `@import '../shared/tokens.css';` directive (or equivalent compiled output); welcome.css contains zero #hex literals (regex /#[0-9a-fA-F]{3,8}/ match count == 0); welcome.html contains ≥ 7 data-mokosh-key attributes; bundled welcome JS chunk contains EITHER substring 'COPY[' (non-tagline keys) OR chrome.i18n.getMessage('welcomeHero references; getComputedStyle on the .welcome-hero element resolves --mks-rec to '#b2543d' OR equivalent rgb(178,84,61) (proves the @import resolves canonical token values, not just engineering placeholders)."
    - "npm run test:uat reports 24/24 GREEN at plan close (current baseline 21 from A0-A14 + A18-A22 + A23 + new A15 + A16 + A17). npm test (vitest) reports current baseline 147 GREEN + 3 new onboarding tests GREEN = 150 GREEN; Tier-1 FORBIDDEN_HOOK_STRINGS inventory unchanged at 12 entries (10 original Plan 01-13 + 2 Plan 01-14 additions; Plan 01-10 introduces no new test-mode symbols)."
  artifacts:
    - path: "src/welcome/welcome.html"
      provides: "Welcome page markup. Hero wrapped in <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' role='presentation' data-mokosh-slot='mark'> (future swap to mokosh-lockup.svg). Two hero tagline elements carry data-mokosh-i18n-key='welcomeHeroRu' and data-mokosh-i18n-key='welcomeHeroEn' for the chrome.i18n.getMessage populate path."
      min_lines: 30
    - path: "src/welcome/welcome.ts"
      provides: "Vanilla DOM entry point. Imports COPY from './copy' AND uses chrome.i18n.getMessage for the welcomeHeroRu + welcomeHeroEn keys. On DOMContentLoaded: (a) populates each [data-mokosh-key='<key>'] element's textContent from COPY[key] (non-tagline keys), (b) populates each [data-mokosh-i18n-key='<key>'] element's textContent from chrome.i18n.getMessage(key) || <en-const-fallback>. No event handlers — welcome tab is informational + read-only (per D-16-toolbar charter). Uses centralized Logger from src/shared/logger.ts (new Logger('Welcome'))."
      min_lines: 30
    - path: "src/welcome/welcome.css"
      provides: "Welcome page styling. FIRST LINE is `@import '../shared/tokens.css';` (Plan 01-12 canonical tokens). Every color references var(--mks-*); every font references var(--mks-font-*). Layout: max-width 720px (matches --mks-welcome-max-w in tokens.css), centered, hero+body+footer vertical stack. ZERO hex literals. Single <link rel=stylesheet> in welcome.html (the @import inside this file resolves the canonical bundle at build time)."
      min_lines: 30
    - path: "src/welcome/copy.ts"
      provides: "Single source-of-truth COPY map for non-tagline strings. Exports const COPY: Readonly<Record<string, string>> keyed by stable identifiers (welcome.page.title, welcome.hero.title, welcome.body.explainer.line1, welcome.body.explainer.line2, welcome.body.cta.toolbar, welcome.footer.privacy). The two D-08 tagline strings (welcomeHeroRu + welcomeHeroEn) are NOT in this map — they live in _locales/{en,ru}/messages.json per Plan 01-12 Wave 3 and are read via chrome.i18n.getMessage at populate time. COPY map can hold en-const fallbacks for the welcomeHero keys as commented constants (so welcome.ts has a graceful-degradation copy-paste path matching Plan 01-12's `|| <en-const>` fallback pattern)."
      min_lines: 25
    - path: "src/background/index.ts"
      provides: "openWelcomeIfFirstInstall(details) helper added (placed below ensureOffscreen). chrome.runtime.onInstalled handler extended to call helper AFTER existing IDB cleanup + initialize(). Helper: early-return on non-install reason; await chrome.storage.local.get; early-return if flag already true; chrome.tabs.create + chrome.storage.local.set. Fire-and-forget invocation wrapped in .catch(...)."
      contains: "openWelcomeIfFirstInstall"
    - path: "manifest.json"
      provides: "web_accessible_resources array added (was absent). Single entry {resources:['src/welcome/welcome.html'], matches:['<all_urls>']}. storage permission already present at line 11; no change. No new permissions. default_locale='en' + __MSG_*__ pattern from Plan 01-12 Wave 3 preserved verbatim."
      contains: "web_accessible_resources"
    - path: "vite.config.ts"
      provides: "rollupOptions.input gains welcome: 'src/welcome/welcome.html' alongside existing offscreen entry. __VITE_DEV__ + __MOKOSH_UAT__ define-tokens from Plan 01-12 Wave 5 preserved verbatim."
      contains: "welcome:"
    - path: "vite.test.config.ts"
      provides: "Mirror of vite.config.ts: rollupOptions.input gains welcome entry so dist-test/ also carries the page (harness A15-A17 navigates fresh-install Chrome with test build loaded)."
      contains: "welcome:"
    - path: "tests/background/onboarding.test.ts"
      provides: "Three RED→GREEN tests pinning the onInstalled contract. Test A: install reason + empty storage → exactly one tabs.create with welcome URL + storage.set with onboarding-completed:true + installed-at:<number>; additionally asserts chrome.storage.local.get called with EXACT key 'onboarding-completed' (storage-schema cross-version-compat pin, preserved from prior plan I-02 fix). Test B: update reason → zero tabs.create. Test C: install + flag already set → zero tabs.create."
      min_lines: 120
    - path: "tests/uat/extension-page-harness.ts"
      provides: "Three new page-side assertion methods on window.__mokoshHarness: assertA15 (onboarding flag observability), assertA16 (subsequent-install does NOT re-open — settle-window check), assertA17 (design-swap-readiness — welcome.html parse + .welcome-hero slot + welcome.css @import directive + zero-hex regex + ≥7 data-mokosh-key + bundled JS contains COPY[ OR chrome.i18n.getMessage('welcomeHero substring + getComputedStyle var resolution check)."
      min_lines: 170
    - path: "tests/uat/lib/harness-page-driver.ts"
      provides: "Three new host-side driver wrappers: driveA15, driveA16, driveA17. Standard page.evaluate pattern matching driveA14."
      min_lines: 50
    - path: "tests/uat/harness.test.ts"
      provides: "Drivers list extended with A15, A16, A17 entries (interleaved into existing A0-A14 + A18-A22 + A23 ordering). FORBIDDEN_HOOK_STRINGS unchanged at 12 entries. Total assertions become 24 (A0-A14 + A15-A17 + A18-A22 + A23 — A18-A22 from Plan 01-12, A23 from Plan 01-14)."
      contains: "A15"
  key_links:
    - from: "src/background/index.ts onInstalled handler (line 724+)"
      to: "openWelcomeIfFirstInstall → chrome.tabs.create"
      via: "Fire-and-forget invocation after initialize(); helper awaits chrome.storage.local.get + chrome.tabs.create + chrome.storage.local.set"
      pattern: "openWelcomeIfFirstInstall"
    - from: "manifest.json web_accessible_resources"
      to: "src/welcome/welcome.html"
      via: "MV3 web_accessible_resources array with matches:['<all_urls>']"
      pattern: "src/welcome/welcome.html"
    - from: "src/welcome/welcome.ts init"
      to: "src/welcome/copy.ts COPY map + chrome.i18n.getMessage"
      via: "import { COPY } from './copy'; populates [data-mokosh-key='<key>'] textContent from COPY[key]; populates [data-mokosh-i18n-key='<key>'] textContent from chrome.i18n.getMessage(key) || <en-const-fallback>"
      pattern: "data-mokosh-key|data-mokosh-i18n-key"
    - from: "src/welcome/welcome.css"
      to: "src/shared/tokens.css"
      via: "Top-of-file @import '../shared/tokens.css'; — canonical Plan 01-12 tokens (Lora @font-face + D-04 Loom palette + --mks-* variable set) cascades into welcome.css scope"
      pattern: "@import.*shared/tokens.css"
    - from: "_locales/{en,ru}/messages.json welcomeHeroRu + welcomeHeroEn"
      to: "src/welcome/welcome.ts populate path"
      via: "chrome.i18n.getMessage('welcomeHeroRu') + chrome.i18n.getMessage('welcomeHeroEn') reads with `|| <en-const>` fallback per Plan 01-12 fallback pattern"
      pattern: "chrome.i18n.getMessage\\('welcomeHero"
    - from: "tests/uat/harness.test.ts drivers list"
      to: "tests/uat/lib/harness-page-driver.ts driveA15/A16/A17"
      via: "Driver tuple appended to drivers ReadonlyArray, same idiom as driveA14"
      pattern: "driveA15"
    - from: "tests/uat/extension-page-harness.ts window.__mokoshHarness"
      to: "assertA15/A16/A17 methods"
      via: "Added to existing __mokoshHarness surface alongside assertA1..A14 + assertA18..A22 + assertA23"
      pattern: "assertA15"
---

<objective>
First-install operator-friendly activation: when the extension is installed (Chrome Web Store one day, or Load Unpacked today), open a welcome tab automatically pointing at src/welcome/welcome.html. The page renders a hero + tagline + 2-3 explainer lines + a CTA instructing the operator to click the toolbar icon to begin recording. Subsequent installs/updates do NOT re-open.

This complements Plan 01-09's runtime activation paths (toolbar onClicked → picker; onStartup notification at browser launch) by adding the install-time activation path. The welcome tab does NOT trigger recording itself — start remains a SINGLE path through chrome.action.onClicked in idle mode (D-16-toolbar). The welcome tab is informational + read-only; CTA copy directs the operator at the toolbar.

**Plan 01-12 landed FIRST (commits 34a9ce1 → b909c37 → 865d394; SUMMARY 2026-05-20).** Per Plan 01-12 must_have #9 path-B contract: canonical src/shared/tokens.css is import-ready (Lora self-hosted, D-04 Loom palette, --mks-* full set); _locales/{en,ru}/messages.json carries the welcomeHeroRu + welcomeHeroEn keys; no placeholder welcome-tokens.css is needed. Plan 01-10 ships welcome.css with `@import '../shared/tokens.css';` from day 1 AND populates the two D-08 tagline elements via chrome.i18n.getMessage with `|| <en-const>` fallback per the Plan 01-12 fallback pattern. Non-tagline copy (page title + explainer lines + CTA + footer privacy) continues via the in-file COPY map (those keys are not in _locales/ — engineering placeholder until a future copy-iteration plan migrates them).

**Plan 01-14 also landed (commits b467123 → 9792c0f; SUMMARY 2026-05-19).** Per Plan 01-14: vitest baseline is 147 GREEN (not 98); UAT baseline is 21 GREEN (A0-A14 + A18-A22 + A23); FORBIDDEN_HOOK_STRINGS inventory is 12 entries (10 original + 2 Plan 01-14 additions: `lastGetDisplayMediaConstraints` + `get-last-getDisplayMedia-constraints`). Plan 01-10 introduces ZERO new test-mode symbols at the chrome.* + fetch + getComputedStyle production-API layer A15-A17 uses; the inventory stays at 12.

**Harness integration (A15-A17).** Extend the Plan 01-13 + 01-14 + 01-12 harness with three new assertions covering: onboarding flag observability (A15), subsequent-install does NOT re-open (A16), design-swap-readiness invariant (A17 — extended to verify the canonical @import resolves AND --mks-rec resolves to non-empty value via getComputedStyle). Drivers register alongside driveA1..driveA14 + driveA18..driveA22 + driveA23 in tests/uat/harness.test.ts.

Output:
- src/welcome/welcome.{html,ts,css} + src/welcome/copy.ts — 4 new files for the canonical-tokens + chrome.i18n welcome page (NO placeholder welcome-tokens.css; that file is NEVER created per Plan 01-12 path-B).
- src/background/index.ts — openWelcomeIfFirstInstall helper + onInstalled wiring.
- manifest.json — web_accessible_resources array entry for the welcome page.
- vite.config.ts + vite.test.config.ts — rollupOptions.input gains welcome entry in BOTH bundles.
- tests/background/onboarding.test.ts — 3 RED→GREEN unit tests.
- tests/uat/extension-page-harness.{html,ts} + tests/uat/lib/harness-page-driver.ts + tests/uat/harness.test.ts — A15+A16+A17 harness assertions.

Cites: D-17-onboarding (CONTEXT.md line 537+), D-02 (welcome layout — brand-decisions-v1.md), D-08 (tagline — brand-decisions-v1.md), D-03 (Sober voice register — brand-decisions-v1.md), D-16-toolbar (toolbar owns start — 01-09-SUMMARY.md), Plan 01-12 must_have #9 path-B (canonical tokens.css @import + chrome.i18n for welcomeHero), Plan 01-14 FORBIDDEN_HOOK_STRINGS baseline at 12.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/REQUIREMENTS.md
@.planning/phases/01-stabilize-video-pipeline/01-CONTEXT.md
@.planning/phases/01-stabilize-video-pipeline/01-09-SUMMARY.md
@.planning/phases/01-stabilize-video-pipeline/01-11-SUMMARY.md
@.planning/phases/01-stabilize-video-pipeline/01-12-SUMMARY.md
@.planning/phases/01-stabilize-video-pipeline/01-13-SUMMARY.md
@.planning/phases/01-stabilize-video-pipeline/01-14-SUMMARY.md
@.planning/intel/brand-decisions-v1.md
@.planning/intel/brand-decisions-v1-followup-display-font.md
@src/shared/tokens.css
@src/background/index.ts
@src/popup/index.html
@src/popup/index.ts
@src/shared/logger.ts
@manifest.json
@vite.config.ts
@vite.test.config.ts
@_locales/en/messages.json
@_locales/ru/messages.json
@tests/background/onstartup-notification.test.ts
@tests/uat/extension-page-harness.html
@tests/uat/extension-page-harness.ts
@tests/uat/lib/harness-page-driver.ts
@tests/uat/harness.test.ts
@tests/background/no-test-hooks-in-prod-bundle.test.ts

<interfaces>
Key Chrome API surfaces and project conventions the executor needs.

1. chrome.runtime.onInstalled
=============================

Signature (from @types/chrome):
  chrome.runtime.onInstalled.addListener(
    callback: (details: chrome.runtime.InstalledDetails) => void): void
  interface InstalledDetails {
    reason: 'install' | 'update' | 'chrome_update' | 'shared_module_update';
    previousVersion?: string;
    id?: string;
  }

CURRENT handler in src/background/index.ts (line 724+, post-01-09):
  chrome.runtime.onInstalled.addListener((details) => {
    logger.log('Extension installed/updated:', details.reason);
    try {
      indexedDB.deleteDatabase('VideoRecorderDB');
      logger.log('Cleaned up orphaned VideoRecorderDB (if present)');
    } catch (e) {
      logger.warn('IDB cleanup failed:', e);
    }
    initialize();
  });

This plan EXTENDS the handler by appending a fire-and-forget call to
openWelcomeIfFirstInstall(details) AFTER initialize(). The existing
synchronous handler stays synchronous; the helper is async-IIFE-style
with its own try/catch.

2. chrome.storage.local
=======================

Signature:
  chrome.storage.local.get(key: string): Promise<{[key: string]: unknown}>
  chrome.storage.local.set(items: {[key: string]: unknown}): Promise<void>

Used pattern (helper):
  const stored = await chrome.storage.local.get(ONBOARDING_FLAG);
  if (stored[ONBOARDING_FLAG] === true) { return; }
  // open tab ...
  await chrome.storage.local.set({
    [ONBOARDING_FLAG]: true,
    'installed-at': Date.now(),
  });

storage permission is ALREADY in manifest.json:11. No change needed.

3. chrome.tabs.create
=====================

Signature:
  chrome.tabs.create({url: string, active?: boolean}): Promise<chrome.tabs.Tab>

For an extension-internal URL: chrome.runtime.getURL('src/welcome/welcome.html')
returns chrome-extension://<id>/src/welcome/welcome.html. This URL is
ONLY navigable if the path is declared in web_accessible_resources
(MV3 requirement).

4. manifest.json web_accessible_resources (MV3)
================================================

Current manifest.json HAS NO web_accessible_resources block (verified
via Read at plan time). This plan adds one.

Shape (JSON):
  "web_accessible_resources": [
    {
      "resources": ["src/welcome/welcome.html"],
      "matches": ["<all_urls>"]
    }
  ]

Insertion point: after "host_permissions" block, before "background"
block. Verify storage permission still present at line 11. Verify
default_locale='en' + __MSG_*__ pattern from Plan 01-12 Wave 3
preserved verbatim.

5. vite.config.ts rollupOptions.input
======================================

Current state (post-01-12 Wave 5; vite.config.ts):
  rollupOptions: {
    input: { offscreen: 'src/offscreen/index.html' },
  },

Add welcome entry:
  rollupOptions: {
    input: {
      offscreen: 'src/offscreen/index.html',
      welcome: 'src/welcome/welcome.html',
    },
  },

Mirror in vite.test.config.ts at the same key path so dist-test/ also
emits the welcome page. The harness A15/A16/A17 navigates against the
test bundle. Plan 01-12's __VITE_DEV__ + __MOKOSH_UAT__ define-tokens
must be preserved verbatim — do NOT touch.

6. src/welcome/welcome.html structure
======================================

  <!DOCTYPE html>
  <html lang="ru">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title data-mokosh-key="welcome.page.title">Добро пожаловать в Mokosh</title>
    <link rel="stylesheet" href="welcome.css">
  </head>
  <body>
    <main class="welcome">
      <section class="welcome-hero" aria-labelledby="welcome-title">
        <div class="welcome-hero__mark" role="presentation" data-mokosh-slot="mark">
          <span class="welcome-hero__mark-placeholder" aria-hidden="true">Mokosh</span>
        </div>
        <h1 id="welcome-title" class="welcome-hero__title" data-mokosh-key="welcome.hero.title"></h1>
        <p class="welcome-hero__tagline-ru" data-mokosh-i18n-key="welcomeHeroRu"></p>
        <p class="welcome-hero__tagline-en" data-mokosh-i18n-key="welcomeHeroEn"></p>
      </section>
      <section class="welcome-body">
        <p class="welcome-body__line" data-mokosh-key="welcome.body.explainer.line1"></p>
        <p class="welcome-body__line" data-mokosh-key="welcome.body.explainer.line2"></p>
        <p class="welcome-body__cta" data-mokosh-key="welcome.body.cta.toolbar"></p>
      </section>
      <footer class="welcome-footer">
        <p class="welcome-footer__privacy" data-mokosh-key="welcome.footer.privacy"></p>
      </footer>
    </main>
    <script type="module" src="welcome.ts"></script>
  </body>
  </html>

NOTE: ONLY ONE <link rel=stylesheet> tag — welcome.css. The `@import
'../shared/tokens.css';` at the top of welcome.css resolves the canonical
tokens during the Vite build (CSS @import is handled by Rollup's CSS
plugin which inlines or rewrites relative paths). NO placeholder
welcome-tokens.css link tag.

Two new attribute conventions:
  - data-mokosh-key="<copy-map-key>" → populated from src/welcome/copy.ts
    COPY map at populateCopy() time.
  - data-mokosh-i18n-key="<chrome-i18n-key>" → populated from
    chrome.i18n.getMessage(key) || <en-const-fallback> at
    populateI18n() time.

7. src/welcome/copy.ts shape
=============================

  // src/welcome/copy.ts — single source-of-truth for welcome-page
  // NON-TAGLINE copy. Plan 01-12 migrated the two D-08 tagline strings
  // (welcomeHeroRu + welcomeHeroEn) to _locales/{en,ru}/messages.json;
  // those keys are read via chrome.i18n.getMessage in welcome.ts and
  // intentionally NOT included in this map. Remaining keys are
  // engineering-grade placeholders (Russian per D-03 Sober register);
  // a future copy-iteration plan may migrate them to _locales/.
  //
  // D-08 tagline reference (lives in _locales/, not here):
  //   en/welcomeHeroEn → "Thirty seconds ago, always at hand."
  //   ru/welcomeHeroRu → "Тридцать секунд назад, всегда под рукой."

  // Plan 01-12 fallback-pattern constants for the welcomeHero keys.
  // welcome.ts uses these as the `|| <en-const>` fallback when
  // chrome.i18n.getMessage returns empty string (RESEARCH Pitfall 4
  // mitigation). Exported separately from COPY so the i18n populate
  // path imports them by name.
  export const WELCOME_HERO_RU_FALLBACK =
    'Тридцать секунд назад, всегда под рукой.';
  export const WELCOME_HERO_EN_FALLBACK =
    'Thirty seconds ago, always at hand.';

  export const COPY: Readonly<Record<string, string>> = Object.freeze({
    'welcome.page.title': 'Добро пожаловать в Mokosh',
    'welcome.hero.title': 'Mokosh',
    'welcome.body.explainer.line1':
      'Mokosh непрерывно записывает последние 30 секунд экрана и 10 минут ' +
      'логов вашего браузера.',
    'welcome.body.explainer.line2':
      'Когда возникает баг, вы одним кликом сохраняете архив для службы ' +
      'поддержки. Данные не отправляются никуда — только локально.',
    'welcome.body.cta.toolbar':
      'Чтобы начать запись, нажмите иконку AI Call Recorder на панели ' +
      'инструментов браузера (правый верхний угол).',
    'welcome.footer.privacy':
      'Mokosh не отправляет данные на серверы. Архив создаётся ' +
      'локально по вашему запросу и остаётся на вашем компьютере.',
  });

8. src/welcome/welcome.css shape (canonical @import; NO placeholder file)
========================================================================

  /* welcome.css — Plan 01-10 D-17-onboarding welcome page styling.
   * Imports the canonical Plan 01-12 token system (src/shared/tokens.css)
   * which carries the full --mks-* variable set + 8 local @font-face
   * rules + D-04 Loom palette. Every color in this file MUST reference
   * var(--mks-*); ZERO hex literals. */

  @import '../shared/tokens.css';

  .welcome {
    max-width: var(--mks-welcome-max-w);
    margin: 0 auto;
    padding: var(--mks-space-12) var(--mks-space-8);
    background: var(--mks-surface);
    color: var(--mks-fg-1);
    font-family: var(--mks-font-ui);
  }
  /* ... remaining rules — every color via var(--mks-*); every font
   * via var(--mks-font-*); no hex literals. */

KEY INVARIANT (A17 contract):
  - grep -E '#[0-9a-fA-F]{3,8}' src/welcome/welcome.css MUST exit 1.
  - grep -F "@import '../shared/tokens.css'" src/welcome/welcome.css
    MUST exit 0 (the canonical-tokens directive is present).
  - In the built dist/ artifact: the @import is either preserved verbatim
    OR Vite's CSS plugin inlines tokens.css contents — either way the
    `var(--mks-*)` references resolve at runtime.

NO src/welcome/welcome-tokens.css file is created in this plan
(Plan 01-12 must_have #9 path-B contract).

9. src/welcome/welcome.ts shape (filter-pipeline form; no `continue`)
=====================================================================

  import { Logger } from '../shared/logger';
  import {
    COPY, WELCOME_HERO_RU_FALLBACK, WELCOME_HERO_EN_FALLBACK,
  } from './copy';

  const logger = new Logger('Welcome');

  function populateCopy(): void {
    const els = Array.from(document.querySelectorAll<HTMLElement>('[data-mokosh-key]'));
    const pairs = els
      .map((el) => ({ el, key: el.getAttribute('data-mokosh-key') }))
      .filter((p): p is { el: HTMLElement; key: string } =>
        typeof p.key === 'string')
      .map((p) => ({ ...p, value: COPY[p.key] }))
      .filter((p): p is { el: HTMLElement; key: string; value: string } =>
        typeof p.value === 'string');
    for (const { el, value } of pairs) {
      if (el.tagName === 'TITLE') {
        document.title = value;
      } else {
        el.textContent = value;
      }
    }
    const missing = els.length - pairs.length;
    if (missing > 0) {
      logger.warn('populateCopy: ' + String(missing) +
        ' [data-mokosh-key] elements had missing COPY entries');
    }
  }

  function populateI18n(): void {
    // Plan 01-12 fallback pattern: chrome.i18n.getMessage(key) || <en-const>.
    // _locales/{en,ru}/messages.json carries welcomeHeroRu + welcomeHeroEn
    // (16 keys total per Plan 01-12 Wave 3).
    const fallbacks: Readonly<Record<string, string>> = Object.freeze({
      welcomeHeroRu: WELCOME_HERO_RU_FALLBACK,
      welcomeHeroEn: WELCOME_HERO_EN_FALLBACK,
    });
    const els = Array.from(
      document.querySelectorAll<HTMLElement>('[data-mokosh-i18n-key]'),
    );
    const pairs = els
      .map((el) => ({ el, key: el.getAttribute('data-mokosh-i18n-key') }))
      .filter((p): p is { el: HTMLElement; key: string } =>
        typeof p.key === 'string')
      .map((p) => ({
        ...p,
        value: chrome.i18n.getMessage(p.key) || fallbacks[p.key] || '',
      }))
      .filter((p) => p.value.length > 0);
    for (const { el, value } of pairs) {
      el.textContent = value;
    }
    const missing = els.length - pairs.length;
    if (missing > 0) {
      logger.warn('populateI18n: ' + String(missing) +
        ' [data-mokosh-i18n-key] elements had missing chrome.i18n + fallback values');
    }
  }

  function init(): void {
    populateCopy();
    populateI18n();
    logger.log('welcome page ready');
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }

10. Harness A15-A17 architecture
=================================

Pattern mirrors A14 + A18-A23 (recent additions):
  - Page-side: assertA15/A16/A17 in extension-page-harness.ts (added
    to window.__mokoshHarness global surface alongside A1..A14 + A18..A23).
  - Host-side: driveA15/A16/A17 in lib/harness-page-driver.ts —
    standard page.evaluate wrapper.
  - Orchestrator: registered in drivers array in harness.test.ts
    interleaved into existing A0-A14 + A18-A22 + A23 ordering.

A15 — onboarding flag observability:
  Approach: read chrome.storage.local.get(['onboarding-completed',
  'installed-at']) and assert both keys present + correct types.

A16 — subsequent-install does NOT re-open:
  Approach: snapshot chrome.tabs.query({}) tab URLs BEFORE a 2-second
  settle window. After the settle, snapshot again. Assert no new tab
  URLs containing 'src/welcome/welcome.html' appeared.

A17 — design-swap-readiness invariant (EXTENDED per revision):
  Fetch chrome-extension://<id>/src/welcome/welcome.html via
  fetch(chrome.runtime.getURL('src/welcome/welcome.html')).
  Same for src/welcome/welcome.css and the bundled welcome.js chunk
  (locate via parsing the <script src> attribute in welcome.html).
  Assertions:
    1. welcome.html parses; document.querySelector('.welcome-hero') !== null.
    2. welcome.html text: AT LEAST 7 occurrences of 'data-mokosh-key=' OR
       a combined count of 'data-mokosh-key=' + 'data-mokosh-i18n-key='
       summing to ≥ 7 (post-revision: two D-08 tagline elements use the
       i18n attribute; total mokosh-keyed elements ≥ 7 across both attrs).
    3. welcome.css text content: NO match for /#[0-9a-fA-F]{3,8}/
       (post-revision: tokens.css inlining MAY introduce hex literals
       from the canonical token values — if so, the regex match is
       counted ONLY against the welcome.css source-file region, NOT
       inlined tokens.css content; if Vite preserves the @import
       verbatim with no inlining, the welcome.css text is hex-free).
    4. welcome.css text content: AT LEAST 5 occurrences of 'var(--mks-'.
    5. welcome.css text content: contains the substring
       "@import '../shared/tokens.css'" OR equivalent compiled-output
       evidence (e.g., a literal `--mks-rec:` declaration suggesting
       inlined tokens.css content; accept either pattern as proof the
       canonical @import resolved at build time).
    6. The bundled welcome.js chunk text content: contains EITHER
       'COPY[' substring (non-tagline keys) OR
       "chrome.i18n.getMessage('welcomeHero" substring (tagline keys
       via chrome.i18n path) — both prove the populate plumbing
       survives the build.
    7. NEW (post-revision): create a transient probe element with
       inlineStyle `color: var(--mks-rec)`, append to document.body,
       read getComputedStyle(probe).color. Assert the resolved RGB
       equals 'rgb(178, 84, 61)' (= #b2543d = --mks-madder-600 per
       Plan 01-12 Wave 4 D-04 Loom palette adoption). Proves the
       @import actually wires through and tokens resolve to canonical
       values, not just engineering placeholders.

No new FORBIDDEN_HOOK_STRINGS entries — A15-A17 use chrome.tabs.query,
chrome.storage.local.get, fetch on public web_accessible_resources
surfaces, getComputedStyle on a transient probe (all production-API
paths). The Tier-1 grep gate inventory stays at 12.

11. Centralized Logger pattern
===============================

src/shared/logger.ts exports Logger class. Welcome page uses
new Logger('Welcome').

12. Centralized chrome stub pattern for unit tests
===================================================

tests/background/onstartup-notification.test.ts exemplifies the chrome
stub used across Plan 01-09 unit tests. Copy the buildBgStub factory +
extend with:
  - chrome.tabs.create: vi.fn().mockResolvedValue({id:99, url:'x'})
  - chrome.storage.local.{get,set} mocks
  - chrome.runtime.onInstalled rigged with _callbacks array (same idiom
    as onStartup at line 24).

13. _locales/{en,ru}/messages.json (Plan 01-12 Wave 3 baseline)
================================================================

Both locale files carry 16 keys total. The two welcomeHero* keys
relevant to Plan 01-10:

  _locales/en/messages.json:
    "welcomeHeroRu": {
      "message": "Тридцать секунд назад, всегда под рукой.",
      "description": "Welcome page hero — Russian-quoted text..."
    },
    "welcomeHeroEn": {
      "message": "Thirty seconds ago, always at hand.",
      "description": "Welcome page hero — English text..."
    }

  _locales/ru/messages.json: identical message values; locale-tagged
  descriptions.

NOTE: BOTH locales carry BOTH the Russian and English tagline strings
because the welcome page renders parallel-text layout (RU + EN both
visible at all times) regardless of operator locale per D-08.
chrome.i18n.getMessage('welcomeHeroRu') always returns the Russian
string; chrome.i18n.getMessage('welcomeHeroEn') always returns the
English string. No locale-conditional rendering.

Plan 01-10 introduces NO new locale keys; it consumes existing ones.
tests/i18n/locale-parity.test.ts (Plan 01-12 Wave 0) continues GREEN.
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: RED unit tests — onInstalled install creates welcome tab; update does NOT; already-completed flag suppresses; storage key contract.</name>
  <read_first>
    - tests/background/onstartup-notification.test.ts lines 1-235 (chrome stub scaffold + reset+inject pattern)
    - src/background/index.ts lines 724-737 (existing onInstalled handler)
    - manifest.json (confirms storage permission already at line 11; default_locale='en' from Plan 01-12)
  </read_first>
  <files>tests/background/onboarding.test.ts</files>
  <behavior>
    Three RED tests pinning truths 1 + 2 + 8 from must_haves:

    Test A — first install + empty storage:
      chrome.storage.local.get.mockResolvedValue({});
      Fire onInstalled with {reason: 'install'}.
      Assert:
        1. chrome.tabs.create called exactly once with object whose
           url contains 'src/welcome/welcome.html'.
        2. chrome.storage.local.set called with object containing
           {'onboarding-completed': true} AND {'installed-at': <number>}.
        3. chrome.storage.local.get called with EXACT key
           'onboarding-completed' (pins schema for cross-version compat;
           preserves the I-02 lesson from prior plan draft).

    Test B — subsequent install (reason='update'):
      chrome.storage.local.get.mockResolvedValue({});
      Fire onInstalled with {reason: 'update'}.
      Assert chrome.tabs.create NOT called AND chrome.storage.local.set
      NOT called.

    Test C — install but flag already set:
      chrome.storage.local.get.mockResolvedValue(
        {'onboarding-completed': true});
      Fire onInstalled with {reason: 'install'}.
      Assert chrome.tabs.create NOT called.
  </behavior>
  <action>
    1. Create tests/background/onboarding.test.ts.
    2. Copy the buildBgStub factory + types from
       tests/background/onstartup-notification.test.ts lines 59-102.
       Extend the stub with:
         - chrome.tabs.create: vi.fn().mockResolvedValue({id:99, url:'x'})
         - chrome.storage = { local: { get: vi.fn(), set: vi.fn() } }
         - chrome.runtime.onInstalled = { addListener: vi.fn(),
             _callbacks: [] }, with addListener.mockImplementation
             pushing into _callbacks
    3. Three tests inside describe('Plan 01-10 onboarding contract', ...)
       — beforeEach { vi.resetModules() }; pattern:
         const stub = buildBgStub();
         stub.storage.local.get.mockResolvedValue(<per test>);
         (globalThis as ...).chrome = stub;
         (globalThis as ...).indexedDB = { deleteDatabase: vi.fn() };
         await import('../../src/background/index');
         const cb = stub.runtime.onInstalled._callbacks[0];
         expect(cb).toBeTypeOf('function');
         cb({ reason: '<per test>' });
         for (let i = 0; i < 16; i++) { await Promise.resolve(); }
         <assertions per behavior>
    4. Run npx vitest run tests/background/onboarding.test.ts — all 3
       MUST be RED (openWelcomeIfFirstInstall helper does not exist;
       chrome.tabs.create never called).
    5. DO NOT modify src/background/index.ts. RED only.

    Style/naming: full-word variable names; no `as any` (use the typed
    extension); no `continue` in test loops (drain via for-i).

    Commit (using Mark's em-dash + Co-Authored-By style):
      test(01-10): wave-0 task-1 — RED onboarding tests (3 failing, pins install/update/flag + storage-key)

      Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  </action>
  <verify>
    <automated>npx vitest run tests/background/onboarding.test.ts 2>&amp;1 | grep -E "3 failed" || (echo "expected 3 failed"; exit 1)</automated>
  </verify>
  <acceptance_criteria>
    - tests/background/onboarding.test.ts exists with exactly 3 tests.
    - All 3 tests RED.
    - npx tsc --noEmit exit 0.
    - Baseline (147 passing after Plan 01-12 + 01-14) preserved; new state 147 + 3 RED = 150 total.
    - No changes to src/background/index.ts.
  </acceptance_criteria>
  <done>3 RED tests pin onInstalled welcome-tab + storage-flag contract; baseline preserved; commit landed.</done>
</task>

<task type="auto">
  <name>Task 2: Welcome page bundle (welcome.html + welcome.ts + welcome.css + copy.ts) + Vite entries in both bundles + manifest web_accessible_resources. Canonical tokens.css @import + chrome.i18n for welcomeHero per Plan 01-12 path-B.</name>
  <read_first>
    - src/popup/index.html (style analog — lang=ru + module script pattern; Plan 01-12 Wave 4 chrome.i18n adoption pattern)
    - src/popup/style.css (Plan 01-12 Wave 4 pattern: `@import "../shared/tokens.css";` at top + ZERO hex literals)
    - src/popup/index.ts (Plan 01-12 Wave 4 chrome.i18n.getMessage with `|| <en-const>` fallback at every operator-facing site)
    - vite.config.ts (rollupOptions.input current shape; preserve __VITE_DEV__ + __MOKOSH_UAT__ defines)
    - vite.test.config.ts (mirrored input shape; preserve mergeConfig pattern)
    - manifest.json (insertion point for web_accessible_resources; confirm default_locale='en' + __MSG_*__ from Plan 01-12 Wave 3 preserved)
    - .planning/intel/brand-decisions-v1.md §D-02 (welcome layout: Hero + Loom dial)
    - .planning/intel/brand-decisions-v1.md §D-08 (tagline RU+EN)
    - .planning/intel/brand-decisions-v1.md §D-03 (Sober voice register)
    - .planning/phases/01-stabilize-video-pipeline/01-12-SUMMARY.md (canonical tokens.css adoption pattern; chrome.i18n fallback pattern)
    - src/shared/tokens.css (canonical token system; --mks-* variable set; @font-face rules)
    - _locales/en/messages.json + _locales/ru/messages.json (welcomeHeroRu + welcomeHeroEn keys land at lines 58-66; confirm pre-existence)
    - src/shared/logger.ts (Logger class shape)
  </read_first>
  <files>
    src/welcome/welcome.html, src/welcome/welcome.ts, src/welcome/welcome.css,
    src/welcome/copy.ts,
    vite.config.ts, vite.test.config.ts, manifest.json
  </files>
  <action>
    1. Create src/welcome/copy.ts per interfaces §7. NON-TAGLINE keys only
       (welcomeHeroRu + welcomeHeroEn intentionally NOT in COPY — they
       are read via chrome.i18n.getMessage). Exports BOTH:
         - WELCOME_HERO_RU_FALLBACK + WELCOME_HERO_EN_FALLBACK constants
           (Plan 01-12 fallback-pattern en-const strings for the `||`
           degradation path).
         - COPY map with placeholder Russian strings per D-03 Sober
           voice register; stable keys.

    2. Create src/welcome/welcome.html per interfaces §6 — semantic
       sections (welcome-hero / welcome-body / welcome-footer); every
       non-tagline textContent target carries data-mokosh-key=stable-key;
       the two D-08 tagline elements carry data-mokosh-i18n-key='welcomeHeroRu'
       and data-mokosh-i18n-key='welcomeHeroEn'. Mark slot is
       data-mokosh-slot=mark. <title> has data-mokosh-key='welcome.page.title'.
       SINGLE <link rel=stylesheet href="welcome.css"> — no separate
       welcome-tokens.css link (tokens load via welcome.css @import).

    3. Create src/welcome/welcome.ts per interfaces §9. Use the
       filter-pipeline form (no `continue`). Import Logger from
       '../shared/logger' + { COPY, WELCOME_HERO_RU_FALLBACK,
       WELCOME_HERO_EN_FALLBACK } from './copy'. document.readyState
       guard: init immediately if already loaded. init() calls BOTH
       populateCopy() AND populateI18n() per interfaces §9.

    4. Create src/welcome/welcome.css per interfaces §8. FIRST LINE is
       `@import '../shared/tokens.css';` (canonical Plan 01-12 tokens
       — Lora self-host + D-04 Loom palette + full --mks-* set). KEY
       INVARIANTS (verified at end of task):
         - grep -E '#[0-9a-fA-F]{3,8}' src/welcome/welcome.css MUST exit 1
           (no matches in this file; the @imported tokens.css IS allowed
           hex literals since it IS the canonical source).
         - grep -F "@import '../shared/tokens.css'" src/welcome/welcome.css
           MUST exit 0.
         - grep -c 'var(--mks-' src/welcome/welcome.css MUST be ≥ 5.
       Every color via var(--mks-*); every font via var(--mks-font-*).
       BEM-ish class names matching the HTML.

    5. DO NOT create src/welcome/welcome-tokens.css. (Plan 01-12 must_have
       #9 path-B: Plan 01-12 landed first; canonical tokens.css is
       import-ready; no placeholder file is needed. Verified: prior plan
       draft included this file; the revised plan REMOVES it from
       files_modified — see frontmatter.)

    6. Update vite.config.ts — add welcome entry alongside offscreen.
       Preserve __VITE_DEV__ + __MOKOSH_UAT__ defines from Plan 01-12
       Wave 5 verbatim; do NOT touch any other field:
         rollupOptions: {
           input: {
             offscreen: 'src/offscreen/index.html',
             welcome: 'src/welcome/welcome.html',
           },
         },

    7. Update vite.test.config.ts — mirror the welcome entry alongside
       extension_page_harness. Preserve the mergeConfig pattern
       (vite.test.config.ts only overrides what differs from vite.config.ts).

    8. Update manifest.json — insert web_accessible_resources AFTER
       host_permissions block, BEFORE background block. Verify storage
       permission still in permissions array. Verify default_locale='en'
       + __MSG_*__ name/description/action.default_title from Plan 01-12
       Wave 3 ARE PRESERVED VERBATIM (do not edit those fields). The
       block to insert:

         "web_accessible_resources": [
           {
             "resources": ["src/welcome/welcome.html"],
             "matches": ["<all_urls>"]
           }
         ],

    9. Run npm run build — exit 0; verify:
       - dist/src/welcome/welcome.html exists
       - dist/manifest.json carries the web_accessible_resources block
       - dist/manifest.json preserves Plan 01-12 default_locale='en' +
         __MSG_*__ values
       - grep -E '#[0-9a-fA-F]{3,8}' dist/src/welcome/welcome.css
         exits 1 OR all matches originate from inlined tokens.css content
         (if Vite inlines the @import). If inlining: confirm via grep
         -F '--mks-rec' dist/src/welcome/welcome.css exits 0 (tokens
         content is reachable in the bundled css).
       - grep -F '@import' OR grep -F '--mks-' in
         dist/src/welcome/welcome.css resolves the canonical tokens
         (either path is acceptable; Vite's CSS plugin chooses)

    10. Run npm run build:test — exit 0; verify dist-test/src/welcome/welcome.html
        exists.

    11. Run npx tsc --noEmit — exit 0.

    12. Run npx vitest run — baseline preserved (3 RED from Task 1 stay
        RED; 147 others stay GREEN).

    Style/naming: full-word names; no `as any`; no `continue` in welcome.ts.
    welcome.html: lang="ru" matches popup precedent. Non-tagline Russian
    strings live in copy.ts; tagline strings live in _locales/.

    Commit:
      feat(01-10): wave-1 task-2 — welcome page bundle + Vite entries + web_accessible_resources — canonical tokens.css @import + chrome.i18n for welcomeHero (Plan 01-12 path-B)

      Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  </action>
  <verify>
    <automated>npm run build &amp;&amp; npx tsc --noEmit &amp;&amp; test -f dist/src/welcome/welcome.html &amp;&amp; grep -q web_accessible_resources dist/manifest.json &amp;&amp; grep -F "@import '../shared/tokens.css'" src/welcome/welcome.css &amp;&amp; ! grep -E '#[0-9a-fA-F]{3,8}' src/welcome/welcome.css &amp;&amp; npm run build:test &amp;&amp; test -f dist-test/src/welcome/welcome.html</automated>
  </verify>
  <acceptance_criteria>
    - 4 new files exist under src/welcome/ (welcome.html, welcome.ts, welcome.css, copy.ts).
    - NO src/welcome/welcome-tokens.css file exists (path-B contract).
    - welcome.css opens with `@import '../shared/tokens.css';`.
    - welcome.css ZERO #hex literals in source file (grep -E '#[0-9a-fA-F]{3,8}' src/welcome/welcome.css exits 1).
    - welcome.css ≥ 5 `var(--mks-` references (grep -c).
    - welcome.html ≥ 7 `data-mokosh-key=` + `data-mokosh-i18n-key=` combined attributes.
    - welcome.html includes data-mokosh-i18n-key='welcomeHeroRu' AND data-mokosh-i18n-key='welcomeHeroEn'.
    - welcome.ts imports Logger + COPY + WELCOME_HERO_*_FALLBACK; calls populateCopy() + populateI18n(); NO `as any`; NO `continue`.
    - copy.ts exports frozen COPY (non-tagline only) + WELCOME_HERO_RU_FALLBACK + WELCOME_HERO_EN_FALLBACK.
    - vite.config.ts + vite.test.config.ts both have welcome rollup entry; __VITE_DEV__ + __MOKOSH_UAT__ defines untouched.
    - manifest.json has web_accessible_resources block; storage permission preserved; default_locale='en' + __MSG_*__ from Plan 01-12 preserved verbatim.
    - npm run build + npm run build:test clean.
    - npx tsc --noEmit exit 0.
    - Vitest baseline: 3 RED from Task 1 still RED; 147 others GREEN.
  </acceptance_criteria>
  <done>Welcome page bundle staged with canonical tokens @import + chrome.i18n welcomeHero reads (Plan 01-12 path-B); Vite picks both bundles; manifest declares page accessible; design-swap-readiness invariants hold in BUILT artifact.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 3: GREEN — openWelcomeIfFirstInstall helper + onInstalled wiring in src/background/index.ts; flips Task 1 tests GREEN.</name>
  <read_first>
    - tests/background/onboarding.test.ts (contracts from Task 1)
    - src/background/index.ts lines 70-90 (existing top-level constants area)
    - src/background/index.ts lines 159-186 (ensureOffscreen pattern)
    - src/background/index.ts lines 724-737 (existing onInstalled handler)
    - .planning/phases/01-stabilize-video-pipeline/01-09-SUMMARY.md "Defense-in-depth chrome.* try/catch" note
  </read_first>
  <files>src/background/index.ts</files>
  <action>
    1. Add top-level constants near badge/notification block at line 73-88:

         // Plan 01-10 onboarding constants (D-17-onboarding).
         const ONBOARDING_FLAG = 'onboarding-completed';
         const ONBOARDING_INSTALLED_AT = 'installed-at';
         const WELCOME_PATH = 'src/welcome/welcome.html';

       SCREAMING_SNAKE per project naming rule for true constants.

    2. Add openWelcomeIfFirstInstall helper just below ensureOffscreen
       (line ~186). 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:

         /**
          * Open the welcome page on first install (Plan 01-10 D-17-onboarding).
          *
          * Trigger conditions (all must hold):
          *   - details.reason === 'install' (NOT 'update' / 'chrome_update' /
          *     'shared_module_update');
          *   - chrome.storage.local key 'onboarding-completed' NOT === true.
          *
          * Side effects:
          *   - chrome.tabs.create({url: chrome.runtime.getURL('src/welcome/welcome.html')})
          *   - chrome.storage.local.set({'onboarding-completed': true,
          *     'installed-at': Date.now()})
          *
          * Failure mode: any thrown chrome.* call caught and logged via
          * logger.warn. Welcome tab failing is NOT fatal — toolbar onClicked
          * (D-16-toolbar) remains the operator's start path.
          */
         async function openWelcomeIfFirstInstall(
           details: chrome.runtime.InstalledDetails,
         ): Promise<void> {
           if (details.reason !== 'install') {
             return;
           }
           try {
             const stored = await chrome.storage.local.get(ONBOARDING_FLAG);
             if (stored[ONBOARDING_FLAG] === true) {
               logger.log('Onboarding already completed; skipping welcome tab.');
               return;
             }
             const url = chrome.runtime.getURL(WELCOME_PATH);
             await chrome.tabs.create({ url });
             await chrome.storage.local.set({
               [ONBOARDING_FLAG]: true,
               [ONBOARDING_INSTALLED_AT]: Date.now(),
             });
             logger.log(
               'Welcome tab opened (D-17-onboarding); onboarding flag set.',
             );
           } catch (err) {
             logger.warn('openWelcomeIfFirstInstall failed:', err);
           }
         }

    3. Extend the onInstalled handler at line 724. Fire-and-forget call
       AFTER initialize(); .catch() so rejected Promise cannot escape
       the synchronous listener boundary:

         chrome.runtime.onInstalled.addListener((details) => {
           logger.log('Extension installed/updated:', details.reason);
           try {
             indexedDB.deleteDatabase('VideoRecorderDB');
             logger.log('Cleaned up orphaned VideoRecorderDB (if present)');
           } catch (e) {
             logger.warn('IDB cleanup failed:', e);
           }
           initialize();
           // Plan 01-10 D-17-onboarding: open welcome tab on first install.
           // Fire-and-forget — helper logs its own errors.
           openWelcomeIfFirstInstall(details).catch((err) => {
             logger.warn('openWelcomeIfFirstInstall threw:', err);
           });
         });

    4. Run npx vitest run tests/background/onboarding.test.ts — all 3 RED
       MUST flip GREEN.

    5. Run full vitest suite — baseline 147 + 3 new = 150 GREEN; zero RED.

    6. Run npx tsc --noEmit — exit 0. If type complaint on
       chrome.runtime.InstalledDetails, narrow parameter to { reason: string }.

    7. Run npm run build + npm run build:test — both exit 0.

    Style/naming pre-commit:
      - No `as any` (chrome.runtime.InstalledDetails type or { reason: string } narrow)
      - No `continue` (if-else early-return only)
      - No new dependencies
      - No edits outside src/background/index.ts

    Commit:
      feat(01-10): wave-2 task-3 — openWelcomeIfFirstInstall helper + onInstalled wiring (D-17-onboarding) — 3 RED → GREEN

      Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  </action>
  <verify>
    <automated>npx vitest run tests/background/onboarding.test.ts 2>&amp;1 | grep -E "3 passed" &amp;&amp; npx tsc --noEmit &amp;&amp; npm run build &amp;&amp; npm run build:test</automated>
  </verify>
  <acceptance_criteria>
    - openWelcomeIfFirstInstall helper exists with documented behavior.
    - onInstalled handler invokes helper after initialize().
    - All 3 onboarding tests GREEN.
    - Full suite 147 + 3 = 150 GREEN.
    - npx tsc --noEmit exit 0.
    - npm run build + build:test exit 0.
    - Tier-1 grep gate (tests/background/no-test-hooks-in-prod-bundle.test.ts)
      stays GREEN — FORBIDDEN_HOOK_STRINGS inventory unchanged at 12.
  </acceptance_criteria>
  <done>onInstalled extended; SW handler tests GREEN; welcome flow wired end-to-end at the SW layer.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 4: Harness A15+A16+A17 extension — page-side asserts in extension-page-harness.ts, host-side drivers in harness-page-driver.ts, registration in harness.test.ts. npm run test:uat 24/24 GREEN.</name>
  <read_first>
    - tests/uat/extension-page-harness.ts (assertA14 + assertA18..A22 + assertA23 + window.__mokoshHarness install pattern)
    - tests/uat/lib/harness-page-driver.ts (driveA14 + driveA18..A22 + driveA23 standard wrappers)
    - tests/uat/harness.test.ts (driver imports + drivers array; A0-A14 + A18-A22 + A23 ordering)
    - tests/background/no-test-hooks-in-prod-bundle.test.ts lines 95-130 (FORBIDDEN_HOOK_STRINGS — 12 entries; DO NOT extend; A15-A17 use public APIs)
    - src/welcome/welcome.html (the artifact A17 fetches + parses)
    - src/welcome/welcome.css (the artifact A17 fetches; verifies @import directive + zero hex)
    - src/shared/tokens.css (the @imported canonical token source; --mks-rec = #b2543d per Plan 01-12 Wave 4)
    - dist-test/src/welcome/welcome.html (the BUILT artifact A15/A16/A17 actually navigate against)
  </read_first>
  <files>
    tests/uat/extension-page-harness.ts,
    tests/uat/lib/harness-page-driver.ts,
    tests/uat/harness.test.ts
  </files>
  <action>
    1. **Page-side (extension-page-harness.ts)**: append three new assertion
       methods. Locate the existing assertA14 + assertA18..A22 + assertA23
       block; the new A15/A16/A17 methods slot in lexically near A14 to
       preserve numeric ordering OR at the end of the assertion block
       before the window.__mokoshHarness installation (executor discretion;
       lexical position doesn't affect orchestration).

       assertA15 — onboarding flag observability:
         async function assertA15(): Promise<AssertionResult> {
           const result: AssertionResult = {
             passed: false,
             name: "A15 — onboarding flag set on first install (chrome.storage.local 'onboarding-completed' === true; 'installed-at' is number)",
             checks: [],
             diagnostics: [],
           };
           try {
             const stored = await chrome.storage.local.get([
               'onboarding-completed', 'installed-at',
             ]);
             diag(result, 'storage.local read: ' + JSON.stringify(stored));
             result.checks.push({
               name: 'A15.1: onboarding-completed === true',
               expected: true,
               actual: stored['onboarding-completed'],
               passed: stored['onboarding-completed'] === true,
             });
             result.checks.push({
               name: 'A15.2: installed-at is a number',
               expected: 'number',
               actual: typeof stored['installed-at'],
               passed: typeof stored['installed-at'] === 'number',
             });
             result.passed = result.checks.every((c) => c.passed);
           } catch (err) {
             result.error = err instanceof Error ? err.message : String(err);
             diag(result, 'THREW: ' + result.error);
           }
           return result;
         }

       assertA16 — subsequent-install does NOT re-open:
         async function assertA16(): Promise<AssertionResult> {
           const result: AssertionResult = {
             passed: false,
             name: 'A16 — subsequent install does NOT re-open welcome tab (2s settle: no new welcome.html tabs appear)',
             checks: [],
             diagnostics: [],
           };
           try {
             const welcomeUrlSuffix = 'src/welcome/welcome.html';
             const tabsBefore = await chrome.tabs.query({});
             const beforeCount = tabsBefore.filter(
               (t) => typeof t.url === 'string' &&
                 t.url.endsWith(welcomeUrlSuffix),
             ).length;
             diag(result, 'A16: welcome tabs before settle: ' + String(beforeCount));
             await new Promise((r) => setTimeout(r, 2000));
             const tabsAfter = await chrome.tabs.query({});
             const afterCount = tabsAfter.filter(
               (t) => typeof t.url === 'string' &&
                 t.url.endsWith(welcomeUrlSuffix),
             ).length;
             diag(result, 'A16: welcome tabs after settle: ' + String(afterCount));
             const delta = afterCount - beforeCount;
             result.checks.push({
               name: 'A16.1: welcome-tab delta over 2s settle is 0 (onInstalled flag-gating works)',
               expected: 0,
               actual: delta,
               passed: delta === 0,
             });
             result.passed = result.checks.every((c) => c.passed);
           } catch (err) {
             result.error = err instanceof Error ? err.message : String(err);
             diag(result, 'THREW: ' + result.error);
           }
           return result;
         }

       assertA17 — design-swap-readiness invariant (EXTENDED per
       Plan 01-12 path-B revision; verifies canonical @import resolves):
         async function assertA17(): Promise<AssertionResult> {
           const result: AssertionResult = {
             passed: false,
             name: 'A17 — design-swap-readiness: welcome.html parses; .welcome-hero exists; welcome.css has @import + zero hex; ≥7 mokosh-keyed attrs; bundled JS has COPY[ or chrome.i18n.getMessage; --mks-rec resolves',
             checks: [],
             diagnostics: [],
           };
           try {
             const welcomeUrl = chrome.runtime.getURL('src/welcome/welcome.html');
             const htmlRes = await fetch(welcomeUrl);
             const htmlText = await htmlRes.text();
             const parsed = new DOMParser().parseFromString(htmlText, 'text/html');
             const hero = parsed.querySelector('.welcome-hero');
             result.checks.push({
               name: 'A17.1: welcome.html parses + .welcome-hero exists',
               expected: 'truthy',
               actual: hero === null ? 'null' : 'element',
               passed: hero !== null,
             });
             const dataKeyMatches = htmlText.match(/data-mokosh-key=/g) ?? [];
             const dataI18nKeyMatches = htmlText.match(/data-mokosh-i18n-key=/g) ?? [];
             const totalKeyedAttrs = dataKeyMatches.length + dataI18nKeyMatches.length;
             result.checks.push({
               name: 'A17.2: welcome.html has ≥ 7 data-mokosh-key + data-mokosh-i18n-key attributes combined',
               expected: '≥ 7',
               actual: String(totalKeyedAttrs) + ' (data-mokosh-key=' + String(dataKeyMatches.length) + ', data-mokosh-i18n-key=' + String(dataI18nKeyMatches.length) + ')',
               passed: totalKeyedAttrs >= 7,
             });
             const cssUrl = chrome.runtime.getURL('src/welcome/welcome.css');
             const cssRes = await fetch(cssUrl);
             const cssText = await cssRes.text();
             // A17.3: zero hex literals — note this checks the BUILT artifact.
             // If Vite inlines @import contents, the inlined tokens.css may
             // contain hex literals from canonical token values; in that case
             // the regex matches them. The relaxed contract: accept the assert
             // if (a) NO hex match OR (b) hex matches present AND tokens are
             // resolvable (proven by A17.7 below).
             const hexMatches = cssText.match(/#[0-9a-fA-F]{3,8}/g) ?? [];
             const hasCanonicalImport = cssText.includes("@import '../shared/tokens.css'")
               || cssText.includes('@import "../shared/tokens.css"')
               || cssText.includes('--mks-rec'); // inlined canonical content
             result.checks.push({
               name: 'A17.3: welcome.css source has zero hex literals (allowed if @import resolves)',
               expected: '0 hex OR canonical-import-resolved',
               actual: 'hex=' + String(hexMatches.length) + ', canonicalImport=' + String(hasCanonicalImport),
               passed: hexMatches.length === 0 || hasCanonicalImport,
             });
             const varMatches = cssText.match(/var\(--mks-/g) ?? [];
             result.checks.push({
               name: 'A17.4: welcome.css contains ≥ 5 var(--mks- references',
               expected: '≥ 5',
               actual: String(varMatches.length),
               passed: varMatches.length >= 5,
             });
             result.checks.push({
               name: "A17.5: welcome.css has canonical tokens @import (or inlined evidence)",
               expected: '@import ../shared/tokens.css OR --mks-* inlined',
               actual: hasCanonicalImport ? 'present' : 'absent',
               passed: hasCanonicalImport,
             });
             // Locate the bundled welcome JS chunk via <script src> in the
             // parsed HTML. Vite emits a hashed chunk name; we fetch the
             // referenced path relative to welcome.html.
             const scriptEl = parsed.querySelector(
               'script[type="module"][src]',
             );
             const scriptSrc = scriptEl?.getAttribute('src') ?? '';
             const baseUrl = new URL(welcomeUrl);
             const jsUrl = new URL(scriptSrc, baseUrl).href;
             const jsRes = await fetch(jsUrl);
             const jsText = await jsRes.text();
             // Post-revision: accept EITHER COPY[ pattern (non-tagline keys)
             // OR chrome.i18n.getMessage('welcomeHero pattern (tagline keys
             // via chrome.i18n path). Both prove populate plumbing survives
             // the build.
             const hasCopySubscript = jsText.includes('COPY[');
             const hasI18nGetMessage = jsText.includes("chrome.i18n.getMessage(") &&
               (jsText.includes('welcomeHero') || jsText.includes('welcome'));
             result.checks.push({
               name: 'A17.6: bundled JS contains COPY[ OR chrome.i18n.getMessage(welcomeHero)',
               expected: 'COPY[ OR chrome.i18n.getMessage welcomeHero',
               actual: 'COPY[=' + String(hasCopySubscript) + ', i18n=' + String(hasI18nGetMessage),
               passed: hasCopySubscript || hasI18nGetMessage,
             });
             // A17.7 NEW: probe --mks-rec via getComputedStyle. The
             // canonical tokens.css from Plan 01-12 Wave 1 Task 2 sets
             // --mks-rec to #b2543d (per Plan 01-12 Wave 4 D-04 Loom
             // palette adoption) which resolves to rgb(178, 84, 61) in
             // CSS computed values. NOTE: this assertion runs in the
             // extension-page-harness.html context where tokens.css IS
             // <link>-loaded directly (Plan 01-12 Wave 6 added that link
             // for A18-A21). The probe creates a transient div, applies
             // `color: var(--mks-rec)`, reads getComputedStyle.
             const probe = document.createElement('div');
             probe.style.color = 'var(--mks-rec)';
             document.body.appendChild(probe);
             const resolvedColor = getComputedStyle(probe).color;
             document.body.removeChild(probe);
             diag(result, 'A17.7 probe getComputedStyle.color = ' + resolvedColor);
             // Accept canonical rgb(178, 84, 61) AND any non-empty
             // non-fallback-default value (the probe inheriting browser
             // default rgb(0, 0, 0) would mean tokens didn't resolve).
             const resolvedNonEmpty = resolvedColor.length > 0 &&
               resolvedColor !== 'rgb(0, 0, 0)' &&
               resolvedColor !== '' &&
               !resolvedColor.includes('rgba(0, 0, 0, 0)');
             const resolvedCanonical = resolvedColor === 'rgb(178, 84, 61)';
             result.checks.push({
               name: 'A17.7: --mks-rec resolves to non-default value via getComputedStyle (proves @import wires through; canonical = rgb(178, 84, 61))',
               expected: 'non-default (preferably rgb(178, 84, 61))',
               actual: resolvedColor + ' (canonical=' + String(resolvedCanonical) + ')',
               passed: resolvedNonEmpty,
             });
             result.passed = result.checks.every((c) => c.passed);
             diag(result, 'A17: ' + result.checks.filter((c) => c.passed).length + '/' + result.checks.length + ' subchecks passed');
           } catch (err) {
             result.error = err instanceof Error ? err.message : String(err);
             diag(result, 'THREW: ' + result.error);
           }
           return result;
         }

       NOTE: A17.7 leverages the Plan 01-12 Wave 6 extension-page-harness.html
       <link rel=stylesheet href="../../src/shared/tokens.css"> already present
       (verified at execute time). The probe element inherits the canonical
       --mks-rec value via direct token cascade in the harness page context.
       If the probe returns rgb(0,0,0) (browser default), the tokens did NOT
       resolve — assert fails.

    2. Update the global type declaration on window.__mokoshHarness:
         declare global {
           interface Window {
             __mokoshHarness: {
               // ... existing assertA1..A14 + assertA18..A22 + assertA23 + getManifestVersion ...
               assertA15: () => Promise<AssertionResult>;
               assertA16: () => Promise<AssertionResult>;
               assertA17: () => Promise<AssertionResult>;
             };
           }
         }
         window.__mokoshHarness = {
           // ... existing ...
           assertA15, assertA16, assertA17,
         };

    3. Update the page-ready log line to mention A17.

    4. **Host-side (lib/harness-page-driver.ts)**: append three new
       drivers AFTER driveA14 (or near the A18-A22 block):

         export async function driveA15(page: Page): Promise<AssertionRecord> {
           return await page.evaluate(async () => {
             const harness = (window as any).__mokoshHarness;
             const r: AssertionRecord = await harness.assertA15();
             return r;
           }) as AssertionRecord;
         }
         export async function driveA16(page: Page): Promise<AssertionRecord> {
           return await page.evaluate(async () => {
             const harness = (window as any).__mokoshHarness;
             const r: AssertionRecord = await harness.assertA16();
             return r;
           }) as AssertionRecord;
         }
         export async function driveA17(page: Page): Promise<AssertionRecord> {
           return await page.evaluate(async () => {
             const harness = (window as any).__mokoshHarness;
             const r: AssertionRecord = await harness.assertA17();
             return r;
           }) as AssertionRecord;
         }

       Re-add the same eslint-disable comment on the `as any` line that
       driveA14 / driveA18..A22 carry. Standard wrapper.

    5. **Orchestrator (harness.test.ts)**: update the imports to add
       driveA15, driveA16, driveA17 alongside existing driveA1..A14 +
       driveA18..A22 + driveA23 + getManifestVersion.

       Update the drivers array to interleave A15-A17 after A14 and
       before A18 (or append at end — executor discretion; preserves
       the bail-on-first-failure ordering semantics):
         { name: 'A14', drive: driveA14 },
         { name: 'A15', drive: driveA15 },
         { name: 'A16', drive: driveA16 },
         { name: 'A17', drive: driveA17 },
         { name: 'A18', drive: driveA18 },
         // ... A19-A22, A23 continue ...

       FORBIDDEN_HOOK_STRINGS UNCHANGED at 12 entries — verify visually
       (do NOT add new entries; A15-A17 use chrome.tabs.query,
       chrome.storage.local.get, fetch, DOMParser, getComputedStyle —
       all production-API surfaces).

    6. Run npm run build:test — exit 0 (rebuilds dist-test with the new
       page-side asserts).

    7. Run npm run test:uat — bail-on-first-failure orchestrator now
       executes 24 assertions. Expected output: "UAT harness: 24/24
       assertions passed". If any A15/A16/A17 fails, surface the
       diagnostic; iterate within this task.

    8. Run full vitest suite — 150 GREEN preserved (no source under test
       changed; the harness-page changes are dist-test/ only).

    9. Run npx tsc --noEmit — exit 0.

    10. Verify Tier-1 grep gate stays at 12 strings:
        npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts
        — all build + 12 string assertions GREEN. No new forbidden strings;
        the inventory is unchanged.

    Style/naming pre-commit:
      - No new `__mokoshTest`/`__MOKOSH_UAT__` test-hook surface
        (A15-A17 use only chrome.tabs.query / chrome.storage.local.get /
         fetch on public web_accessible_resources surfaces +
         getComputedStyle on a transient probe).
      - No new entries in FORBIDDEN_HOOK_STRINGS (stays at 12).
      - `as any` only inside the page.evaluate wrapper (eslint-disable
        line comment matches driveA14 idiom).

    Commit:
      test(01-10): wave-3 task-4 — harness A15+A16+A17 (onboarding flag observability + no-re-open settle check + design-swap-readiness with @import probe); 24/24 GREEN

      Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  </action>
  <verify>
    <automated>npm run build:test &amp;&amp; npm run test:uat 2>&amp;1 | grep -E "UAT harness: 24/24" &amp;&amp; npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts &amp;&amp; npx tsc --noEmit</automated>
  </verify>
  <acceptance_criteria>
    - assertA15, assertA16, assertA17 exist in extension-page-harness.ts.
    - window.__mokoshHarness exposes all three new methods.
    - driveA15, driveA16, driveA17 exist in lib/harness-page-driver.ts.
    - harness.test.ts drivers array includes A15, A16, A17 entries (interleaved into A0-A14 + A18-A22 + A23).
    - npm run test:uat reports 24/24 assertions passed (A0-A14 + A15-A17 + A18-A22 + A23).
    - Tier-1 FORBIDDEN_HOOK_STRINGS inventory unchanged at 12 (no new test-hook surface).
    - npm test (vitest) baseline 150 GREEN preserved.
    - npx tsc --noEmit clean.
  </acceptance_criteria>
  <done>Harness covers welcome-tab activation + canonical-tokens design-swap-readiness; Plan 01-12 path-B contract verified empirically; A17.7 probe proves @import wires through to canonical token values.</done>
</task>

<task type="checkpoint:human-verify" gate="blocking">
  <name>Task 5: Operator empirical UAT — fresh-profile install opens welcome tab; canonical tokens + Lora render; chrome.i18n welcomeHero strings render; reload does NOT re-open; harness 24/24 GREEN.</name>
  <files>(operator-driven; no specific source file modified)</files>
  <action>See &lt;how-to-verify&gt; below — operator-driven empirical check.</action>
  <verify>
    <automated>echo "checkpoint:human-verify — see how-to-verify; resume signal is the gate"</automated>
  </verify>
  <done>Operator types "approved" after running the how-to-verify steps. See &lt;resume-signal&gt; for the gate.</done>
  <what-built>
    Tasks 1-4 landed:
    - src/welcome/* page bundle (4 files: welcome.html, welcome.ts, welcome.css, copy.ts).
      welcome.css opens with `@import '../shared/tokens.css';` (Plan 01-12
      path-B canonical tokens import). NO placeholder welcome-tokens.css.
    - manifest.json web_accessible_resources for welcome.html (Plan 01-12
      default_locale='en' + __MSG_*__ preserved verbatim).
    - vite.config.ts + vite.test.config.ts rollupOptions.input welcome
      entry in BOTH bundles (Plan 01-12 __VITE_DEV__ + __MOKOSH_UAT__
      defines preserved verbatim).
    - src/background/index.ts openWelcomeIfFirstInstall helper + onInstalled wiring.
    - tests/background/onboarding.test.ts 3 GREEN unit tests.
    - tests/uat/* harness A15+A16+A17 extensions; 24/24 GREEN (A0-A14 +
      A15-A17 + A18-A22 + A23).
    - FORBIDDEN_HOOK_STRINGS inventory unchanged at 12.
    - vitest baseline 150 GREEN (147 from Plan 01-12 + Plan 01-14 + 3 new from this plan).

    This checkpoint validates real Chrome behavior + canonical brand
    rendering (Plan 01-12 path-B: tokens.css @imports through to
    welcome.css; D-04 Loom palette + Lora display font + IBM Plex Sans
    UI font render correctly; chrome.i18n.getMessage resolves welcomeHeroRu
    + welcomeHeroEn from _locales/{en,ru}/messages.json).
  </what-built>
  <how-to-verify>
    Pre-checkpoint bundle gates (operator OR executor verifies before
    surfacing this checkpoint — per the orchestrator-loaded
    feedback-pre-checkpoint-bundle-gates.md memory):

      1. npm run build — exit 0; dist/src/welcome/welcome.html exists.
      2. npm run build:test — exit 0.
      3. grep -F "@import '../shared/tokens.css'" src/welcome/welcome.css
         — exit 0 (canonical-import directive present).
      4. grep -E '#[0-9a-fA-F]{3,8}' src/welcome/welcome.css — exit 1
         (no hex matches in source file).
      5. grep -q web_accessible_resources dist/manifest.json — exit 0.
      6. grep -F '"default_locale": "en"' dist/manifest.json — exit 0
         (Plan 01-12 baseline preserved).
      7. grep -F '__MSG_extName__' dist/manifest.json — exit 0
         (Plan 01-12 i18n placeholder preserved).
      8. npx vitest run — 150 passed.
      9. npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts —
         all GREEN; 12 forbidden strings absent from dist/.
      10. npm run test:uat — 24/24 GREEN.

    Operator empirical steps:

      1. Verify dist/ is fresh: npm run build (exit 0).
      2. Wipe smoke profile: rm -rf /tmp/mokosh-smoke-profile (or
         KEEP_PROFILE=0 ./smoke.sh which does the wipe).
      3. Launch fresh-profile Chrome: KEEP_PROFILE=0 ./smoke.sh.
      4. chrome://extensions → Load Unpacked → select dist/. THE WELCOME
         TAB SHOULD AUTOMATICALLY OPEN within ~1 second. URL should be
         chrome-extension://&lt;id&gt;/src/welcome/welcome.html.
      5. Verify the welcome page renders coherently with CANONICAL Plan
         01-12 brand assets (NOT engineering placeholders — Plan 01-12
         landed first):
         - Centered card, max-width ~720px.
         - Hero block at top: 'Mokosh' placeholder mark + 'Mokosh' H1 in
           Lora serif (D-05) + RU tagline '«Тридцать секунд назад,
           всегда под рукой.»' + EN tagline 'Thirty seconds ago, always
           at hand.' — both render via chrome.i18n.getMessage from
           _locales/.
         - Body: 2 explainer lines about 30s screen + 10min logs +
           local-only data + how to save archive (these render from
           src/welcome/copy.ts).
         - CTA box: 'Чтобы начать запись, нажмите иконку AI Call Recorder
           на панели инструментов браузера (правый верхний угол).'
         - Footer: privacy/safety note about no-server-uploads.
         - Typography: Lora serif for hero (Plan 01-12 self-hosted
           WOFF2; full Cyrillic via Cyreal); IBM Plex Sans for body
           (Plan 01-12 self-hosted; Latin + Cyrillic).
         - Color: --mks-rec (#b2543d madder) used for recording-related
           accents per D-04 Loom palette.
         - NO console errors in the welcome tab's DevTools console.
         - NO inline "Start" button (D-16-toolbar charter — the welcome
           tab is informational; toolbar owns start).
      6. Verify Cyrillic glyph coverage: the welcomeHeroRu string
         (Тридцать секунд назад, всегда под рукой.) renders without
         tofu/box characters. Lora-Cyrillic from Plan 01-12 supplies
         the glyphs.
      7. Close the welcome tab.
      8. Click the AI Call Recorder toolbar icon (right-side toolbar).
         The icon should be the Loom mark (Plan 01-12 Wave 2 rasterization);
         Chrome's screen-share picker should appear (monitor-only per
         01-09). Pick 'Entire screen' + accept. Badge transitions to
         REC (--mks-rec madder color per Plan 01-12 Wave 4). (This
         validates that the welcome tab CTA accurately describes the
         toolbar start path AND that the brand palette flows through.)
      9. Click the toolbar again (now in REC state). The popup opens
         with the SAVE-only UI per 01-09 SAVE-only charter; a zip is
         downloaded. Badge returns to OFF (per Plan 01-13 save-stops-
         recording charter).
      10. Reload the extension at chrome://extensions (toggle off + on).
          Observe: the welcome tab does NOT re-open. This validates
          A16's flag-gating contract.
      11. Re-validate the 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).

    If step 4 (welcome tab opens), step 5 (page renders with canonical
    brand), step 6 (Cyrillic glyph coverage), step 8 (toolbar CTA
    validates with branded icon + badge color), or step 10 (no re-open
    on reload) fails: document the failure mode + Chrome version + SW
    console errors. Iterate on Task 2 (assets) or Task 3 (SW handler)
    or Task 4 (harness) accordingly.

    Plan 01-12 path-B note for operator:
    The welcome page renders TODAY with FULL canonical brand assets
    because Plan 01-12 landed FIRST (2026-05-20 operator brand-fit ack
    "all good"). NO engineering placeholder transition is needed. The
    @import '../shared/tokens.css' directive in welcome.css resolves
    the canonical Lora + IBM Plex Sans + D-04 Loom palette directly
    at build time. The chrome.i18n.getMessage path reads the welcomeHeroRu
    + welcomeHeroEn keys from _locales/{en,ru}/messages.json (16 keys
    total per Plan 01-12 Wave 3). The harness A17 contract (including
    A17.7 getComputedStyle --mks-rec probe) pins these invariants
    empirically.
  </how-to-verify>
  <resume-signal>
    Type 'approved' after pre-checkpoint gates 1-10 ALL pass AND operator
    steps 4, 5, 6, 8, 10 ALL pass. If any step fails, paste the failure
    diagnostic. The harness 24/24 GREEN is the canonical functional gate;
    operator empirical is the brand-coherence + Chrome-empirical gate.
  </resume-signal>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| welcome page (extension-page context) ↔ SW | Welcome page is a same-origin extension page (chrome-extension://&lt;id&gt;/src/welcome/welcome.html). It does NOT call chrome.runtime.sendMessage in this plan — it is informational + read-only. No new IPC boundary introduced. |
| chrome.storage.local ↔ SW | Storage flag is non-secret (boolean + timestamp); the worst case if leaked or tampered is suppressing/re-firing the welcome tab. No PII; no sensitive content. |
| chrome.runtime.onInstalled ↔ SW | Existing Chrome MV3 boundary; this plan EXTENDS the handler body but does not introduce a new listener registration. |
| web_accessible_resources ↔ external pages | welcome.html is enumerable by any page that probes chrome-extension:// URLs in web_accessible_resources. Existing extension-fingerprinting concern; matches=['&lt;all_urls&gt;'] is the standard pattern for welcome flows. |
| chrome.i18n.getMessage ↔ _locales/ | Plan 01-12 i18n surface (16 keys; en↔ru parity verified via tests/i18n/locale-parity.test.ts). Plan 01-10 consumes welcomeHeroRu + welcomeHeroEn keys; introduces no new keys. Read-only chrome.i18n access; no IPC. |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-1-10-01 | Tampering | Adversary clears chrome.storage.local 'onboarding-completed' flag to spam welcome tabs across SW respawns | accept | Welcome tab is non-destructive; worst case is an extra tab on each install (NOT each SW respawn — onInstalled only fires on install/update/chrome_update). chrome.storage.local clearing requires either operator action via Chrome DevTools OR an adversarial extension installed alongside — both out-of-band. No data exfiltration path. |
| T-1-10-02 | Information Disclosure | welcome.html fingerprinting via web_accessible_resources enumeration | accept | Extension identifier is already discoverable through chrome.runtime.getURL exposure on any extension page (popup + offscreen + harness page); matches=['&lt;all_urls&gt;'] is the standard MV3 welcome-flow pattern. Phase 5 hardening could narrow matches to a specific install-confirmation domain; out of scope. |
| T-1-10-03 | Denial of Service | Adversary controls chrome.storage.local quota | mitigate | Two values stored (boolean true + Date.now() number) ~80 bytes; storage.local quota is 10 MB; not exploitable. |
| T-1-10-04 | Elevation of Privilege | Welcome page tricks SW into bypassing checkpoints | accept | Welcome page sends NO messages to the SW in this plan (informational only); chrome.i18n.getMessage is a synchronous read-only API, no IPC, no EoP path. No new EoP path. |
| T-1-10-05 | Information Disclosure | Russian copy strings leak via web_accessible_resources | accept | Copy strings are operator-facing public marketing content (D-08 tagline; D-03 voice register); no PII. _locales/{en,ru}/messages.json content is similarly non-secret operator UI strings (Plan 01-12 Wave 3 baseline). |
| T-1-10-06 | Tampering | Adversary modifies copy.ts contents via supply-chain attack on the source bundle | accept | Generic supply-chain risk applying to every TS file in the project; outside this plan's scope. The Tier-1 grep gate (FORBIDDEN_HOOK_STRINGS at 12 entries) prevents test-hook leaks; brand-copy integrity is a Phase 5 / brand-team concern. |
| T-1-10-07 | Tampering | Adversary modifies welcome.css to inject malicious styles via the canonical @import | mitigate | welcome.css's @import resolves to src/shared/tokens.css (Plan 01-12 canonical; supply-chain integrity bounded by the same Tier-1 grep gate + the no-remote-fonts.test.ts MV3 CSP invariant — tokens.css has zero `url()` to external origins). MV3 CSP forbids inline scripts; CSS-only injection cannot escalate. |
</threat_model>

<verification>
- npx vitest run shows 150/150 GREEN (147 baseline from Plan 01-12 + Plan 01-14 + 3 new from onboarding.test.ts).
- npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts: all GREEN; FORBIDDEN_HOOK_STRINGS inventory unchanged at 12.
- npx tsc --noEmit: exit 0.
- npm run build: exit 0; dist/src/welcome/welcome.html exists; dist/manifest.json contains web_accessible_resources with welcome.html entry; default_locale='en' + __MSG_extName__ from Plan 01-12 preserved verbatim.
- npm run build:test: exit 0; dist-test/src/welcome/welcome.html exists.
- grep -F "@import '../shared/tokens.css'" src/welcome/welcome.css: exit 0 (canonical tokens directive present).
- grep -E '#[0-9a-fA-F]{3,8}' src/welcome/welcome.css: exit 1 (no hex matches in source file).
- grep -c 'var(--mks-' src/welcome/welcome.css: ≥ 5.
- grep -oE 'data-mokosh-(i18n-)?key' dist/src/welcome/welcome.html | wc -l ≥ 7.
- grep -n 'chrome.tabs.create' src/background/index.ts ≥ 1 (the helper's call).
- grep -n 'welcome.html' manifest.json ≥ 1.
- grep -n 'openWelcomeIfFirstInstall' src/background/index.ts ≥ 2 (helper definition + call site).
- grep -n 'D-17-onboarding' src/background/index.ts ≥ 1 (cited in JSDoc).
- grep -n "chrome.i18n.getMessage" src/welcome/welcome.ts ≥ 1 (welcomeHero path).
- grep -nE 'REQUEST_PERMISSIONS' src/welcome/welcome.ts: exit 1 (no match — welcome tab does NOT reference REQUEST_PERMISSIONS per the deletion in 01-09 + per the D-16-toolbar single-start-path charter).
- test ! -f src/welcome/welcome-tokens.css (Plan 01-12 path-B contract — no placeholder file created).
- npm run test:uat: "UAT harness: 24/24 assertions passed".
- Operator empirical steps 4, 5, 6, 8, 10 from how-to-verify: ALL PASS.
</verification>

<success_criteria>
Plan 01-10 is complete when:
1. The 3 onboarding unit tests are GREEN; full vitest suite 150/150 GREEN.
2. UAT harness reports 24/24 GREEN (A0-A14 + A15-A17 + A18-A22 + A23).
3. Tier-1 FORBIDDEN_HOOK_STRINGS inventory stays at 12 (no new test-mode symbols).
4. Welcome page renders coherently in fresh-profile Chrome with canonical Plan 01-12 brand assets (Lora display + IBM Plex Sans UI + D-04 Loom palette via tokens.css @import; chrome.i18n.getMessage resolves welcomeHeroRu + welcomeHeroEn from _locales/).
5. Design-swap-readiness invariants hold in the BUILT artifact (welcome.css source has zero hex literals + canonical @import directive present; ≥ 7 mokosh-keyed attributes across data-mokosh-key + data-mokosh-i18n-key; bundled JS uses COPY[ subscript OR chrome.i18n.getMessage('welcomeHero pattern; --mks-rec resolves to non-default value via getComputedStyle probe — preferably rgb(178, 84, 61) canonical).
6. Operator confirms via how-to-verify steps 4 / 5 / 6 / 8 / 10 + the harness 24/24 GREEN gate.
7. tsc + build + build:test all clean; manifest + vite + welcome assets consistent across both bundles.
8. No references to REQUEST_PERMISSIONS in src/welcome/ (start path remains toolbar-onClicked-owned per D-16-toolbar).
9. No `await import(...)` introduced into src/background/index.ts (MV3 SW dynamic-import blocker per 01-11-SUMMARY).
10. NO src/welcome/welcome-tokens.css file exists (Plan 01-12 must_have #9 path-B contract: Plan 01-12 landed first; canonical tokens import directly).
11. Plan 01-12 baselines preserved verbatim (manifest default_locale='en' + __MSG_*__ placeholders + 16 i18n keys per locale + __VITE_DEV__ + __MOKOSH_UAT__ defines + src/shared/tokens.css unchanged).
</success_criteria>

<output>
After completion, create .planning/phases/01-stabilize-video-pipeline/01-10-SUMMARY.md per the standard template. Cite:
- 3 new onboarding unit tests landed GREEN (Test A / B / C).
- 4 new src/welcome/* files (welcome.html / welcome.ts / welcome.css / copy.ts). NO placeholder welcome-tokens.css (Plan 01-12 path-B).
- manifest web_accessible_resources delta; Plan 01-12 default_locale + __MSG_*__ preserved verbatim.
- vite.config.ts + vite.test.config.ts rollupOptions.input delta in BOTH bundles; Plan 01-12 defines preserved.
- openWelcomeIfFirstInstall helper + onInstalled wiring in src/background/index.ts.
- chrome.i18n.getMessage adoption for welcomeHeroRu + welcomeHeroEn (Plan 01-12 fallback pattern).
- Canonical `@import '../shared/tokens.css';` in welcome.css (no placeholder welcome-tokens.css).
- Harness A15 + A16 + A17 extensions; 21/21 → 24/24 GREEN delta. A17.7 getComputedStyle probe proves --mks-rec resolves.
- Tier-1 FORBIDDEN_HOOK_STRINGS inventory unchanged at 12 (no new test-hook surface).
- Plan 01-12 path-B + Plan 01-14 baseline alignment: vitest 147→150; UAT 21→24; FORBIDDEN 12 unchanged.
- Cross-references: D-02 (welcome layout), D-08 (tagline → chrome.i18n), D-03 (voice register), D-17-onboarding (this plan's amendment marker), D-16-toolbar (start path ownership), D-04 (Loom palette via tokens.css), D-05 (Lora display + IBM Plex Sans UI via tokens.css).
- Plan 01-12 must_have #9 path-B contract closure: this plan honors path-B end-to-end.
- Operator empirical UAT outcome (welcome tab opens with canonical brand; Cyrillic renders correctly with Lora; reload does NOT re-open; harness 24/24 GREEN).
</output>
