Files
Mark 3fe018beb9 fix(01-12): revise plan baselines per Plan 01-14 landing (vitest 98→100, UAT 15→16, FORBIDDEN_HOOK_STRINGS 10→12)
Surgical amendment — Plan 01-12 is unexecuted; safe for in-place revision.
Plan 01-14 landed (b46712352541459792c0f) since 01-12 was authored,
adding monitorTypeSurfaces:'include' + UAT A23 + 2 FORBIDDEN_HOOK_STRINGS
entries. Plan 01-12 baseline arithmetic updated to reflect post-01-14
floors and depends_on chain extended.

Changes:
- frontmatter depends_on: append 01-14 (canonical sequential-dependency declaration)
- must_haves truth #11: 10 strings → 12 strings post-Plan-01-14, with provenance note
- must_haves truth #13: vitest 98→100→106; UAT 15→16→21 (or 19→24 with 01-10);
  A23 added to no-regression list alongside A0-A14 + (A15-A17 if 01-10)
- Embedded FORBIDDEN_HOOK_STRINGS code block: 10 → 12 entries (lastGetDisplayMediaConstraints,
  get-last-getDisplayMedia-constraints) with attribution comment
- Wave 6 task narrative + verify + done: 20/20 → 21/21; 23/23 → 24/24
- Wave 7 closure narrative + one-liner + checkpoint copy: same arithmetic update
- Threat model T-01-12-08: inheritance updated to include T-1-14-*; grep-gate floor 10→12
- Verify-block harness orchestrator grep gate: strengthened with `grep -v '^#'` filter
  to prevent self-invalidating count from comment lines (per planner.md gate hygiene)
- Success criteria items 3, 4, 9: vitest 98→100; UAT 20→21; inventory 10→12

Preserved verbatim (per surgeon-not-architect):
- 7-wave structure (Wave 0 fonts/tokens/icons/i18n RED scaffolds, Wave 1 fonts+tokens.css,
  Wave 2 icons, Wave 3 manifest i18n, Wave 4 popup/welcome adoption, Wave 5 8 i18n strings,
  Wave 6 harness A18-A22, Wave 7 operator brand-fit checkpoint)
- 10 tasks (validated via gsd-sdk verify.plan-structure)
- R2 Lora baked-in decision
- D-01..D-09 decision references
- All files_modified entries
- Threat model entries other than T-01-12-08 inheritance line
- Designer ack references
- Operator checkpoint (Wave 7)
- Must_have truths #1-10 and #12

Validation:
- gsd-sdk frontmatter.validate → valid:true
- gsd-sdk verify.plan-structure → valid:true, errors:0, task_count:10
- grep verification: zero stale references to 98/104/15/15/20/20/23/23/18/18/"at 10"
- All new baselines present at 32 update sites
- diff: 1 file changed, 34 insertions(+), 30 deletions(-)

Next step: plan-checker re-validation of arithmetic + consistency before executor spawn.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 21:45:58 +02:00

109 KiB
Raw Permalink Blame History

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, tags, must_haves, tags_repeat, user_setup
phase plan type wave depends_on files_modified autonomous requirements tags must_haves tags_repeat user_setup
01-stabilize-video-pipeline 12 tdd 6
01-09
01-13
01-14
src/shared/tokens.css
src/shared/fonts/Lora-VariableFont.woff2
src/shared/fonts/Lora-Italic-VariableFont.woff2
src/shared/fonts/IBMPlexSans-Regular.woff2
src/shared/fonts/IBMPlexSans-Medium.woff2
src/shared/fonts/IBMPlexSans-SemiBold.woff2
src/shared/fonts/IBMPlexSans-Bold.woff2
src/shared/fonts/IBMPlexMono-Regular.woff2
src/shared/fonts/IBMPlexMono-Medium.woff2
src/shared/fonts/LICENSE-Lora.txt
src/shared/fonts/LICENSE-IBM-Plex.txt
src/shared/fonts/README.md
src/shared/brand/mokosh-mark.svg
src/shared/brand/mokosh-lockup.svg
icons/icon16.png
icons/icon48.png
icons/icon128.png
scripts/rasterize-icons.sh
scripts/subset-fonts.sh
scripts/README.md
manifest.json
_locales/en/messages.json
_locales/ru/messages.json
src/popup/index.html
src/popup/index.ts
src/popup/style.css
src/welcome/welcome.css
src/welcome/welcome-tokens.css
src/welcome/welcome.ts
src/welcome/copy.ts
src/background/index.ts
vite.config.ts
vite.test.config.ts
tests/build/tokens-adopted.test.ts
tests/build/fonts-present.test.ts
tests/build/icons-present.test.ts
tests/build/no-remote-fonts.test.ts
tests/i18n/manifest-i18n.test.ts
tests/i18n/locale-parity.test.ts
tests/uat/extension-page-harness.ts
tests/uat/lib/harness-page-driver.ts
tests/uat/harness.test.ts
.planning/STATE.md
.planning/ROADMAP.md
false
REQ-video-ring-buffer
REQ-install-clean
REQ-manifest-permissions
design-integration
brand
tokens-css
woff2-selfhost
svg-rasterization
mv3-csp
i18n
locales
chrome-i18n
D-01-mark
D-02-welcome
D-03-voice-sober
D-04-palette
D-05-typography
D-06-icon-strategy
D-07-extname-override
D-08-tagline
D-09-smoke-dev-only
R2-lora-substitute
Approach-B-harness-extension
harness-A18-A22
truths artifacts key_links
Self-hosted WOFF2 font bundle lands at src/shared/fonts/: Lora variable (Latin+Cyrillic via OFL Cyreal foundry release, replaces Newsreader per R2 designer reply 2026-05-19); IBM Plex Sans 400/500/600/700 latin+cyrillic; IBM Plex Mono 400/500 latin+cyrillic. Total bundle ~250-300KB. LICENSE-Lora.txt + LICENSE-IBM-Plex.txt + README.md ship alongside per OFL-1.1 attribution best practice.
src/shared/tokens.css is the canonical token system: copy of .planning/intel/design-incoming/system/bundle/mokosh-handoff/tokens.css with two surgical edits — (a) line 12 Google Fonts @import REPLACED with 7 local @font-face rules (Lora variable normal + italic; Plex Sans 4 weights; Plex Mono 2 weights), and (b) --mks-font-display value substituted from 'Newsreader' to 'Lora' per R2. Adds .mks-word class definition (font-family: var(--mks-font-display); font-size: 32px; font-weight: var(--mks-weight-regular); fill: var(--mks-ink-900);) required by mokosh-lockup.svg line 21.
src/shared/tokens.css contains ZERO references to fonts.googleapis.com or any https:// URL — MV3 CSP `style-src 'self'` + `font-src 'self'` enforced. Verified by tests/build/no-remote-fonts.test.ts grepping the built dist/ for the substring `googleapis` (occurrences === 0) and `https://fonts` (occurrences === 0).
icons/icon{16,48,128}.png are rasterized from src/shared/brand/mokosh-mark.svg via rsvg-convert (per RESEARCH §3 verified locally: 16=406B, 48=784B, 128=1952B). Each clears the assets-spec.md FLOOR: 16≥200B, 48≥500B, 128≥1024B. PNGs are committed as static artifacts (NOT regenerated at build time per RESEARCH §3 anti-pattern); scripts/rasterize-icons.sh is the documented re-run recipe. The Bug A placeholder icons (dark-square + green-dot per Plan 01-09 Path A; verified pre-existing as 574B/1153B/2615B 16-bit RGB at icons/icon{16,48,128}.png) are OVERWRITTEN.
manifest.json:name = '__MSG_extName__' (was 'AI Call Recorder'); manifest.json:description = '__MSG_extDesc__' (was 'Запись сессий операторов для диагностики ошибок'); manifest.json:default_locale = 'en'; manifest.json:action.default_title = '__MSG_tooltipOff__'. _locales/en/messages.json carries extName='Mokosh — Session Capture' per D-07 user override and extDesc='Thirty seconds ago, always at hand.' per D-08 (using brand-decisions-v1.md final wording, not Brief §02's '...within reach.' variant — A4 from RESEARCH Open Questions resolved to brand-decisions-v1.md as the more recent doc).
8 i18n keys land in BOTH _locales/en/messages.json AND _locales/ru/messages.json (default_locale parity per RESEARCH Pitfall 4): extName, extDesc, tooltipOff, tooltipRecPrefix, tooltipErr, popupSavePrompt, popupSaveCta, popupSaveDone, notifStartup, notifRecovery, welcomeHeroRu, welcomeHeroEn (12 total keys; the 8 Brief §02 strings + 4 supporting keys including manifest fields and tooltipRec split). Every key in ru/messages.json is also present in en/messages.json (verified by tests/i18n/locale-parity.test.ts).
src/background/index.ts is migrated from hardcoded brand strings (line 82 BADGE_REC_TITLE 'Recording — last 30 s buffered. Click to save.'; line 83 BADGE_OFF_TITLE 'Not recording. Click to start.'; line 84 BADGE_ERROR_TITLE 'Recording error. Click to try again.'; line 862 'Mokosh stopped'; line 863 'Recording stopped. Click here to start a new session.'; line 964 'Mokosh ready'; line 965 startup-message) to chrome.i18n.getMessage('<key>') reads. BADGE_REC_COLOR ('#00C853' material-green at line 76) is updated to '#b2543d' (= --mks-madder-600 per D-04 loom palette) with a comment cross-referencing the token, per RESEARCH §10 Open Questions A7 default-action. BADGE_OFF_COLOR + BADGE_ERROR_COLOR remain as plan-discretion engineering choices unless the palette dictates otherwise; document as such in the source comment.
src/popup/index.html title element becomes '__MSG_extName__' (or empty, populated by JS via chrome.i18n.getMessage('extName')); span.button-text remains empty (populated by JS per RESEARCH Pitfall 3); src/popup/index.ts updateUI() reads buttonText.textContent = chrome.i18n.getMessage('popupSaveCta') in idle case, 'popupSaveDone' in done case; the 'Архив успешно сохранён!' literal at saveArchive() success branch (line 91) becomes chrome.i18n.getMessage('popupSaveDone'); the info-text 'Последние 30 сек видео + 10 мин лога' at index.html line 15 becomes a data-mks-key='popupInfoText' attribute populated by JS. src/popup/style.css imports `../shared/tokens.css` and ALL hardcoded hex literals (#2563eb, #1d4ed8, #94a3b8, #f8f9fa, #0891b2, #059669, #dc2626, #64748b) are replaced with var(--mks-*) references (--mks-rec for save button, --mks-success for done state, --mks-error for error, --mks-fg-2 for info text, --mks-surface for body bg, --mks-fg-disabled for disabled, --mks-warning for warning info).
If Plan 01-10 (welcome tab) has landed before Plan 01-12 executes: src/welcome/welcome-tokens.css placeholder is REPLACED with a one-liner `@import '../shared/tokens.css';`, and any per-key copy literals in src/welcome/copy.ts are migrated to chrome.i18n.getMessage('<key>') reads (welcomeHeroRu, welcomeHeroEn) with the in-file COPY map removed or reduced to a fallback shim. If Plan 01-10 has NOT yet landed: src/welcome/* files are NOT created by Plan 01-12; the canonical tokens.css is import-ready from src/shared/tokens.css and Plan 01-10 executor can swap to it from day 1 (no placeholder welcome-tokens.css needed).
vite.config.ts gains a `define` block setting __VITE_DEV__ = JSON.stringify(process.env.VITE_DEV === '1') per RESEARCH §12 D-09 spirit-satisfaction (defensive flag for any future inline smoke-mode check; smoke.sh continues to live outside Vite's input set entirely — verified by `grep -rn 'smoke|SMOKE|data:text/html' src/` returning empty). vite.test.config.ts inherits via mergeConfig. scripts/README.md (NEW) documents the smoke isolation invariant in one paragraph.
Tier-1 forbidden-strings inventory in tests/background/no-test-hooks-in-prod-bundle.test.ts AND tests/uat/harness.test.ts is UNCHANGED at 12 strings post-Plan-01-14 (no new test-mode symbols introduced by Plan 01-12; the i18n + tokens + font work uses production chrome.* APIs end-to-end). Plan 01-14 established the 12-string baseline by adding `lastGetDisplayMediaConstraints` + `get-last-getDisplayMedia-constraints` for its monitorTypeSurfaces harness. Verified by file-diff after plan close.
UAT harness extended with 5 new assertions A18-A22: A18 = Lora WOFF2 loads from chrome.runtime.getURL('shared/fonts/Lora-VariableFont.woff2') OR equivalent emitted asset path (response.ok && content-length > 50000 — Lora variable Latin+Cyrillic is ~80-120KB after subsetting). A19 = icons are NOT the Bug A placeholders (icon128.png file size differs from the known 2615-byte 16-bit-RGB placeholder; OR icon128.png file content hash differs from the placeholder content hash). A20 = chrome.runtime.getManifest().name === 'Mokosh — Session Capture' (i18n resolution test; the en-locale fallback always returns this exact string per default_locale='en'). A21 = getComputedStyle on a test element with CSS class .mks-display-1 inside the harness page resolves to a font-family stack starting with 'Lora' (not 'Newsreader', not system fallback). A22 = welcome page tokens.css is loaded if 01-10 has landed (fetch chrome.runtime.getURL('src/welcome/welcome.html') returns 200 + welcome.css resolves --mks-rec to '#b2543d'); skip with a documented diagnostic if 01-10 has not landed.
Every Plan 01-13 harness assertion (A0-A14), Plan 01-14 assertion (A23), and Plan 01-10 assertion (A15-A17, IF landed) remains GREEN after Plan 01-12 changes (no regression). vitest baseline grows by 6 tests (icons-present + tokens-adopted + fonts-present + no-remote-fonts + manifest-i18n + locale-parity) from 100 GREEN (post-01-14) to 106 GREEN; npm run test:uat grows from 16/16 (post-01-14, or 19/19 with 01-10) to 21/21 (or 24/24 with 01-10 + 01-12) — all GREEN.
MV3 architectural constraints from 01-11-SUMMARY enforced: NO `await import(...)` anywhere in src/background/index.ts (chrome.i18n.getMessage is a synchronous API; no dynamic imports required); test-mode symbols stay in dist-test/ only via __MOKOSH_UAT__ define-token (Plan 01-12 introduces no new test-mode symbols).
path provides contains
src/shared/tokens.css Canonical token system (Mokosh design tokens; single source of truth). Copy of design-incoming tokens.css with: (1) line 12 Google Fonts @import REMOVED + replaced with 7 local @font-face rules pointing at ./fonts/*.woff2 (Lora variable normal + italic; Plex Sans 400/500/600/700; Plex Mono 400/500); (2) --mks-font-display value substituted from 'Newsreader' → 'Lora' per R2 designer reply 2026-05-19; (3) .mks-word class definition added (font-family: var(--mks-font-display); font-size: 32px; font-weight: var(--mks-weight-regular); fill: var(--mks-ink-900); letter-spacing: var(--mks-tracking-display);) at end-of-file per RESEARCH §8 + lockup SVG line 21 requirement. ZERO https:// URLs anywhere in this file. @font-face
path provides
src/shared/fonts/Lora-VariableFont.woff2 Lora variable font, normal style, weight axis 400-700, subset to Latin (U+0020-007E, U+00A0-00FF) + Cyrillic basic (U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116) via pyftsubset --flavor=woff2 from upstream OFL .ttf at github.com/cyrealtype/Lora-Cyrillic or Google Fonts metadata source. Expected size 80-120KB.
path provides
src/shared/fonts/Lora-Italic-VariableFont.woff2 Lora variable italic, same subset as above. Expected size 80-120KB. NB: if upstream Lora ships italic in same file as normal via opsz/wght/ital axes (variable font with multi-axis support), this file may be omitted — A5 from RESEARCH Assumptions Log: verify upstream variable build structure at execute-plan time and adjust if italic+normal collapse into one file.
path provides
src/shared/fonts/IBMPlexSans-Regular.woff2 IBM Plex Sans 400, Latin + Cyrillic basic. ~25KB per RESEARCH §1 sizing table.
path provides
src/shared/fonts/IBMPlexSans-Medium.woff2 IBM Plex Sans 500, Latin + Cyrillic basic. ~25KB.
path provides
src/shared/fonts/IBMPlexSans-SemiBold.woff2 IBM Plex Sans 600, Latin + Cyrillic basic. ~25KB.
path provides
src/shared/fonts/IBMPlexSans-Bold.woff2 IBM Plex Sans 700, Latin + Cyrillic basic. ~25KB.
path provides
src/shared/fonts/IBMPlexMono-Regular.woff2 IBM Plex Mono 400, Latin + Cyrillic basic. ~28KB.
path provides
src/shared/fonts/IBMPlexMono-Medium.woff2 IBM Plex Mono 500, Latin + Cyrillic basic. ~28KB.
path provides
src/shared/fonts/LICENSE-Lora.txt Verbatim copy of Lora OFL-1.1 license + Cyreal foundry copyright lines, per OFL-FAQ best practice.
path provides
src/shared/fonts/LICENSE-IBM-Plex.txt Verbatim copy of IBM Plex OFL-1.1 license from github.com/IBM/plex/blob/master/LICENSE.txt.
path provides min_lines
src/shared/fonts/README.md One-pager attribution + regeneration recipe per RESEARCH §7 template. Lists all bundled faces + upstream URLs + subsetting recipe + license notes. 15
path provides
src/shared/brand/mokosh-mark.svg Copy of .planning/intel/design-incoming/system/bundle/mokosh-handoff/assets/mokosh-mark.svg (Loom 2×2 weave intersection; D-01 brand mark). Source-of-truth for icon rasterization. Stays at hardcoded stroke='#181b2a' for PNG output; future Plan 01-10 may copy + adapt to currentColor for inline-SVG welcome usage (see RESEARCH §5).
path provides
src/shared/brand/mokosh-lockup.svg Copy of mokosh-lockup.svg (mark + 'Mokosh' wordmark at 240×56 viewBox). The .mks-word class referenced at line 21 is defined in src/shared/tokens.css per artifact above.
path provides
icons/icon16.png Rasterized Loom mark at 16×16 via `rsvg-convert -w 16 -h 16 src/shared/brand/mokosh-mark.svg -o icons/icon16.png`. ≥200 bytes (~406B per RESEARCH §3 verified). REPLACES the prior 574B 16-bit-RGB placeholder.
path provides
icons/icon48.png Rasterized Loom mark at 48×48. ≥500 bytes (~784B). REPLACES the prior 1153B 16-bit-RGB placeholder.
path provides
icons/icon128.png Rasterized Loom mark at 128×128. ≥1024 bytes (~1952B). REPLACES the prior 2615B 16-bit-RGB placeholder.
path provides min_lines
scripts/rasterize-icons.sh One-off rasterization recipe per RESEARCH §3. Bash script reading src/shared/brand/mokosh-mark.svg and emitting icons/icon{16,48,128}.png via rsvg-convert. Documented for re-run when the mark SVG changes; NOT wired into prebuild (anti-pattern per RESEARCH §3). 15
path provides min_lines
scripts/subset-fonts.sh One-off font subsetting recipe per RESEARCH §1. Bash script for Lora (Latin+Cyrillic subset, variable normal + italic), Plex Sans 400/500/600/700 (Latin+Cyrillic), Plex Mono 400/500 (Latin+Cyrillic), via pyftsubset --flavor=woff2 from upstream .ttf sources downloaded one-off into a scratch dir. Documents the canonical --unicodes string. 30
path provides min_lines
scripts/README.md One-paragraph note per RESEARCH §12: smoke.sh and other dev-only scripts live in this directory and are NOT bundled by `npm run build`. The production dist/ contains no smoke artifacts (verified by `grep -rn smoke dist/` returning empty). 8
path provides contains
manifest.json Updated manifest: `name` = '__MSG_extName__'; `description` = '__MSG_extDesc__'; `default_locale` = 'en' (NEW field); `action.default_title` = '__MSG_tooltipOff__' (NEW field per RESEARCH §11 + Brief §02 string #1). All other fields preserved unchanged (permissions, host_permissions, background, content_scripts, action.default_popup, action.default_icon, icons). default_locale
path provides min_lines
_locales/en/messages.json English locale messages (default_locale fallback). 12 keys: extName, extDesc, tooltipOff, tooltipRecPrefix, tooltipErr, popupSavePrompt, popupSaveCta, popupSaveDone, popupInfoText, notifStartup, notifRecovery, welcomeHeroRu, welcomeHeroEn. extName='Mokosh — Session Capture' per D-07; extDesc='Thirty seconds ago, always at hand.' per D-08 (brand-decisions-v1.md wording). Strings inherit D-03 Sober register. Each key has both message + description fields per Chrome i18n schema. 70
path provides min_lines
_locales/ru/messages.json Russian locale messages (primary operator locale). Same 12 keys as en/messages.json with Russian text per RESEARCH §10 Brief §02 verbatim extraction: extName='Mokosh — Запись сессии'; extDesc='Тридцать секунд назад, всегда под рукой.'; tooltipOff='Mokosh — щёлкните, чтобы начать запись'; tooltipRecPrefix='Mokosh — идёт запись'; tooltipErr='Mokosh — ошибка записи, щёлкните для восстановления'; popupSavePrompt='Сохранить отчёт об ошибке?'; popupSaveCta='Сохранить отчёт'; popupSaveDone='Архив сохранён в Загрузки.'; popupInfoText='Последние 30 сек видео + 10 мин лога'; notifStartup='Запись запущена. Я слежу за последними 30 секундами.'; notifRecovery='Запись возобновлена. Буфер снова заполняется.'; welcomeHeroRu='Тридцать секунд назад, всегда под рукой.'; welcomeHeroEn='Thirty seconds ago, always at hand.'. 70
path provides contains
src/popup/index.html Popup HTML migrated to i18n: <title>Mokosh — Session Capture</title> retained (chrome doesn't substitute __MSG__ in HTML body per RESEARCH Pitfall 3); button.button-text starts empty (populated by JS); info-text gains data-mks-key='popupInfoText' attribute; statusMessage retained for runtime status. Stylesheet link to style.css unchanged. data-mks-key
path provides contains
src/popup/index.ts Popup TS migrated to chrome.i18n.getMessage: updateUI() idle case sets buttonText.textContent = chrome.i18n.getMessage('popupSaveCta'); done case sets 'Готово! ✓' replaced with chrome.i18n.getMessage('popupSaveDone') OR a dedicated 'popupSaveDoneShort' key; init() populates all [data-mks-key] elements via a small helper. Empty-state message line 62 'Откройте запись через иконку расширения' becomes chrome.i18n.getMessage('popupEmptyState') with a corresponding key added to messages.json. Logger import + log helper unchanged. chrome.i18n.getMessage
path provides contains
src/popup/style.css Restyled per D-04 loom palette: @import '../shared/tokens.css' at top; body { background: var(--mks-surface); color: var(--mks-fg-1); font-family: var(--mks-font-ui); }; .save-button { background: var(--mks-rec); color: var(--mks-fg-inverse); border-radius: var(--mks-radius-md); box-shadow: var(--mks-shadow-1); }; .save-button:hover, .save-button.saving, .save-button.done all use --mks-madder-700, --mks-amber-600, --mks-moss-600 respectively; .info-text uses var(--mks-fg-2); .status-message.* use --mks-error / --mks-success / --mks-warning. ZERO hex literals. var(--mks-
path provides contains
src/welcome/welcome.css (CONDITIONAL — only modified if Plan 01-10 has landed at execute-plan time): Replace placeholder color tokens with @import '../shared/tokens.css' at top; keep all var(--mks-*) references (welcome.css already uses var(--mks-*) per 01-10 design-swap-ready architecture). If 01-10 hasn't landed: this file is NOT created by Plan 01-12. ../shared/tokens.css
path provides
src/welcome/welcome-tokens.css (CONDITIONAL — only modified if Plan 01-10 has landed): file becomes a one-liner `@import '../shared/tokens.css';` OR is deleted entirely with welcome.css importing tokens.css directly. If 01-10 hasn't landed: file is NOT created.
path provides contains
src/welcome/welcome.ts (CONDITIONAL — only modified if Plan 01-10 has landed): copy population uses chrome.i18n.getMessage('welcomeHeroRu'), chrome.i18n.getMessage('welcomeHeroEn'), and other welcome keys instead of the in-file COPY map. The COPY import from './copy' is removed OR copy.ts is reduced to a fallback-only shim. If 01-10 hasn't landed: file is NOT created. chrome.i18n.getMessage
path provides
src/welcome/copy.ts (CONDITIONAL — only modified if Plan 01-10 has landed): file becomes either deleted or a fallback shim (e.g., a function `getCopy(key)` that returns `chrome.i18n.getMessage(key) || COPY[key]` for grace under missing locales). If 01-10 hasn't landed: file is NOT created.
path provides contains
src/background/index.ts Migrated to i18n + token palette: lines 76 BADGE_REC_COLOR='#00C853' updated to '#b2543d' with comment `// --mks-madder-600 per D-04 loom palette`; lines 82-84 BADGE_*_TITLE constants are unused once setTitle reads chrome.i18n.getMessage at the call site (the 4 setTitle call sites in setBadgeState read chrome.i18n.getMessage('tooltipOff'|'tooltipRec'|'tooltipErr')); lines 862-865 notification title='Mokosh stopped' + message='Recording stopped...' migrate to chrome.i18n.getMessage('notifRecoveryTitle' or similar + 'notifRecovery'); line 964 notification 'Mokosh ready' + 'Click here to start recording your session.' migrate to chrome.i18n.getMessage('notifStartupTitle' + 'notifStartup'). For tooltipRec the runtime time-suffix `' (' + formatElapsed(seconds) + ')'` is concatenated after the chrome.i18n.getMessage('tooltipRecPrefix') call per RESEARCH §10 note. No dynamic imports added (MV3 SW constraint). chrome.i18n.getMessage
path provides contains
vite.config.ts Adds `define` field property __VITE_DEV__ = JSON.stringify(process.env.VITE_DEV === '1') alongside existing __MOKOSH_UAT__: 'false', per RESEARCH §12 D-09 defensive flag. NO other changes (build target, resolve.alias, rollupOptions.input, plugins all preserved). __VITE_DEV__
path provides
vite.test.config.ts Inherits __VITE_DEV__ via mergeConfig (base config gains the field; test config doesn't override). No structural change to existing __MOKOSH_UAT__: 'true' override or rollupOptions.input.
path provides min_lines
tests/build/tokens-adopted.test.ts Vitest unit test: src/shared/tokens.css exists + parses as CSS; popup/style.css contains `@import '../shared/tokens.css'`; src/popup/style.css contains ZERO `#[0-9a-fA-F]{3,8}` hex literals (regex match count === 0); src/welcome/welcome.css if exists contains ZERO hex literals. 30
path provides min_lines
tests/build/fonts-present.test.ts Vitest unit test: every WOFF2 file declared in src/shared/tokens.css @font-face exists at the relative path; each is > 0 bytes; the file-name format ('Lora-VariableFont.woff2' or similar) matches the @font-face src URL. Validates the 7 expected files (Lora normal, Lora italic if separate, Plex Sans ×4, Plex Mono ×2) per Wave 0 inventory. 25
path provides min_lines
tests/build/icons-present.test.ts Vitest unit test: icons/icon16.png ≥ 200 B; icons/icon48.png ≥ 500 B; icons/icon128.png ≥ 1024 B (Chrome imageUtil floors per assets-spec.md). Each is a PNG by signature (first 8 bytes match \x89PNG\r\n\x1a\n). Each is at the expected dimensions (read via simple PNG header parse: width at bytes 16-19, height at bytes 20-23, network byte order). 35
path provides min_lines
tests/build/no-remote-fonts.test.ts Vitest unit test: `npm run build` runs (or SKIP_BUILD=1); reads dist/ recursively; asserts ZERO files contain 'googleapis' OR 'https://fonts'. Mirrors the existing no-test-hooks-in-prod-bundle.test.ts execFile pattern (RESEARCH §1 + §13 CSP migration verification). 40
path provides min_lines
tests/i18n/manifest-i18n.test.ts Vitest unit test: manifest.json parses; manifest.name === '__MSG_extName__'; manifest.description === '__MSG_extDesc__'; manifest.default_locale === 'en'; manifest.action.default_title === '__MSG_tooltipOff__'; _locales/en/messages.json parses with extName.message === 'Mokosh — Session Capture' + extDesc.message === 'Thirty seconds ago, always at hand.'; _locales/ru/messages.json parses with extName.message === 'Mokosh — Запись сессии' + extDesc.message === 'Тридцать секунд назад, всегда под рукой.'. 35
path provides min_lines
tests/i18n/locale-parity.test.ts Vitest unit test: every key in _locales/ru/messages.json exists in _locales/en/messages.json (default_locale parity per RESEARCH Pitfall 4). Every key's .message field is a non-empty string. 20
path provides min_lines
tests/uat/extension-page-harness.ts EXTENDED with 5 new page-side assertion methods on window.__mokoshHarness: assertA18 (font asset reachability + size floor), assertA19 (icon not Bug A placeholder — hash-distinct), assertA20 (manifest name resolved via i18n), assertA21 (--mks-font-display computed value === 'Lora'), assertA22 (welcome tokens loaded — gated on welcome.html being reachable). Each method returns the standard AssertionRecord { passed, name, checks, diagnostics } shape consistent with A1-A17. The harness page DOM gains a small test-element div with class='mks-display-1' for A21's getComputedStyle probe; this DOM element ships in the test bundle only (dist-test/) since it lives inside extension-page-harness.html. 200
path provides min_lines
tests/uat/lib/harness-page-driver.ts Adds driveA18, driveA19, driveA20, driveA21, driveA22 host-side wrappers following the existing driveA4 pattern (page.evaluate → window.__mokoshHarness.assertXX). No new host-side filesystem or ffprobe primitives; all A18-A22 work runs page-side via fetch + getComputedStyle + chrome.runtime.getManifest. 60
path provides contains
tests/uat/harness.test.ts Orchestrator extended with A18-A22 entries. FORBIDDEN_HOOK_STRINGS list UNCHANGED at 12 entries post-Plan-01-14 (no new test-mode symbols introduced by Plan 01-12). Assertion count grows from 16/16 (post-01-13+01-14) or 19/19 (post-01-10+01-13+01-14) to 21/21 or 24/24 (post-01-12). Bail-on-first-failure orchestration unchanged. A18
path provides
.planning/STATE.md Wave 7 closure update: completed_plans increments by 1 (Plan 01-12 closes); decision-log appends '[Phase 01-12]: design integration landed — Lora self-hosted (R2 per designer 2026-05-19) + tokens.css canonical + 8 i18n strings + branded icons + manifest:name i18n; UAT harness 21/21 GREEN (or 24/24 if 01-10 also landed); operator brand-fit ack received'. percent recomputed.
path provides
.planning/ROADMAP.md Phase 1 Plans list gains `- [x] 01-12-PLAN.md — Design integration (R2 Lora, tokens.css, 8 i18n strings, branded icons, manifest i18n)` entry after 01-13. If Plan 01-10 not yet in list, also add its entry as `- [ ]` placeholder unless already present.
from to via pattern
src/shared/tokens.css @font-face src urls src/shared/fonts/*.woff2 relative url('./fonts/<face>.woff2') per RESEARCH §2 Pattern A; Vite asset pipeline rebases at build to dist/assets/<hash>.woff2 with @crxjs auto-generated web_accessible_resources entry url("./fonts/[A-Z][A-Za-z]+-[A-Za-z]+.woff2")
from to via pattern
src/popup/style.css src/shared/tokens.css @import '../shared/tokens.css'; at top of file @import.*shared/tokens.css
from to via pattern
manifest.json:action.default_title _locales/{en,ru}/messages.json:tooltipOff.message Chrome i18n runtime substitution via __MSG_tooltipOff__ token + default_locale fallback chain __MSG_tooltipOff__
from to via pattern
src/background/index.ts setBadgeState() setTitle call _locales/{en,ru}/messages.json chrome.i18n.getMessage('tooltipOff'|'tooltipRec'|'tooltipErr') synchronous read chrome.i18n.getMessage
from to via pattern
src/popup/index.ts saveArchive success branch _locales/{en,ru}/messages.json:popupSaveDone.message chrome.i18n.getMessage('popupSaveDone') in updateUI/saveArchive popupSaveDone
from to via pattern
icons/icon{16,48,128}.png src/shared/brand/mokosh-mark.svg scripts/rasterize-icons.sh runs rsvg-convert -w N -h N <svg> -o icons/iconN.png rsvg-convert.*mokosh-mark
from to via pattern
tests/uat/extension-page-harness.ts assertA18 src/shared/fonts/Lora-VariableFont.woff2 fetch(chrome.runtime.getURL('shared/fonts/Lora-VariableFont.woff2')) OR fetch using emitted asset-rebased path (extracted via document.styleSheets[i].cssRules @font-face src.url at runtime — works regardless of Vite hashing) chrome.runtime.getURL.*.woff2
from to via pattern
tests/uat/extension-page-harness.ts assertA21 src/shared/tokens.css --mks-font-display value Creates a transient div, applies class='mks-display-1', reads getComputedStyle(div).fontFamily, asserts it starts with 'Lora' getComputedStyle.*fontFamily
from to via pattern
tests/build/no-remote-fonts.test.ts dist/ artifact tree execFile npm run build; recursive readFileSync over dist/; grep for 'googleapis' + 'https://fonts' googleapis|https://fonts
design-integration
locales
mv3-csp
service why network_needed one_off_download license_attribution_required license_file
Lora font (OFL-1.1) R2 designer reply 2026-05-19 substitutes Newsreader → Lora for Cyrillic coverage; Plan 01-12 self-hosts the WOFF2 per MV3 CSP true
url why installs_to
https://github.com/cyrealtype/Lora-Cyrillic OR Google Fonts Lora release page upstream Lora variable TTF — subsetted to Latin+Cyrillic by scripts/subset-fonts.sh (transient scratch dir; only the subsetted woff2 committed to src/shared/fonts/)
true src/shared/fonts/LICENSE-Lora.txt
service why network_needed one_off_download license_attribution_required license_file
IBM Plex Sans + Mono (OFL-1.1) D-05 type pairing; Plex Sans is the UI body face with full Cyrillic; Plex Mono is the diagnostic/timer face true
url why installs_to
https://github.com/IBM/plex/releases upstream Plex Sans + Mono v1.1.0 TTFs (transient scratch dir)
true src/shared/fonts/LICENSE-IBM-Plex.txt

Scope Sanity Note

7 waves, 14 tasks total (Wave 0 TDD scaffolds + Waves 1-7 implementation including 1 closing checkpoint). Above the "split signal" thresholds in <scope_estimation>, but the scope is dictated by the source artifact set (8 i18n strings + token system + 7 fonts + 3 icons + manifest + popup + welcome conditional + harness extension + UAT closure) and the planner has no authority to reduce coverage (see planner_authority_limits). Each wave is intended for a fresh-context executor spawn per the GSD execute-phase pattern.

Why not split further:

  1. Wave structure already maps to natural seams. Wave 0 (TDD scaffolds — RED-by-design unit tests for tokens/fonts/icons/i18n/no-remote-fonts) gates every later wave's GREEN outcome. Wave 1 (fonts + tokens.css) is the foundation. Wave 2 (icons) is independent. Wave 3 (manifest + locales) depends only on tokens existing. Wave 4 (popup + background i18n + palette) depends on Waves 1-3. Wave 5 (welcome conditional + Vite define) is a single mostly-cosmetic wave. Wave 6 (harness A18-A22) extends the proven 01-13 architecture. Wave 7 closure + operator checkpoint.
  2. Per-executor context budget: Wave 0 ~10%, Wave 1 ~35% (font subsetting + tokens.css migration; the heaviest wave), Wave 2 ~10%, Wave 3 ~15%, Wave 4 ~25%, Wave 5 ~10%, Wave 6 ~20%, Wave 7 ~5%. Each wave fits the ~50% target with margin.
  3. Cross-wave atomicity: every wave produces a coherent commit-set with vitest GREEN at wave-close (Tier-1 gate, locale-parity, no-remote-fonts, etc. all run after every wave).

If a future revision DOES force a split, natural cut line: Plan 01-12A = Waves 0-3 (test scaffolds + fonts + tokens + icons + manifest + locales); Plan 01-12B = Waves 4-7 (source-code adoption + welcome + harness + closure). Wave numbering reflects this potential split cleanly.

Land the full Mokosh design integration end-to-end across 7 waves. Three coordinated changes from the current Plan 01-13 baseline:
  1. Self-host the OFL font bundle + the canonical token system. Subset Lora variable (per R2 designer reply 2026-05-19: Newsreader substituted with Lora for Cyrillic coverage), IBM Plex Sans 400/500/600/700, and IBM Plex Mono 400/500 to Latin + Cyrillic basic via pyftsubset. Place WOFF2 + LICENSE + README files under src/shared/fonts/. Land src/shared/tokens.css as the canonical token system — copy of designer's handoff with two surgical edits: (a) Google Fonts @import → local @font-face rules; (b) --mks-font-display from "Newsreader" to "Lora". Add .mks-word class for the lockup wordmark. Verifies via tests/build/no-remote-fonts.test.ts (CSP-safe production bundle).

  2. Brand-mark PNG icons + manifest i18n. Rasterize src/shared/brand/mokosh-mark.svg to icons/icon{16,48,128}.png via rsvg-convert, replacing the Plan 01-09 Path A dark-square + green-dot placeholders. Migrate manifest.json to __MSG_extName__ + __MSG_extDesc__ + default_locale: "en" + action.default_title: "__MSG_tooltipOff__". Land _locales/{en,ru}/messages.json with the 8 Brief §02 operator-facing strings + 4 supporting keys (extName per D-07 override, extDesc per D-08 tagline, popupInfoText, popupEmptyState).

  3. Source-code adoption of tokens + i18n. Restyle src/popup/style.css to import src/shared/tokens.css and replace all hex literals with var(--mks-*) references per D-04 loom palette. Migrate src/popup/index.ts + src/background/index.ts from hardcoded RU/EN strings to chrome.i18n.getMessage reads at every call site (badge titles, notification copy, popup CTA + status). Update BADGE_REC_COLOR from material-green #00C853 to madder #b2543d (= --mks-madder-600). Wire the welcome page (Plan 01-10 conditional) to the canonical tokens.css.

Wave 6 extends the 01-13 UAT harness with 5 new assertions A18-A22 covering: WOFF2 reachability + size floor; icons-not-placeholders; manifest:name i18n resolution; --mks-font-display resolves to Lora; welcome tokens loaded (conditional on 01-10 landing). The harness pattern follows 01-13 Wave 3 exactly (page-side assertA18..A22 methods + host-side driveA18..A22 wrappers + orchestrator entries).

Wave 7 closure: operator empirical brand-fit checkpoint (autonomous: false) — fresh build, load unpacked, verify branded surfaces (toolbar icon, popup palette + Lora display heading, manifest name in chrome://extensions, Russian copy with Lora rendering, notification copy). Operator ack closes the plan + Phase 1 charter.

Operator role at closure: post-01-13, the operator's gates are brand/design only. Plan 01-12 satisfies the LAST remaining Phase 1 operator gate (brand-fit ack of branded surfaces).

Output:

  • Wave 0: 6 RED unit tests scaffolded (tokens-adopted, fonts-present, icons-present, no-remote-fonts, manifest-i18n, locale-parity); each RED today, GREEN after later waves land their respective artifacts.
  • Wave 1: src/shared/fonts/ + src/shared/tokens.css + scripts/subset-fonts.sh; fonts-present + no-remote-fonts flip GREEN.
  • Wave 2: icons/icon{16,48,128}.png + scripts/rasterize-icons.sh + src/shared/brand/; icons-present flips GREEN.
  • Wave 3: manifest.json + _locales/{en,ru}/messages.json; manifest-i18n + locale-parity flip GREEN; manifest:name resolves to "Mokosh — Session Capture" on extension reload.
  • Wave 4: src/popup/style.css + src/popup/index.html + src/popup/index.ts + src/background/index.ts; tokens-adopted flips GREEN; existing 100+ vitest tests stay GREEN (i18n-call-site migration is behavior-equivalent for the chrome.* mock pattern used by existing tests).
  • Wave 5: src/welcome/* conditional migration (if 01-10 landed) OR skip; vite.config.ts VITE_DEV define-token + scripts/README.md.
  • Wave 6: tests/uat/extension-page-harness.ts + tests/uat/lib/harness-page-driver.ts + tests/uat/harness.test.ts extended with A18-A22; npm run test:uat reports 21/21 (or 24/24 with 01-10) GREEN.
  • Wave 7: operator checkpoint; STATE.md + ROADMAP.md sync; SUMMARY produced.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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-PLAN.md @.planning/phases/01-stabilize-video-pipeline/01-09-SUMMARY.md @.planning/phases/01-stabilize-video-pipeline/01-10-PLAN.md @.planning/phases/01-stabilize-video-pipeline/01-11-SUMMARY.md @.planning/phases/01-stabilize-video-pipeline/01-13-PLAN.md @.planning/phases/01-stabilize-video-pipeline/01-13-SUMMARY.md @.planning/phases/01-stabilize-video-pipeline/01-12-RESEARCH.md @.planning/intel/brand-decisions-v1.md @.planning/intel/brand-decisions-v1-followup-display-font.md @.planning/intel/assets-spec.md @.planning/intel/design-incoming/system/bundle/mokosh-handoff/tokens.css @.planning/intel/design-incoming/system/bundle/mokosh-handoff/README.txt @.planning/intel/design-incoming/system/bundle/mokosh-handoff/assets/mokosh-mark.svg @.planning/intel/design-incoming/system/bundle/mokosh-handoff/assets/mokosh-lockup.svg @src/background/index.ts @src/popup/index.html @src/popup/index.ts @src/popup/style.css @manifest.json @vite.config.ts @vite.test.config.ts @tests/uat/extension-page-harness.ts @tests/uat/harness.test.ts @tests/uat/lib/harness-page-driver.ts @tests/background/no-test-hooks-in-prod-bundle.test.ts

Designer reply ratifying R2 (per follow-up doc + the 2026-05-19 reply quoted in this plan's spawn brief)

--mks-font-display: "Lora", "Iowan Old Style", "Times New Roman", serif;

Use this EXACT string as the canonical token value in src/shared/tokens.css. Do NOT derive a different chain. Lora is OFL-1.1, variable-font (400-700 weight in one file), with FULL Cyrillic coverage (Cyreal foundry, designed by Olga Karpushina with Cyrillic-Latin parity).

Per-face WOFF2 subsetting recipe (RESEARCH §1 + §6)

UNICODES='U+0020-007E,U+00A0-00FF,U+0131,U+0152-0153,U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116'

# Lora variable (Latin+Cyrillic; variable wght axis 400-700)
pyftsubset Lora-VariableFont_wght.ttf \
  --unicodes="$UNICODES" \
  --flavor=woff2 \
  --output-file=src/shared/fonts/Lora-VariableFont.woff2 \
  --layout-features='*' --no-hinting --desubroutinize

# Repeat for Lora-Italic-VariableFont_wght.ttf if upstream ships italic separately
# (A5 in RESEARCH Assumptions Log — verify at execute-plan time)

# Plex Sans 400/500/600/700 (each as a separate static-weight file)
for weight in Regular Medium SemiBold Bold; do
  pyftsubset "IBMPlexSans-${weight}.ttf" \
    --unicodes="$UNICODES" \
    --flavor=woff2 \
    --output-file="src/shared/fonts/IBMPlexSans-${weight}.woff2" \
    --layout-features='*' --no-hinting --desubroutinize
done

# Plex Mono 400/500
for weight in Regular Medium; do
  pyftsubset "IBMPlexMono-${weight}.ttf" \
    --unicodes="$UNICODES" \
    --flavor=woff2 \
    --output-file="src/shared/fonts/IBMPlexMono-${weight}.woff2" \
    --layout-features='*' --no-hinting --desubroutinize
done

Local-faced tokens.css @font-face replacement block (replaces line 12 Google Fonts @import)

/* ── Self-hosted fonts (MV3 CSP) ──
   Faces per D-05 with R2 Newsreader→Lora substitution (designer reply 2026-05-19).
   Subsetting: all faces are Latin (U+0020-007E + U+00A0-00FF) + Cyrillic basic
   (U+0400-045F + supplemental code points per pyftsubset recipe). All OFL-1.1
   (LICENSE-Lora.txt + LICENSE-IBM-Plex.txt next to this directory). */

/* Lora variable — display family with full Cyrillic (R2 substitute for Newsreader) */
@font-face {
  font-family: "Lora";
  src: url("./fonts/Lora-VariableFont.woff2") format("woff2-variations"),
       url("./fonts/Lora-VariableFont.woff2") format("woff2");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Lora";
  src: url("./fonts/Lora-Italic-VariableFont.woff2") format("woff2-variations"),
       url("./fonts/Lora-Italic-VariableFont.woff2") format("woff2");
  font-weight: 400 700;
  font-style: italic;
  font-display: swap;
}

/* IBM Plex Sans — UI body family with full Cyrillic */
@font-face {
  font-family: "IBM Plex Sans";
  src: url("./fonts/IBMPlexSans-Regular.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "IBM Plex Sans";
  src: url("./fonts/IBMPlexSans-Medium.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "IBM Plex Sans";
  src: url("./fonts/IBMPlexSans-SemiBold.woff2") format("woff2");
  font-weight: 600; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "IBM Plex Sans";
  src: url("./fonts/IBMPlexSans-Bold.woff2") format("woff2");
  font-weight: 700; font-style: normal; font-display: swap;
}

/* IBM Plex Mono — diagnostic / timer family with full Cyrillic */
@font-face {
  font-family: "IBM Plex Mono";
  src: url("./fonts/IBMPlexMono-Regular.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "IBM Plex Mono";
  src: url("./fonts/IBMPlexMono-Medium.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap;
}

NOTE: if upstream Lora ships normal+italic in a single multi-axis variable file (italic via ital axis), the italic @font-face rule above collapses into the normal one with font-style: oblique 0deg 14deg per the variable-italic syntax. Verify upstream Lora release structure during execute-plan; adjust @font-face block accordingly. RESEARCH §A5 calls this out as a verify-at-execute checkpoint.

.mks-word class (RESEARCH §8)

/* ── Lockup wordmark — referenced by mokosh-lockup.svg line 21 ── */
.mks-word {
  font-family: var(--mks-font-display);
  font-size: 32px;
  font-weight: var(--mks-weight-regular);
  letter-spacing: var(--mks-tracking-display);
  fill: var(--mks-ink-900);
}

Icon rasterization recipe (RESEARCH §3 verified locally)

#!/usr/bin/env bash
set -euo pipefail
SVG="src/shared/brand/mokosh-mark.svg"
for size in 16 48 128; do
  rsvg-convert -w "$size" -h "$size" "$SVG" -o "icons/icon${size}.png"
  bytes=$(stat -c%s "icons/icon${size}.png")
  echo "✓ icons/icon${size}.png (${bytes} bytes)"
  case $size in
    16)  [[ $bytes -ge 200  ]] || { echo "FAIL: icon16 < 200 B"; exit 1; } ;;
    48)  [[ $bytes -ge 500  ]] || { echo "FAIL: icon48 < 500 B"; exit 1; } ;;
    128) [[ $bytes -ge 1024 ]] || { echo "FAIL: icon128 < 1024 B"; exit 1; } ;;
  esac
done

messages.json schema (Chrome i18n; RESEARCH §10)

{
  "extName": {
    "message": "Mokosh — Session Capture",
    "description": "manifest.json:name; surfaces in chrome://extensions, Web Store, and right-click menus"
  },
  "tooltipRecPrefix": {
    "message": "Mokosh — recording",
    "description": "Prefix; the SW concatenates ' (MM:SS)' at runtime via formatElapsed(seconds)"
  }
}

JS-side read (synchronous; chrome.i18n.getMessage does NOT return a promise):

const cta = chrome.i18n.getMessage('popupSaveCta');  // returns '' on missing key
buttonText.textContent = cta;

Background SW migration shape (src/background/index.ts)

// REPLACES line 76: const BADGE_REC_COLOR = '#00C853';     // material green
const BADGE_REC_COLOR = '#b2543d';     // --mks-madder-600 per D-04 loom palette (RESEARCH §10 Open Question A7)

// REPLACES the const BADGE_*_TITLE block (lines 82-84) — read at call site instead:
function setBadgeState(state: BadgeState): void {
  let text: string;
  let color: string;
  let titleKey: 'tooltipOff' | 'tooltipRec' | 'tooltipErr';
  if (state === 'REC') {
    text = BADGE_REC_TEXT;
    color = BADGE_REC_COLOR;
    titleKey = 'tooltipRec';
  } else if (state === 'OFF') {
    text = BADGE_OFF_TEXT;
    color = BADGE_OFF_COLOR;
    titleKey = 'tooltipOff';
  } else {
    text = BADGE_ERROR_TEXT;
    color = BADGE_ERROR_COLOR;
    titleKey = 'tooltipErr';
  }
  const title = chrome.i18n.getMessage(titleKey);
  try { chrome.action.setBadgeText({ text }); } catch (e) { logger.warn('setBadgeText failed:', e); }
  try { chrome.action.setBadgeBackgroundColor({ color }); } catch (e) { logger.warn('setBadgeBackgroundColor failed:', e); }
  try { chrome.action.setTitle({ title }); } catch (e) { logger.warn('setTitle failed:', e); }
}

// REPLACES line 862-865 (RECORDING_ERROR recovery notification body):
chrome.notifications.create(recoveryId, {
  type: 'basic',
  iconUrl: chrome.runtime.getURL(NOTIFICATION_ICON_PATH),
  title: chrome.i18n.getMessage('extName'),
  message: chrome.i18n.getMessage('notifRecovery'),
  priority: 1,
});

// REPLACES line 961-965 (onStartup notification body):
chrome.notifications.create(notificationId, {
  type: 'basic',
  iconUrl: chrome.runtime.getURL(NOTIFICATION_ICON_PATH),
  title: chrome.i18n.getMessage('extName'),
  message: chrome.i18n.getMessage('notifStartup'),
  priority: 1,
});

Popup TS migration shape (src/popup/index.ts)

// in updateUI()
case 'idle':
  buttonText.textContent = chrome.i18n.getMessage('popupSaveCta');  // was 'Сохранить отчёт об ошибке'
  saveButton.className = 'save-button';
  saveButton.disabled = !popupState.hasPermissions;
  break;
case 'saving':
  buttonText.textContent = chrome.i18n.getMessage('popupSaving');  // new key OR keep 'Сохраняю...' as fallback
  break;
case 'done':
  buttonText.textContent = chrome.i18n.getMessage('popupSaveDoneShort');  // new key 'Готово! ✓'
  break;

// in saveArchive() success branch (was line 91):
statusMessage.textContent = chrome.i18n.getMessage('popupSaveDone');  // was 'Архив успешно сохранён!'

// in init() — populate data-mks-key elements (info-text + status):
function populateMksKeys(): void {
  document.querySelectorAll<HTMLElement>('[data-mks-key]').forEach((el) => {
    const key = el.dataset.mksKey;
    if (key) el.textContent = chrome.i18n.getMessage(key);
  });
}

Harness assertion A18-A22 shape (extends 01-13 pattern)

// tests/uat/extension-page-harness.ts (page-side)
async function assertA18(): Promise<AssertionRecord> {
  const checks: string[] = [];
  // Resolve the actual emitted asset URL from the loaded stylesheet (handles Vite hashing).
  const sheets = Array.from(document.styleSheets);
  let woff2Url: string | null = null;
  for (const sheet of sheets) {
    try {
      for (const rule of Array.from(sheet.cssRules)) {
        if (rule.constructor.name === 'CSSFontFaceRule') {
          const src = (rule as CSSFontFaceRule).style.getPropertyValue('src');
          const match = src.match(/url\(["']?([^"')]+Lora[^"')]+\.woff2)["']?\)/);
          if (match) { woff2Url = match[1]; break; }
        }
      }
    } catch { /* cross-origin sheets throw; skip */ }
    if (woff2Url) break;
  }
  if (!woff2Url) {
    return { passed: false, name: 'A18', checks, diagnostics: 'No Lora @font-face rule found in any stylesheet', error: '' };
  }
  checks.push(`A18.1: found Lora @font-face src=${woff2Url}`);
  const resp = await fetch(new URL(woff2Url, location.href).href);
  if (!resp.ok) return { passed: false, name: 'A18', checks, diagnostics: `fetch ${woff2Url} returned ${resp.status}`, error: '' };
  checks.push(`A18.2: fetch returned ${resp.status}`);
  const buf = await resp.arrayBuffer();
  if (buf.byteLength < 50000) return { passed: false, name: 'A18', checks, diagnostics: `Lora woff2 only ${buf.byteLength} B (expected >50000)`, error: '' };
  checks.push(`A18.3: byteLength=${buf.byteLength}`);
  return { passed: true, name: 'A18', checks, diagnostics: '' };
}

async function assertA20(): Promise<AssertionRecord> {
  const manifest = chrome.runtime.getManifest();
  if (manifest.name === 'Mokosh — Session Capture') {
    return { passed: true, name: 'A20', checks: [`A20.1: manifest.name='${manifest.name}'`], diagnostics: '' };
  }
  return { passed: false, name: 'A20', checks: [], diagnostics: `Expected 'Mokosh — Session Capture'; got '${manifest.name}'`, error: '' };
}

async function assertA21(): Promise<AssertionRecord> {
  // Create transient probe div with .mks-display-1 class
  const probe = document.createElement('div');
  probe.className = 'mks-display-1';
  probe.textContent = 'Probe';
  document.body.appendChild(probe);
  // Force layout so CSS resolves
  void probe.offsetHeight;
  const computed = window.getComputedStyle(probe).fontFamily;
  document.body.removeChild(probe);
  if (computed.startsWith('Lora') || computed.startsWith('"Lora"')) {
    return { passed: true, name: 'A21', checks: [`A21.1: fontFamily='${computed}'`], diagnostics: '' };
  }
  return { passed: false, name: 'A21', checks: [], diagnostics: `--mks-font-display did not resolve to Lora; computed='${computed}'`, error: '' };
}

Tier-1 forbidden-strings inventory (UNCHANGED at 12 post-Plan-01-14)

const FORBIDDEN_HOOK_STRINGS: ReadonlyArray<string> = [
  '__mokoshTest',
  'setCurrentStream',
  'setSegmentCountGetter',
  'installFakeDisplayMedia',
  'uninstallFakeDisplayMedia',
  'dispatchEndedOnTrack',
  'getSegmentCount',
  '__mokoshOffscreenQuery',
  'get-display-surface',
  'get-segment-count',
  'lastGetDisplayMediaConstraints',         // added by Plan 01-14
  'get-last-getDisplayMedia-constraints',   // added by Plan 01-14
];
// Plan 01-12 introduces NO new test-mode symbols (A18-A22 use production chrome.* APIs
// + fetch + getComputedStyle exclusively). Do NOT extend this list beyond the 12 entries
// established by Plan 01-14.

Locale-parity test shape

import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';

describe('_locales/ key parity', () => {
  const ru = JSON.parse(readFileSync('_locales/ru/messages.json', 'utf8'));
  const en = JSON.parse(readFileSync('_locales/en/messages.json', 'utf8'));
  it('every key in ru/messages.json exists in en/messages.json', () => {
    for (const k of Object.keys(ru)) expect(en).toHaveProperty(k);
  });
  it('every key in en/messages.json exists in ru/messages.json (symmetric)', () => {
    for (const k of Object.keys(en)) expect(ru).toHaveProperty(k);
  });
  it('every key has a non-empty .message string', () => {
    for (const k of Object.keys(en)) expect(typeof en[k].message).toBe('string');
    for (const k of Object.keys(en)) expect(en[k].message.length).toBeGreaterThan(0);
    for (const k of Object.keys(ru)) expect(typeof ru[k].message).toBe('string');
    for (const k of Object.keys(ru)) expect(ru[k].message.length).toBeGreaterThan(0);
  });
});
Wave 0 Task 1: Scaffold RED unit tests (TDD baseline) tests/build/tokens-adopted.test.ts, tests/build/fonts-present.test.ts, tests/build/icons-present.test.ts, tests/build/no-remote-fonts.test.ts, tests/i18n/manifest-i18n.test.ts, tests/i18n/locale-parity.test.ts - tokens-adopted: RED because src/shared/tokens.css does not yet exist + src/popup/style.css contains hex literals (#2563eb, #1d4ed8, ...). GREEN after Wave 1 + Wave 4. - fonts-present: RED because src/shared/fonts/*.woff2 do not exist. GREEN after Wave 1. - icons-present: RED-ish (existing placeholders DO clear floors: 574/1153/2615 > 200/500/1024 per `ls -la icons/`); the test should assert dimensions + signature + size FLOOR strictly. RED-FOR-DIMS today only if Wave 0 includes the dimension parse (the 16-bit-RGB placeholders ARE the right pixel dims). Test designed to be GREEN after Wave 2 (Loom mark rasterized via rsvg-convert produces 8-bit-RGBA per RESEARCH §3 — file content hash will change, but dim + floor checks pass before AND after Wave 2). Add explicit RED case: assert PNG color-type byte (offset 25) === 6 (RGBA) — placeholder is 2 (RGB) so this assertion RED today, GREEN after Wave 2 (rsvg-convert defaults to RGBA per RESEARCH §3 "PNG output is `8-bit/color RGBA, non-interlaced` per `file(1)`"). - no-remote-fonts: RED-vacuous today (no dist/ tokens.css exists). After Wave 1 + Wave 4 + npm run build, dist/ exists + tokens.css inside it must have ZERO `googleapis` matches. - manifest-i18n: RED today (manifest.name === 'AI Call Recorder' + no default_locale). GREEN after Wave 3. - locale-parity: RED today (no _locales/). GREEN after Wave 3. Create the six test files per the `` block patterns and the artifact-table `provides` summaries. Each test file follows the existing tests/background/no-test-hooks-in-prod-bundle.test.ts and tests/offscreen/codec-check.test.ts patterns for module structure (vitest describe/it + readFileSync + JSON.parse + regex checks).
Key implementation notes:

1. tests/build/tokens-adopted.test.ts — three vitest cases: (a) `src/shared/tokens.css` exists + parses (`readFileSync`); (b) `src/popup/style.css` contains the import line `@import '../shared/tokens.css'` OR `@import "../shared/tokens.css"`; (c) `src/popup/style.css` matches `/#[0-9a-fA-F]{3,8}/g` ZERO times. NO build dependency — runs on source files only.

2. tests/build/fonts-present.test.ts — single describe; iterate over the expected file list (Lora-VariableFont.woff2, Lora-Italic-VariableFont.woff2 [SKIP if upstream consolidates italic into normal — flag via a TODO comment per RESEARCH §A5], IBMPlexSans-Regular.woff2, IBMPlexSans-Medium.woff2, IBMPlexSans-SemiBold.woff2, IBMPlexSans-Bold.woff2, IBMPlexMono-Regular.woff2, IBMPlexMono-Medium.woff2). For each: assert existsSync + statSync.size > 0. Also assert src/shared/tokens.css contains each filename via substring match.

3. tests/build/icons-present.test.ts — for each of {16: 200, 48: 500, 128: 1024} → assert existsSync(`icons/icon${size}.png`) + statSync.size >= floor + first 8 bytes match `\\x89PNG\\r\\n\\x1a\\n`. Read bytes 16-19 (width) + 20-23 (height) as big-endian uint32 and assert each === size. Read byte 25 (color-type) and assert === 6 (RGBA per Wave 2 rsvg-convert output) — this is the RED-today-GREEN-after-Wave-2 case.

4. tests/build/no-remote-fonts.test.ts — mirrors tests/background/no-test-hooks-in-prod-bundle.test.ts execFile pattern: `execFile('npm', ['run', 'build'])` (gated by `process.env.SKIP_BUILD === '1'` escape hatch), then recursive readdirSync over `dist/`, then for each file read content + assert ZERO matches for `googleapis` AND `https://fonts`. Use a 60_000ms build timeout (PROD_BUILD_TIMEOUT_MS).

5. tests/i18n/manifest-i18n.test.ts — read manifest.json + parse. Assert: `manifest.name === '__MSG_extName__'`; `manifest.description === '__MSG_extDesc__'`; `manifest.default_locale === 'en'`; `manifest.action.default_title === '__MSG_tooltipOff__'`. Read _locales/en/messages.json + parse + assert extName.message === 'Mokosh — Session Capture' + extDesc.message === 'Thirty seconds ago, always at hand.'. Read _locales/ru/messages.json + parse + assert extName.message === 'Mokosh — Запись сессии' + extDesc.message === 'Тридцать секунд назад, всегда под рукой.'.

6. tests/i18n/locale-parity.test.ts — per the `<interfaces>` block; three describe blocks (ru→en parity; en→ru symmetric; every key has non-empty .message string).

Create the directories `tests/build/` and `tests/i18n/` (they do not yet exist — verified via `ls tests/`). Commit all six files in a single atomic commit.
npx vitest run tests/build tests/i18n 2>&1 | tail -20 && echo "Expected: 6 test files, all RED (or RED on at least one case each — fonts-present, tokens-adopted, no-remote-fonts, manifest-i18n, locale-parity all RED today; icons-present RED on color-type-byte case only)" - 6 test files exist at the documented paths - vitest discovers all 6 + reports them in `npx vitest run tests/build tests/i18n` - At least one case per file is RED today (per behavior above) - npm run build / build:test still pass (these tests do not change source code yet — the existing 100 vitest GREEN baseline (post-01-14) stays GREEN; only the 6 new tests are RED) - Atomic commit `test(01-12): wave-0 — scaffold RED unit tests for tokens / fonts / icons / no-remote-fonts / manifest-i18n / locale-parity` Wave 1 Task 1: Subset OFL fonts to src/shared/fonts/ src/shared/fonts/Lora-VariableFont.woff2, src/shared/fonts/Lora-Italic-VariableFont.woff2, src/shared/fonts/IBMPlexSans-Regular.woff2, src/shared/fonts/IBMPlexSans-Medium.woff2, src/shared/fonts/IBMPlexSans-SemiBold.woff2, src/shared/fonts/IBMPlexSans-Bold.woff2, src/shared/fonts/IBMPlexMono-Regular.woff2, src/shared/fonts/IBMPlexMono-Medium.woff2, src/shared/fonts/LICENSE-Lora.txt, src/shared/fonts/LICENSE-IBM-Plex.txt, src/shared/fonts/README.md, scripts/subset-fonts.sh Per RESEARCH §1 + §6 + the `` block subsetting recipe. Each WOFF2 file: - Lora-VariableFont.woff2: variable-axis weight 400-700, Latin + Cyrillic basic subset, expected 80-120KB - Lora-Italic-VariableFont.woff2: if upstream ships italic separately (A5 verify); otherwise document the consolidation in README.md and omit this file (and the corresponding @font-face rule in tokens.css) - IBMPlexSans-{Regular,Medium,SemiBold,Bold}.woff2: ~25KB each - IBMPlexMono-{Regular,Medium}.woff2: ~28KB each Expected total ~250-300KB per the R2 substitution adjustment to RESEARCH §1's ~216KB estimate. 1. Create scripts/subset-fonts.sh per the `` block recipe. Header comment documents the source URLs (github.com/IBM/plex/releases for Plex v1.1.0; for Lora: github.com/cyrealtype/Lora-Cyrillic OR the Google Fonts release page accessed via `curl`). Script downloads upstream TTFs into a scratch tmpdir (`mktemp -d`), runs pyftsubset per face with the UNICODES range from ``, then cleans up the tmpdir.
2. Execute scripts/subset-fonts.sh. Verify each produced file:
   - exists at the documented path
   - size > 0 (typical: Lora ~100KB; Plex faces ~25-28KB each)
   - is WOFF2 (first 4 bytes === 'wOF2' = 0x77 0x4F 0x46 0x32)
   Document any execute-time surprises (italic consolidation per A5, etc.) in a brief note in src/shared/fonts/README.md.

3. Create src/shared/fonts/LICENSE-Lora.txt by curling https://raw.githubusercontent.com/cyrealtype/Lora-Cyrillic/master/OFL.txt (or equivalent canonical Lora OFL source — verify upstream layout during execute). Include copyright lines per the actual upstream content.

4. Create src/shared/fonts/LICENSE-IBM-Plex.txt by curling https://raw.githubusercontent.com/IBM/plex/master/LICENSE.txt (verbatim copy).

5. Create src/shared/fonts/README.md per RESEARCH §7 template + the R2 substitution note. Document: what's bundled; upstream URLs; subsetting unicodes; license notes; regeneration recipe (`scripts/subset-fonts.sh`). Mention the R2 substitution (Newsreader → Lora) + the designer 2026-05-19 reply.

6. Verify tests/build/fonts-present.test.ts now passes (all 7 or 8 expected files exist).

NOTE on Lora italic: if upstream variable file includes italic via the `ital` axis (single multi-axis file), update scripts/subset-fonts.sh to subset that single file + adjust tokens.css (next task) to use `font-style: oblique 0deg 14deg` per CSS Fonts Module 4 variable-italic syntax. Document the decision in src/shared/fonts/README.md.
ls -la src/shared/fonts/ && stat -c '%n: %s bytes' src/shared/fonts/*.woff2 && head -3 src/shared/fonts/LICENSE-*.txt && head -10 src/shared/fonts/README.md && (npx vitest run tests/build/fonts-present.test.ts 2>&1 | tail -10) && echo "Wave 1.1 done — fonts present; total bundle:" && du -ch src/shared/fonts/*.woff2 | tail -1 - 7 or 8 WOFF2 files exist (8 if Lora italic separate; 7 if collapsed) - Each WOFF2 file is > 0 bytes + WOFF2 signature verified - LICENSE-Lora.txt + LICENSE-IBM-Plex.txt exist + non-empty - src/shared/fonts/README.md exists + lists all bundled faces + upstream URLs + R2 note - scripts/subset-fonts.sh exists + executable - Total bundle size between 200KB and 350KB (relaxed envelope for R2 italic variance) - tests/build/fonts-present.test.ts GREEN - Atomic commit `feat(01-12): wave-1 task-1 — self-host OFL font bundle (Lora + Plex Sans + Plex Mono; R2 designer reply 2026-05-19)` Wave 1 Task 2: Land canonical src/shared/tokens.css (R2 Lora substitution + .mks-word + CSP-safe @font-face) src/shared/tokens.css, src/shared/brand/mokosh-mark.svg, src/shared/brand/mokosh-lockup.svg src/shared/tokens.css becomes the canonical token system. Contents: - Verbatim copy of .planning/intel/design-incoming/system/bundle/mokosh-handoff/tokens.css (281 lines) with three surgical edits: 1. Line 12 Google Fonts @import REMOVED + replaced with the 7-rule (or 8-rule if Lora italic separate) @font-face block from `` 2. Line 76 `--mks-font-display: "Newsreader", "Iowan Old Style", "Times New Roman", serif;` REPLACED with `--mks-font-display: "Lora", "Iowan Old Style", "Times New Roman", serif;` per R2 designer reply 2026-05-19 3. .mks-word class added at end of file per the `` block definition - ZERO references to fonts.googleapis.com or any https:// URL - ZERO Newsreader font-family references (R2 substitution complete)
src/shared/brand/mokosh-mark.svg + mokosh-lockup.svg are verbatim copies of the design-incoming files (so the engineering source tree has its own copy decoupled from the intel/ ingestion location).
1. Copy .planning/intel/design-incoming/system/bundle/mokosh-handoff/tokens.css to src/shared/tokens.css (use `cp` or Read + Write). Apply the three edits: - Replace line 12 (the Google Fonts @import) with the local @font-face block from `` — wrap in a comment header noting MV3 CSP + R2 designer reply 2026-05-19 - Replace line 76 `--mks-font-display: "Newsreader"...` with `--mks-font-display: "Lora", "Iowan Old Style", "Times New Roman", serif;` — also update the comment at line 72 to note "Lora for display (Cyreal foundry; R2 substitute for Newsreader per designer reply 2026-05-19; full Cyrillic coverage)" - Append the .mks-word class block from `` at end of file with a clear header `/* ── Lockup wordmark — referenced by mokosh-lockup.svg line 21 ── */`
2. Create src/shared/brand/ directory; copy mokosh-mark.svg + mokosh-lockup.svg from .planning/intel/design-incoming/system/bundle/mokosh-handoff/assets/ to src/shared/brand/. The intel/ copies stay as the original handoff source-of-truth; src/shared/brand/ is the engineering working copy.

3. Verify src/shared/tokens.css:
   - `grep -c 'googleapis\\|https://fonts' src/shared/tokens.css` returns 0
   - `grep -c 'Newsreader' src/shared/tokens.css` returns 0
   - `grep -c '@font-face' src/shared/tokens.css` returns 7 (or 8 with Lora italic)
   - `grep '\\.mks-word' src/shared/tokens.css` finds the new class

4. Verify CSS-parses by spawning `npx vite build --mode test --config vite.test.config.ts` (dry-build) and confirming no warnings about tokens.css; OR by running a quick CSS-syntax check via `npx stylelint src/shared/tokens.css --config '{}'` if available; OR by inspection.

5. Confirm tests/build/no-remote-fonts.test.ts can now run (after a `npm run build` it will read dist/ and verify zero remote-font URLs). Run it with SKIP_BUILD=1 to defer the actual build to a later wave verification.
grep -c "googleapis\|https://fonts" src/shared/tokens.css && grep -c "Newsreader" src/shared/tokens.css && grep -c "@font-face" src/shared/tokens.css && grep -c "\.mks-word" src/shared/tokens.css && grep -c "Lora" src/shared/tokens.css && ls -la src/shared/brand/ && npx vitest run tests/build/tokens-adopted.test.ts 2>&1 | tail -10 - src/shared/tokens.css exists + has 0 occurrences of `googleapis` / `https://fonts` / `Newsreader` - src/shared/tokens.css has 7 (or 8) `@font-face` rule occurrences - src/shared/tokens.css contains `.mks-word` class definition - src/shared/tokens.css contains `Lora` font-family reference (≥1) - src/shared/brand/mokosh-mark.svg + mokosh-lockup.svg exist + are byte-identical copies of intel/ originals - tests/build/tokens-adopted.test.ts case (a) GREEN (tokens.css exists); cases (b)+(c) still RED until Wave 4 (popup/style.css doesn't yet import tokens.css) - Atomic commit `feat(01-12): wave-1 task-2 — canonical src/shared/tokens.css (R2 Lora substitution + .mks-word + local @font-face block)` Wave 2 Task 1: Rasterize Loom brand mark to icons/icon{16,48,128}.png icons/icon16.png, icons/icon48.png, icons/icon128.png, scripts/rasterize-icons.sh Rasterize src/shared/brand/mokosh-mark.svg to icons/icon{16,48,128}.png via rsvg-convert (verified locally at RESEARCH §3: 16=406B, 48=784B, 128=1952B — all clear assets-spec.md FLOOR). The Plan 01-09 Path A placeholder icons (16-bit-RGB dark-square + green-dot) are OVERWRITTEN.
Per D-06 (Neutral mark + dynamic badge): single neutral mark per size; NO per-state PNG sets (the dynamic chrome.action.setBadgeBackgroundColor in src/background/index.ts does state communication).
1. Create scripts/rasterize-icons.sh per the `` block recipe (with the embedded size-floor sanity checks). Make it executable (`chmod +x`).
2. Execute scripts/rasterize-icons.sh. Verify each produced PNG:
   - exists at icons/icon{16,48,128}.png
   - file(1) reports "PNG image data, NxN, 8-bit/color RGBA, non-interlaced" (RESEARCH §3 verified)
   - size meets the FLOOR: 16≥200B, 48≥500B, 128≥1024B
   - byte 25 (color-type) === 6 (RGBA) — this is the case Wave 0 Task 1's icons-present test asserts

3. Verify the OVERWRITE happened: the prior placeholders were `16-bit/color RGB` (per `file icons/icon*.png` at plan-write time); the new files MUST be `8-bit/color RGBA`. This is a load-bearing behavioral change confirming the new rasterized mark is in place.

4. Re-verify all Plan 01-13 harness assertions still pass — specifically A9 (icon file sizes meet imageUtil floors per RESEARCH §3 verification) and A8 (Bug A regression: chrome.notifications.create succeeds with the new icon).

Run npx vitest run tests/build/icons-present.test.ts; expect GREEN.
file icons/icon*.png && stat -c '%n: %s bytes' icons/icon*.png && npx vitest run tests/build/icons-present.test.ts 2>&1 | tail -10 && head -c 8 icons/icon16.png | xxd - icons/icon16.png ≥ 200 B + 16×16 + 8-bit RGBA - icons/icon48.png ≥ 500 B + 48×48 + 8-bit RGBA - icons/icon128.png ≥ 1024 B + 128×128 + 8-bit RGBA - scripts/rasterize-icons.sh exists + executable - tests/build/icons-present.test.ts GREEN (all cases including the color-type RGBA case) - Plan 01-13 harness A8 + A9 still GREEN (re-run `npm run test:uat -- --only=A8 --only=A9` if standalone selector supported; otherwise run full suite and confirm 15+/15+ GREEN) - Atomic commit `feat(01-12): wave-2 task-1 — rasterize Loom mark to icons/{16,48,128}.png (overwrites Bug A placeholders)` Wave 3 Task 1: Land _locales/{en,ru}/messages.json + migrate manifest.json to __MSG_*__ + default_locale _locales/en/messages.json, _locales/ru/messages.json, manifest.json Two messages.json files at _locales/en/ + _locales/ru/ carry the 12-key matrix per RESEARCH §10 + Brief §02 verbatim + D-07 user override + D-08 tagline. manifest.json migrates to i18n with __MSG_extName__ + __MSG_extDesc__ + default_locale='en' + action.default_title='__MSG_tooltipOff__'. On extension reload after this wave, chrome://extensions displays "Mokosh — Session Capture" (D-07) — verifies the end-to-end i18n wiring.
Test gates: tests/i18n/manifest-i18n.test.ts + tests/i18n/locale-parity.test.ts flip GREEN.
1. Create _locales/ + _locales/en/ + _locales/ru/ directories (do not yet exist).
2. Create _locales/en/messages.json with the 12 keys per RESEARCH §10 + D-07 override + D-08 tagline (using brand-decisions-v1.md "always at hand." wording per RESEARCH Open Questions #4 default-action). Each key is a `{ "message": "<text>", "description": "<context for translators>" }` shape per Chrome i18n schema:

   - extName: "Mokosh — Session Capture"
   - extDesc: "Thirty seconds ago, always at hand."
   - tooltipOff: "Mokosh — click to start recording"
   - tooltipRecPrefix: "Mokosh — recording"
   - tooltipErr: "Mokosh — recording error, click to recover"
   - popupSavePrompt: "Save error report?"
   - popupSaveCta: "Save report"
   - popupSaveDone: "Archive saved to Downloads."
   - popupInfoText: "Last 30 s video + 10 min log"
   - notifStartup: "Recording started. I'm watching the last 30 seconds."
   - notifRecovery: "Recording resumed. Buffer refilling."
   - welcomeHeroRu: (RU primary; same as RU file's entry — used as the Russian-quoted hero string on welcome.html under EN locale)
   - welcomeHeroEn: "Thirty seconds ago, always at hand."

3. Create _locales/ru/messages.json with the same 12 keys + Russian text per RESEARCH §10 Brief §02 verbatim extraction:

   - extName: "Mokosh — Запись сессии"
   - extDesc: "Тридцать секунд назад, всегда под рукой."
   - tooltipOff: "Mokosh — щёлкните, чтобы начать запись"
   - tooltipRecPrefix: "Mokosh — идёт запись"
   - tooltipErr: "Mokosh — ошибка записи, щёлкните для восстановления"
   - popupSavePrompt: "Сохранить отчёт об ошибке?"
   - popupSaveCta: "Сохранить отчёт"
   - popupSaveDone: "Архив сохранён в Загрузки."
   - popupInfoText: "Последние 30 сек видео + 10 мин лога"
   - notifStartup: "Запись запущена. Я слежу за последними 30 секундами."
   - notifRecovery: "Запись возобновлена. Буфер снова заполняется."
   - welcomeHeroRu: "Тридцать секунд назад, всегда под рукой."
   - welcomeHeroEn: "Thirty seconds ago, always at hand."

Each key gets a `description` field carrying the surface + production call site (e.g. for tooltipRecPrefix: "Suffix '(MM:SS)' appended at runtime by the SW via formatElapsed(seconds)").

4. Update manifest.json (Read → Edit):
   - `name` 'AI Call Recorder' → `__MSG_extName__`
   - `description` 'Запись сессий операторов для диагностики ошибок' → `__MSG_extDesc__`
   - Add `default_locale` field with value `"en"` (between version and permissions, or wherever idiomatic per current shape)
   - Add `default_title` field to `action` (alongside default_popup + default_icon) with value `"__MSG_tooltipOff__"`
   - Verify the trailing JSON is valid (`JSON.parse(readFileSync('manifest.json', 'utf8'))` succeeds)

5. Run `npm run build`. Verify:
   - `dist/manifest.json` carries the new shape (crxjs writes its own manifest copy)
   - `dist/_locales/en/messages.json` + `dist/_locales/ru/messages.json` exist (crxjs propagates the _locales/ directory verbatim)
   
   NB: if crxjs's manifest-rewriting clobbers the i18n placeholders, document the issue + use the public/ directory fallback per RESEARCH §2 Pattern B (place _locales/ in public/_locales/ instead).

6. Verify tests/i18n/manifest-i18n.test.ts + tests/i18n/locale-parity.test.ts both GREEN.

7. Manual sanity: load the extension unpacked + observe chrome://extensions name field reads "Mokosh — Session Capture" (in English Chrome) or "Mokosh — Запись сессии" (in Russian Chrome). Document the observed display name in the commit body.
cat manifest.json | python3 -c 'import sys,json;m=json.load(sys.stdin);assert m["name"]=="__MSG_extName__",m["name"];assert m["description"]=="__MSG_extDesc__",m["description"];assert m["default_locale"]=="en";assert m["action"]["default_title"]=="__MSG_tooltipOff__";print("manifest OK")' && cat _locales/en/messages.json | python3 -c 'import sys,json;m=json.load(sys.stdin);assert m["extName"]["message"]=="Mokosh — Session Capture";assert m["extDesc"]["message"]=="Thirty seconds ago, always at hand.";print("en/messages.json OK")' && cat _locales/ru/messages.json | python3 -c 'import sys,json;m=json.load(sys.stdin);assert m["extName"]["message"]=="Mokosh — Запись сессии";assert m["extDesc"]["message"]=="Тридцать секунд назад, всегда под рукой.";print("ru/messages.json OK")' && npx vitest run tests/i18n/manifest-i18n.test.ts tests/i18n/locale-parity.test.ts 2>&1 | tail -10 - _locales/en/messages.json + _locales/ru/messages.json exist + parse + each carry the 12 keys with the canonical values - manifest.json:name === '__MSG_extName__'; :description === '__MSG_extDesc__'; :default_locale === 'en'; :action.default_title === '__MSG_tooltipOff__' - npm run build succeeds + dist/_locales/{en,ru}/messages.json exist + dist/manifest.json carries the new i18n shape - tests/i18n/manifest-i18n.test.ts + tests/i18n/locale-parity.test.ts GREEN - Existing 100 vitest baseline (post-01-14) + 6 new tests = 106 vitest GREEN - Atomic commit `feat(01-12): wave-3 task-1 — manifest i18n (__MSG_*__ + default_locale='en') + _locales/{en,ru}/messages.json (12 keys; D-07 + D-08 baked in)` Wave 4 Task 1: Migrate src/popup/ + src/background/ to tokens.css palette + chrome.i18n.getMessage src/popup/index.html, src/popup/index.ts, src/popup/style.css, src/background/index.ts src/popup/style.css imports tokens.css + replaces every hex literal with var(--mks-*) reference. src/popup/index.html + src/popup/index.ts populate operator-facing strings via chrome.i18n.getMessage. src/background/index.ts migrates badge titles + notification copy to chrome.i18n.getMessage + updates BADGE_REC_COLOR to madder (--mks-madder-600 = #b2543d per D-04). All existing vitest tests stay GREEN (chrome.i18n is a synchronous API; existing chrome.* mocks need a getMessage stub returning the key as the value for round-trip identity). 1. Update src/popup/style.css per the artifact-table `provides` summary: - Add `@import "../shared/tokens.css";` at top (above `* { margin: 0; ...}`) - body { background: var(--mks-surface); color: var(--mks-fg-1); font-family: var(--mks-font-ui); } - .save-button { background: var(--mks-rec); color: var(--mks-fg-inverse); border-radius: var(--mks-radius-md); box-shadow: var(--mks-shadow-1); transition: all var(--mks-dur-base) var(--mks-ease-out); } - .save-button:hover:not(:disabled) { background: var(--mks-madder-700); box-shadow: var(--mks-shadow-2); } - .save-button:disabled { background: var(--mks-ink-300); } - .save-button.saving { background: var(--mks-amber-600); } - .save-button.done { background: var(--mks-moss-600); } - .info-text { color: var(--mks-fg-2); font-family: var(--mks-font-ui); } - .status-message.error { color: var(--mks-error); } - .status-message.success { color: var(--mks-success); } - .status-message.info { color: var(--mks-warning); } - ZERO hex literals (regex /#[0-9a-fA-F]{3,8}/ match count === 0; verified by tests/build/tokens-adopted.test.ts case (c))
2. Update src/popup/index.html per artifact-table:
   - <title>Mokosh — Session Capture</title> (literal English title — chrome doesn't substitute __MSG__ in HTML body text or <title>; popup's title is a sensible default for non-RU locales). Document the limitation in a comment.
   - <span class="button-text"></span> — empty (populated by JS); remove the hardcoded 'Сохранить отчёт об ошибке'
   - <p class="info-text" data-mks-key="popupInfoText"></p> — empty + data-mks-key attribute; remove the hardcoded 'Последние 30 сек видео + 10 мин лога'

3. Update src/popup/index.ts per artifact-table:
   - In init(): call populateMksKeys() helper that loops over [data-mks-key] elements and sets each textContent = chrome.i18n.getMessage(el.dataset.mksKey || '')
   - In updateUI() idle case: buttonText.textContent = chrome.i18n.getMessage('popupSaveCta')
   - In updateUI() saving case: buttonText.textContent = chrome.i18n.getMessage('popupSaving') (new key — add to messages.json with 'Сохраняю...' RU / 'Saving...' EN; OR fallback to literal if missing — document choice)
   - In updateUI() done case: buttonText.textContent = chrome.i18n.getMessage('popupSaveDoneShort') (new key — 'Готово! ✓' RU / 'Done ✓' EN) OR retain literal with a TODO
   - In updateUI() empty-state branch: statusMessage.textContent = chrome.i18n.getMessage('popupEmptyState') (new key — 'Откройте запись через иконку расширения' RU / 'Open recording via the extension icon' EN)
   - In saveArchive() success branch: statusMessage.textContent = chrome.i18n.getMessage('popupSaveDone')
   
   Add the new keys (popupSaving, popupSaveDoneShort, popupEmptyState) to BOTH _locales/en/messages.json AND _locales/ru/messages.json. Re-verify locale-parity.test.ts.

4. Update src/background/index.ts per the `<interfaces>` shape block:
   - Line 76 BADGE_REC_COLOR: '#00C853' → '#b2543d' with comment '// --mks-madder-600 per D-04 loom palette (Plan 01-12)'
   - BADGE_OFF_COLOR ('#D32F2F') stays (no loom-palette token for OFF) OR change to var(--mks-ink-700) hex literal '#2f3349'; document choice in commit
   - BADGE_ERROR_COLOR ('#F9A825') stays (no loom-palette token for warning amber) OR change to '#c98b3a' (= --mks-amber-600); document choice in commit
   - Lines 82-84 BADGE_*_TITLE constants: KEEP as fallback strings (for any chrome.* stub contexts) BUT replace the runtime read in setBadgeState with `chrome.i18n.getMessage(<key>) || <fallback_const>` so the existing vitest tests that don't mock chrome.i18n still pass via fallback
   - Line 862-865 RECOVERY notification body: title=chrome.i18n.getMessage('extName') || 'Mokosh', message=chrome.i18n.getMessage('notifRecovery') || 'Recording resumed.'
   - Line 961-965 STARTUP notification body: title=chrome.i18n.getMessage('extName') || 'Mokosh', message=chrome.i18n.getMessage('notifStartup') || 'Recording started.'

   Use the `|| fallback` pattern uniformly so chrome.i18n unavailability (in unit-test mocks lacking i18n) degrades to literal English-ish text rather than the empty string Chrome returns for missing keys.

5. Update relevant existing vitest tests that may need a chrome.i18n.getMessage stub:
   - tests/background/badge-state-machine.test.ts: if setBadgeState now reads chrome.i18n.getMessage, the chromeStub needs `i18n: { getMessage: vi.fn((k: string) => k) }` so the test asserts title === <key-name> as an identity-passthrough. Adjust assertions accordingly.
   - tests/background/onstartup-notification.test.ts: same — add i18n.getMessage stub.
   - tests/background/save-archive-stops-recording.test.ts: same if it asserts notification copy.
   
   The fallback `|| <const>` pattern reduces the need for chrome.i18n stubs in tests that don't depend on the displayed text; tests that DO depend on text shape need the identity-passthrough stub.

6. Run `npm test` — expect 106+ GREEN (100 existing post-01-14 + 6 new tests + any new copy in popup tests adjusted via chrome.i18n stub).

7. Run `npm run build` then re-run `tests/build/no-remote-fonts.test.ts` — expect GREEN (dist/tokens.css contains zero `googleapis` / `https://fonts`).
grep -c "#[0-9a-fA-F]\{3,8\}" src/popup/style.css && grep -c "var(--mks-" src/popup/style.css && grep -c "chrome.i18n.getMessage" src/popup/index.ts && grep -c "chrome.i18n.getMessage" src/background/index.ts && grep -n "BADGE_REC_COLOR.*#b2543d\|--mks-madder-600" src/background/index.ts && npx vitest run 2>&1 | tail -10 && npm run build 2>&1 | tail -5 && npx vitest run tests/build/no-remote-fonts.test.ts 2>&1 | tail -10 - src/popup/style.css contains 0 hex literals + ≥ 8 var(--mks-*) references + @import of tokens.css - src/popup/index.html + src/popup/index.ts use chrome.i18n.getMessage at every operator-facing copy site (verified by grep ≥ 4 occurrences in popup/index.ts) - src/background/index.ts uses chrome.i18n.getMessage at every operator-facing copy site (badge titles + 2 notification call sites; verified by grep ≥ 4 occurrences); BADGE_REC_COLOR === '#b2543d' - tests/build/tokens-adopted.test.ts ALL cases GREEN (including (b)+(c) which were RED in Wave 0) - tests/build/no-remote-fonts.test.ts GREEN (after npm run build) - vitest baseline 106+ GREEN (existing 100 post-01-14 + 6 new); no regressions in tests/background/* - Atomic commit `feat(01-12): wave-4 task-1 — adopt tokens.css + chrome.i18n.getMessage in src/popup/ + src/background/ (loom palette + RU i18n + en fallback)` Wave 5 Task 1: Welcome page conditional migration + Vite __VITE_DEV__ define-token + scripts/README.md src/welcome/welcome.css, src/welcome/welcome-tokens.css, src/welcome/welcome.ts, src/welcome/copy.ts, vite.config.ts, scripts/README.md If Plan 01-10 has landed (src/welcome/ exists at execute-plan time): migrate welcome-tokens.css → @import '../shared/tokens.css' (one-liner) AND migrate copy.ts to chrome.i18n.getMessage reads for welcomeHeroRu + welcomeHeroEn + any other welcome keys.
If Plan 01-10 has NOT landed: skip all src/welcome/* changes; Plan 01-10 executor uses src/shared/tokens.css directly when 01-10 lands later (no placeholder welcome-tokens.css needed in that path).

Unconditionally: add __VITE_DEV__ define-token to vite.config.ts (RESEARCH §12 D-09 spirit-satisfaction) + create scripts/README.md documenting the smoke-isolation invariant.
1. Detect Plan 01-10 state: `ls src/welcome/welcome.html 2>/dev/null && echo "01-10 landed" || echo "01-10 not yet"`. Branch on result.
2A. IF Plan 01-10 has landed:
   - Read src/welcome/welcome-tokens.css. Replace contents with `@import '../shared/tokens.css';\n` (single line file). NB: per RESEARCH §9 anti-pattern, do NOT create per-surface mini-tokens — collapse into the canonical tokens.css.
   - Read src/welcome/welcome.css. Verify it uses var(--mks-*) references throughout (per 01-10 design-swap-ready architecture; no hex literals expected). Add a comment header noting "tokens.css now canonical via welcome-tokens.css → @import."
   - Read src/welcome/copy.ts. Reduce to a fallback shim: export `function getCopy(key: string): string { const i18n = chrome.i18n?.getMessage?.(key); return i18n && i18n.length > 0 ? i18n : COPY[key] || ''; }` where COPY is the existing static map (retained as the safety net). Update src/welcome/welcome.ts to call `getCopy()` instead of `COPY[key]` directly.
   - Add to _locales/en/messages.json + _locales/ru/messages.json the welcome keys that copy.ts exposes (welcomeHeroTitle, welcomeBodyExplainerLine1, welcomeBodyExplainerLine2, welcomeBodyCtaToolbar, welcomeFooterPrivacy, etc. — per 01-10-PLAN.md src/welcome/copy.ts artifact list). Re-verify locale-parity.test.ts GREEN.
   - Re-run 01-10's Plan 01-10 onboarding tests + harness A17 (welcome.css zero-hex regex + COPY[ substring check): assert A17's regex-based welcome.css check still GREEN (welcome.css continues to have zero hex literals).

2B. IF Plan 01-10 has NOT landed:
   - Skip all src/welcome/* file modifications. Document this in the commit body: "Plan 01-10 not yet landed; welcome migration deferred. When 01-10 lands, its executor uses src/shared/tokens.css directly (skipping the placeholder welcome-tokens.css step entirely)." Add a paragraph to .planning/STATE.md noting the deferral so 01-10's planner can adjust.

3. Unconditionally update vite.config.ts: in the existing `define` block, add `__VITE_DEV__: JSON.stringify(process.env.VITE_DEV === '1')` alongside the existing `__MOKOSH_UAT__: 'false'`. Add an inline comment per RESEARCH §12 explaining the defensive intent ("reserved for any future inline smoke-mode check; currently smoke lives entirely in smoke.sh which is outside Vite's input set — verified by `grep -rn 'smoke\\|SMOKE\\|data:text/html' src/` returning empty").

4. Verify vite.test.config.ts mergeConfig propagates __VITE_DEV__ (no explicit override needed — mergeConfig spreads define fields).

5. Create scripts/README.md (single paragraph per RESEARCH §12): document that smoke.sh + future dev-only scripts live in this directory + are NOT bundled by npm run build + the production dist/ contains no smoke artifacts. Cross-reference RESEARCH.md §12 for the no-op-confirmation rationale.

6. Run npm test + npm run build + npm run build:test — expect all GREEN baseline (100 post-01-14) + 6 new tests = 106 GREEN.

7. (Optional but recommended per RESEARCH §12 + the existing Tier-1 gate pattern): add a `tests/build/no-smoke-in-dist.test.ts` test asserting `grep -rn smoke dist/` returns 0 matches post-build. RED today if any smoke string leaks; GREEN per current architecture. Keep this OPTIONAL — if context budget tight, defer to a follow-up plan.
(ls src/welcome/welcome.html 2>/dev/null && echo "Plan 01-10 landed — checking welcome migration:" && grep -c "tokens.css" src/welcome/welcome-tokens.css && grep -c "chrome.i18n" src/welcome/welcome.ts) || echo "Plan 01-10 not landed — skipped welcome migration" && grep -c "__VITE_DEV__" vite.config.ts && ls -la scripts/README.md && npx vitest run 2>&1 | tail -10 - IF 01-10 landed: src/welcome/welcome-tokens.css === single @import line; welcome.ts uses chrome.i18n.getMessage via getCopy() shim - IF 01-10 NOT landed: src/welcome/* unchanged; STATE.md gains deferral note - vite.config.ts:define block contains __VITE_DEV__ entry - scripts/README.md exists + documents smoke-isolation invariant - vitest baseline 106+ GREEN (+ N welcome keys added to messages.json if 01-10 landed; locale-parity stays GREEN) - Atomic commit `feat(01-12): wave-5 task-1 — welcome i18n migration (conditional on 01-10) + __VITE_DEV__ define + scripts/README.md` Wave 6 Task 1: Extend UAT harness with A18-A22 (font / icon / manifest-i18n / Lora-resolved / welcome-tokens) tests/uat/extension-page-harness.ts, tests/uat/lib/harness-page-driver.ts, tests/uat/harness.test.ts The 01-13 + 01-14 harness gains 5 new assertions following the established Approach-B pattern (page-side assertA* + host-side driveA* wrapper + harness.test.ts orchestrator entry). Each assertion uses production chrome.* APIs + fetch + getComputedStyle (no new test-mode symbols). After this wave: `npm run test:uat` reports 21/21 GREEN (or 24/24 with 01-10's A15-A17 already in place). A0-A14 (Plan 01-13) and A23 (Plan 01-14) remain GREEN as no-regression guarantees.
Tier-1 FORBIDDEN_HOOK_STRINGS list stays at 12 entries post-Plan-01-14 (no new test-mode surface introduced by Plan 01-12).
1. Extend tests/uat/extension-page-harness.ts: add 5 page-side methods to window.__mokoshHarness per the `` block shapes: - assertA18(): walks document.styleSheets for the first @font-face rule whose src URL contains 'Lora'; resolves the URL relative to location.href; fetches it; asserts response.ok + arrayBuffer().byteLength >= 50000. Returns AssertionRecord with structured checks (A18.1 found rule; A18.2 fetch status; A18.3 byteLength). - assertA19(): fetches chrome.runtime.getURL('icons/icon128.png'); reads arrayBuffer; computes a simple identity check (first 32 bytes byte-by-byte comparison against a hardcoded snapshot of the Bug A placeholder file's first 32 bytes); returns A19.1: "icon differs from Bug A placeholder fingerprint". Stores the placeholder fingerprint as a const at top of the file with a comment referencing this plan + the verification command to refresh it. - assertA20(): chrome.runtime.getManifest().name === 'Mokosh — Session Capture'; structured AssertionRecord with A20.1 manifest.name === '...'. - assertA21(): creates a transient div, applies class='mks-display-1', reads getComputedStyle.fontFamily, asserts startsWith 'Lora' or '"Lora"'. Returns A21.1: fontFamily resolved. - assertA22(): fetches chrome.runtime.getURL('src/welcome/welcome.html'); if status === 404, returns PASS with diagnostic "Plan 01-10 not landed; A22 skipped"; if status === 200, also fetches the linked welcome.css (extract from the HTML) and asserts welcome.css contains either `tokens.css` import OR substantive var(--mks-*) usage (regex /var\\(--mks-[a-z-]+\\)/ match count >= 3).
2. Extend tests/uat/lib/harness-page-driver.ts: add driveA18 / driveA19 / driveA20 / driveA21 / driveA22 wrappers following the established driveA4 pattern (page.evaluate(() => window.__mokoshHarness.assertXX())). No new host-side fs / ffprobe / zip primitives.

3. Extend tests/uat/harness.test.ts: import driveA18..A22; add entries to the orchestrator's assertion list in sequence (after A14, or A17 if 01-10 landed, with A23 from 01-14 elsewhere in the list). Keep FORBIDDEN_HOOK_STRINGS UNCHANGED at 12 entries (the 01-14-established baseline) — verify by `wc -l` diff before/after this task.

4. Run npm run build:test (rebuilds dist-test/). Then run `npm run test:uat` (or just `tsx tests/uat/harness.test.ts`). Expected output: 21/21 (or 24/24) GREEN.

5. Run `npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts` — confirm Tier-1 grep gate GREEN (no new hook strings leaked into dist/).
grep -c "assertA1[89]\|assertA2[012]" tests/uat/extension-page-harness.ts && grep -c "driveA1[89]\|driveA2[012]" tests/uat/lib/harness-page-driver.ts && grep -c "driveA1[89]\|driveA2[012]" tests/uat/harness.test.ts && (wc -l tests/uat/harness.test.ts && grep "FORBIDDEN_HOOK_STRINGS" tests/uat/harness.test.ts -A 15 | grep -v "^#" | grep -c "'") && npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts 2>&1 | tail -5 && (npm run test:uat 2>&1 | tail -25 || echo "test:uat invocation needs verification; check for 21/21 or 24/24 GREEN") - 5 new assertA* methods on window.__mokoshHarness (verified by grep ≥ 5) - 5 new driveA* wrappers in harness-page-driver.ts - harness.test.ts orchestrator runs all 20 (or 23 with 01-10) assertions sequentially - npm run test:uat exits 0 with 21/21 (or 24/24) GREEN - Tier-1 forbidden-strings inventory in no-test-hooks-in-prod-bundle.test.ts UNCHANGED at 12 entries post-Plan-01-14 (no new test-mode symbols added by Plan 01-12) - Atomic commit `feat(01-12): wave-6 task-1 — harness A18-A22 (font reachability + icon-distinct + manifest-i18n + Lora-resolved + welcome-tokens)` Wave 7 Checkpoint: Operator empirical brand-fit ack (operator-driven; no source files modified by this checkpoint — Wave 1-6 already landed the artifacts) See below — operator-driven empirical check against fresh build loaded in Chrome. The executor agent must NOT bypass this checkpoint by stubbing; brand fit is the LAST Phase 1 operator gate and must be confirmed against the real branded extension. echo "checkpoint:human-verify — see how-to-verify section; resume signal is the gate" Operator types 'approved' after running the how-to-verify steps. See . Plan 01-12 lands the full design integration: Lora self-hosted (R2 designer reply); IBM Plex Sans + Mono self-hosted (Latin + Cyrillic); src/shared/tokens.css canonical (Newsreader → Lora substitution + .mks-word class + zero remote @import); icons rasterized from Loom mark (replaces Bug A placeholders); manifest:name + :description + :default_title migrated to chrome i18n (__MSG_*__ + _locales/{en,ru}/messages.json); 12 i18n keys land with D-07 + D-08 + Brief §02 verbatim Russian copy; src/popup/ + src/background/ use chrome.i18n.getMessage at every operator-facing site + tokens.css palette; BADGE_REC_COLOR madder-rust per D-04; welcome page (if 01-10 landed) inherits canonical tokens; vite __VITE_DEV__ defensive define; UAT harness 21/21 (or 24/24) GREEN with A18-A22 covering font reachability + icon-distinct + manifest-i18n + Lora-resolved + welcome-tokens (A0-A14 from 01-13 and A23 from 01-14 remain GREEN as no-regression guarantees). 1. Fresh build + load: - `rm -rf dist/ dist-test/` - `npm run build` - Open `chrome://extensions/`, toggle Developer mode, click "Load unpacked", select the `dist/` directory. - Verify: extension name shows "Mokosh — Session Capture" (en locale) or "Mokosh — Запись сессии" (ru locale). The extension's icon in the toolbar is the rasterized Loom mark (4×4-grid silhouette inside a rounded square), NOT the prior dark-square + green-dot placeholder.
2. Popup brand fit:
   - Click the Mokosh toolbar icon to open the popup.
   - Verify: popup background is warm-linen (--mks-surface = #faf7f1), NOT cool-gray; SAVE button is madder-rust (--mks-rec = #b2543d), NOT material-blue; info text is warm gray (--mks-fg-2 = #5b5f76); copy reads "Сохранить отчёт" (RU) or "Save report" (EN) per chrome's resolved locale.

3. Russian copy rendering in Lora:
   - Switch Chrome to Russian locale (chrome://settings/languages — add Russian + move to top + restart).
   - Re-open the Mokosh popup. Verify Russian strings render correctly in IBM Plex Sans (UI body) — text reads "Сохранить отчёт", "Архив сохранён в Загрузки.", etc.
   - The display-register text (if any visible in popup) renders in Lora — verify via DevTools → Inspect → Computed → font-family (should read 'Lora' as the matched family for any .mks-display-* class). For now, popup doesn't have visible display-register text; the load-bearing display-register check happens on the welcome page (if 01-10 landed) OR by spawning a temporary `chrome-extension://<id>/src/welcome/welcome.html` if available.

4. Manifest name in chrome://extensions:
   - Switch back to English Chrome (or stay in RU for parallel verification).
   - chrome://extensions/ shows "Mokosh — Session Capture" (en) or "Mokosh — Запись сессии" (ru) in the extension card.

5. Notification icon + copy:
   - Trigger a recording start (click toolbar — toolbar:onClicked → startVideoCapture → eventually notification).
   - Verify: startup notification icon is the rasterized Loom mark (not placeholder); copy reads "Запись запущена. Я слежу за последними 30 секундами." (ru) / "Recording started. I'm watching the last 30 seconds." (en).

6. Welcome page (if 01-10 landed):
   - Navigate to chrome-extension://<id>/src/welcome/welcome.html in a tab.
   - Verify: welcome hero renders D-08 tagline in Lora; warm-linen background; mark slot shows the Loom mark; Russian + English parallel-text copy renders correctly.

7. Harness gate:
   - `npm run test:uat` exits 0 with 21/21 GREEN (or 24/24 with 01-10).
   - Tier-1 grep gate GREEN: `npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts`.

8. Type "approved" if brand fit accepted, OR describe specific issues for follow-up.
Type "approved" or describe issues for follow-up Wave 7 Task 1: Plan closure (SUMMARY + STATE.md + ROADMAP.md sync) .planning/phases/01-stabilize-video-pipeline/01-12-SUMMARY.md, .planning/STATE.md, .planning/ROADMAP.md 1. Create .planning/phases/01-stabilize-video-pipeline/01-12-SUMMARY.md following the template at $HOME/.claude/get-shit-done/templates/summary.md AND mirroring the structural choices of 01-13-SUMMARY.md (frontmatter with phase/plan/subsystem/tags/requires/provides/affects/tech-stack/key-files/decisions/metrics; body sections One-Liner / What Landed by Wave / Test Counts / Deviations from Plan / Architectural Notes Worth Carrying Forward / Self-Check / Known Limitations / Bridge to Phase 1 Closure).
   Key content:
   - One-Liner: "Design integration landed end-to-end: Lora self-hosted (R2 designer reply 2026-05-19); src/shared/tokens.css canonical; 12 i18n keys via _locales/{en,ru}; branded Loom icons replace placeholders; src/popup + src/background use chrome.i18n.getMessage + loom palette; harness 21/21 (or 24/24) GREEN with A18-A22; operator brand-fit ack received."
   - Wave-by-wave breakdown with commit refs.
   - Test Counts table: vitest before/after; harness before/after.
   - Architectural Notes: R2 substitution rationale; OFL attribution pattern; tokens.css as single source of truth; chrome.i18n.getMessage fallback pattern (|| <const>); .mks-word class as engineering working-defn (designer overridable); icons-as-static-artifacts (NOT regenerated in prebuild per RESEARCH §3 anti-pattern).
   - Self-Check: FOUND list including the 6 new vitest tests, 5 new harness assertions, font bundle, icons, manifest i18n, _locales/, source-code adoption.
   - Bridge: Phase 1 closure status (the LAST Phase 1 gate, brand-fit ack, is now closed); Phase 2 inherits tokens.css + chrome.i18n.getMessage as production conventions.

2. Update .planning/STATE.md:
   - completed_plans + 1 (current 12 → 13)
   - percent re-computed (likely 100% Phase 1 now, since this was the last gate)
   - Add a "Plan 01-12 closure" section under "## Current Position" with the brand-fit ack date + harness count + R2 substitution note
   - Update "stopped_at" + "last_updated" + "last_activity" fields
   - Update "## Outstanding Phase 1 gates" section: remove Plan 01-12 entry (it's now closed); confirm Plan 01-10 status (closed if landed; still pending if not)

3. Update .planning/ROADMAP.md:
   - In the Phase 1 Plans list (after `01-13-PLAN.md` entry per 01-13 SUMMARY ROADMAP.md known-gap), add `- [x] 01-12-PLAN.md — Design integration (R2 Lora, tokens.css, 8 i18n strings, branded icons, manifest i18n)`
   - If Plan 01-10 entry is missing per the 01-13 SUMMARY note about the ROADMAP.md gap, add it too as `- [x]` or `- [ ]` depending on its current status. (Per 01-13 SUMMARY Self-Check: Plan 01-10 was still pending at 01-13 close; if it landed between 01-13 close and this 01-12 close, mark `[x]`.)
   - Update the Phase 1 header line if it carries a count (e.g., "**Plans:** N plans" → updated to actual final count).

4. Atomic commit: `docs(01-12): wave-7 task-1 — Plan 01-12 SUMMARY + STATE.md + ROADMAP.md sync (design integration closed; Phase 1 charter complete)`.
ls -la .planning/phases/01-stabilize-video-pipeline/01-12-SUMMARY.md && grep -n "01-12-PLAN.md\|R2 Lora\|design integration" .planning/ROADMAP.md && grep -n "01-12\|completed_plans" .planning/STATE.md | head -10 && head -20 .planning/phases/01-stabilize-video-pipeline/01-12-SUMMARY.md - .planning/phases/01-stabilize-video-pipeline/01-12-SUMMARY.md exists + follows template + carries all 9 frontmatter sections - STATE.md completed_plans incremented + percent recomputed + Plan 01-12 closure section added - ROADMAP.md Phase 1 Plans list contains the 01-12 entry (and 01-10 entry if previously missing) - Atomic commit lands all three docs in one - Plan 01-12 functionally CLOSED; Phase 1 charter complete (subject to the Wave 7 checkpoint operator ack)

<threat_model>

Trust Boundaries

Boundary Description
Chrome extension boundary (MV3 CSP) tokens.css @font-face MUST reference local URLs only; remote @import (Google Fonts) violates style-src 'self' + font-src 'self' per RESEARCH §13
User-locale boundary (chrome.i18n) messages.json values are operator-facing copy; manifest:name + :description surface in Chrome Web Store + chrome://extensions across all locales
Asset-pipeline boundary (Vite + @crxjs) WOFF2 files are emitted to dist/assets/ with content-hashed names; web_accessible_resources auto-generated by @crxjs
Test-only-symbol boundary (MOKOSH_UAT) Tier-1 grep gate enforces test-mode hook symbols are absent from production dist/ — Plan 01-12 does NOT extend the inventory (no new symbols)

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-01-12-01 Information Disclosure Remote font load via @import url(https://fonts.googleapis.com/...) in tokens.css mitigate Replace line 12 Google Fonts @import with 7-rule local @font-face block (Wave 1 Task 2). Verify zero googleapis matches in dist/ via tests/build/no-remote-fonts.test.ts (Wave 0). Fonts ship under src/shared/fonts/ with OFL LICENSE files (Wave 1 Task 1).
T-01-12-02 Tampering CDN MITM on remote font fetch mitigate Same as T-01-12-01: self-hosting eliminates the CDN trust dependency entirely. WOFF2 files ship as extension-internal assets; integrity is guaranteed by Chrome's extension-package signature.
T-01-12-03 Denial of Service PNG icon below Chrome imageUtil floor → chrome.notifications.create silent rejection mitigate tests/build/icons-present.test.ts asserts each PNG ≥ FLOOR (16≥200B, 48≥500B, 128≥1024B) per RESEARCH §3 + assets-spec.md (Wave 0 + Wave 2). Plan 01-13 harness A9 + A8 (regression rewind for Bug A) catch leaks at harness-time.
T-01-12-04 Denial of Service _locales/ key tampering causing manifest:name resolution failure → manifest validation error at install mitigate tests/i18n/manifest-i18n.test.ts asserts manifest fields + messages.json key shapes (Wave 0); tests/i18n/locale-parity.test.ts enforces default_locale parity per RESEARCH Pitfall 4. JSON validity guaranteed by JSON.parse in tests.
T-01-12-05 Tampering Vite asset hashing breaking @font-face url() resolution at runtime mitigate RESEARCH §2 Pattern A validated empirically (tokens.css → relative url(./fonts/x.woff2) → Vite rebases → @crxjs auto-WAR). UAT harness A18 walks document.styleSheets to resolve the actual emitted URL (handles hashing); fetch + size check verifies runtime reachability. Pattern B fallback (public/fonts/) documented in RESEARCH §2 if Pattern A misfires.
T-01-12-06 Spoofing __MSG_@@extension_id__ substitution in manifest fields (not supported per Chrome docs) accept Plan 01-12 does NOT use @@extension_id; documented as RESEARCH §11 anti-pattern + per the explicit "Anti-pattern" subsection. Low blast radius (only affects if engineer ignores docs).
T-01-12-07 Information Disclosure chrome.i18n.getMessage returning empty string when default_locale lacks key (RESEARCH Pitfall 4) mitigate The `
T-01-12-08 Elevation of Privilege Test-mode hook strings leaking into production dist/ (T-1-11-01 + T-1-13-* + T-1-14-* inheritance) accept Plan 01-12 introduces ZERO new test-mode symbols (A18-A22 use production chrome.* + fetch + getComputedStyle exclusively). Tier-1 grep gate at tests/background/no-test-hooks-in-prod-bundle.test.ts remains at 12 entries post-Plan-01-14; no extension needed. Verified by file-diff at task end.
T-01-12-09 Tampering OFL license/attribution non-compliance mitigate src/shared/fonts/LICENSE-Lora.txt + LICENSE-IBM-Plex.txt ship verbatim per OFL-1.1 attribution best practice (RESEARCH §7); src/shared/fonts/README.md cross-references upstream URLs + R2 substitution rationale.
</threat_model>
Phase-level invariants to assert post-plan:
  1. Build cleanliness: npm run build exits 0; npm run build:test exits 0; npx tsc --noEmit exits 0.
  2. MV3 CSP compliance: zero googleapis / https://fonts matches in dist/ (tests/build/no-remote-fonts.test.ts).
  3. Vitest baseline grown: 100 (post-01-14) → 106+ GREEN (+6 new unit tests; +N if welcome migration adds keys).
  4. UAT harness 21/21 (or 24/24 with 01-10) GREEN: npm run test:uat exits 0; A0 Tier-1 grep gate GREEN; A1-A14 (or A1-A17 with 01-10) + A23 (Plan 01-14) GREEN; A18-A22 GREEN.
  5. Font bundle present: all 7 or 8 WOFF2 files exist under src/shared/fonts/; total bundle ≤ 350KB; LICENSE files + README.md present.
  6. Icons rasterized: icons/icon{16,48,128}.png are 8-bit RGBA PNGs at correct dims + clearing FLOOR; differ in content from the Bug A placeholders.
  7. Manifest i18n wired: chrome://extensions name reads "Mokosh — Session Capture" (en) or "Mokosh — Запись сессии" (ru); operator-facing tooltips + notification copy + popup CTA all resolve via chrome.i18n.getMessage.
  8. Tokens adopted: src/popup/style.css has 0 hex literals + imports tokens.css; src/welcome/welcome.css (if 01-10 landed) imports tokens.css; BADGE_REC_COLOR is madder-rust #b2543d.
  9. Tier-1 forbidden-strings inventory UNCHANGED at 12 entries post-Plan-01-14.
  10. R2 substitution complete: zero Newsreader references anywhere in src/shared/tokens.css; --mks-font-display === '"Lora", "Iowan Old Style", "Times New Roman", serif'.
  11. Operator brand-fit ack: Wave 7 checkpoint received "approved".

<success_criteria>

  • All 6 vitest tests in tests/build/ + tests/i18n/ GREEN
  • src/shared/fonts/ contains all required WOFF2 + LICENSE + README.md files
  • src/shared/tokens.css canonical: zero remote-font URLs, R2 Lora substitution, .mks-word class present
  • icons/icon{16,48,128}.png rasterized from Loom mark, clearing all FLOORs, 8-bit RGBA
  • manifest.json carries __MSG_extName__ + __MSG_extDesc__ + default_locale: "en" + action.default_title: "__MSG_tooltipOff__"
  • _locales/{en,ru}/messages.json each carry the 12-key matrix with D-07 + D-08 + Brief §02 values
  • src/popup/ + src/background/ use chrome.i18n.getMessage at every operator-facing copy site (with || <const> fallback)
  • src/popup/style.css has zero hex literals + imports tokens.css
  • BADGE_REC_COLOR === '#b2543d' (--mks-madder-600 per D-04)
  • If Plan 01-10 landed: welcome-tokens.css = @import '../shared/tokens.css';; welcome.ts uses chrome.i18n.getMessage
  • vite.config.ts has __VITE_DEV__ defensive define
  • scripts/{rasterize-icons.sh,subset-fonts.sh,README.md} all present
  • UAT harness A18-A22 implemented + GREEN; npm run test:uat exits 0 with 21/21 (or 24/24 with 01-10) GREEN
  • Tier-1 forbidden-strings inventory UNCHANGED at 12 entries post-Plan-01-14
  • Operator empirical brand-fit ack ("approved") received at Wave 7 checkpoint
  • 01-12-SUMMARY.md exists + STATE.md + ROADMAP.md synced
  • All commits atomic + follow feat(01-12)/test(01-12)/docs(01-12): wave-N task-M — <one-liner> convention </success_criteria>
After completion, create `.planning/phases/01-stabilize-video-pipeline/01-12-SUMMARY.md` per the Wave 7 Task 1 spec — mirroring the structure of 01-13-SUMMARY.md (frontmatter with phase/plan/subsystem/tags/requires/provides/affects/tech-stack/key-files/decisions/metrics + body sections One-Liner / What Landed / Test Counts / Deviations / Architectural Notes Worth Carrying Forward / Self-Check / Known Limitations / Bridge to Phase 1 Closure).