docs(01): add Plans 01-08 / 01-09 / 01-10 (amended Phase 1 charter)

Plans cover the post-D-13 architecture and the auto-start UX charter
expansion that landed during 2026-05-16 UAT:

- Plan 01-08 — WebM remux via ts-ebml@3.0.2 + webm-muxer@5.1.4. Replaces
  the broken file-concat in mergeVideoSegments with a real single-EBML
  remux. Drives the 2 RED tests in tests/offscreen/webm-playback.test.ts
  to GREEN. Regenerates the canonical fixture against the remuxer.
  5 tasks (4 TDD + 1 operator empirical checkpoint), wave 1.

- Plan 01-09 — Whole-desktop constraint (displaySurface:'monitor',
  cursor:'always') + post-grant validation, chrome.action.onClicked
  direct toolbar invocation, chrome.action badge state machine
  (REC/OFF/ERROR), chrome.runtime.onStartup notification + recovery
  notification on onUserStoppedSharing, popup scoped to SAVE-only.
  17 new test assertions across 4 test files. smoke.sh updated to
  auto-select an entire screen. 5 tasks (4 TDD + 1 operator empirical
  checkpoint), wave 2 (depends on 01-08).

- Plan 01-10 — chrome.runtime.onInstalled welcome tab on first install
  via chrome.storage.local guard; vanilla welcome.html/ts/css bundle
  with single "Начать запись" button consuming install-time activation.
  Uses centralized Logger pattern. 4 tasks (3 TDD + 1 operator empirical
  checkpoint), wave 3 (depends on 01-09).

CONTEXT.md amendment block appended with 4 disambiguated decisions:
- D-14-remux: WebM remux supersedes D-13 file-concat
- D-15-display-surface: whole-desktop + cursor visibility lifted from
  Phase 5 deferral
- D-16-toolbar: toolbar onClicked + popup SAVE-only + badge state
  machine + onStartup/recovery notifications
- D-17-onboarding: welcome tab on first install (distinct from
  D-17-port-lifecycle from Option C)
The earlier D-17 port-lifecycle heading also renamed to hyphenated
form for cross-ref consistency.

Plan-check loop: 3 iterations (initial + 2 revisions). Iteration 1
surfaced 11 findings (2 BLOCKER + 6 WARNING + 3 INFO); all addressed
via revision iter 1 with checker-recommended fixes. Iteration 2
surfaced 3 derivative regressions (literal-string grep anchors from
the iter-1 fixes did not match live CONTEXT.md); all addressed in iter
2 with empirically-validated literals. Iteration 3 PASSED clean.

Validation: gsd-sdk frontmatter.validate + verify.plan-structure both
return valid=True for all 3 plans. Plan 01-08 Task 4 verify-chain
grep tested end-to-end against live CONTEXT.md (exit 0).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 09:19:22 +02:00
parent bc310d98cf
commit 2e499d7387
4 changed files with 1849 additions and 2 deletions

View File

@@ -0,0 +1,550 @@
---
phase: 01-stabilize-video-pipeline
plan: 10
type: execute
wave: 3
depends_on:
- 01-09
files_modified:
- manifest.json
- src/welcome/welcome.html
- src/welcome/welcome.ts
- src/welcome/welcome.css
- src/background/index.ts
- tests/background/onboarding.test.ts
autonomous: false
requirements:
- REQ-video-ring-buffer
tags:
- onboarding
- welcome
- chrome.runtime.onInstalled
- chrome.storage
- web_accessible_resources
must_haves:
truths:
- "On first install (chrome.runtime.onInstalled with reason 'install') a welcome tab opens automatically pointing at src/welcome/welcome.html (resolved via chrome.runtime.getURL)."
- "On subsequent installs (reason 'update' or 'chrome_update') OR on a second-install when chrome.storage.local already has onboarding-completed flag, NO welcome tab opens (assert via tests/background/onboarding.test.ts: chrome.tabs.create is called exactly once across two install events)."
- "Welcome page has a single 'Start Mokosh' button that, on click, sends a REQUEST_PERMISSIONS message to the SW which kicks off the existing startVideoCapture flow (consuming the click as user gesture for getDisplayMedia)."
- "After successful start the welcome page updates its DOM to show a 'Recording started — close this tab when ready' confirmation state (no full page reload; uses chrome.runtime.onMessage to listen for the RECORDING_STARTED ack OR observes the popup-toolbar badge transition by re-checking chrome.action.getBadgeText)."
- "manifest.json adds 'storage' to permissions (if not already present — it IS already present per current manifest, so just verify; do not duplicate) AND adds web_accessible_resources entry for src/welcome/welcome.html so chrome.tabs.create with chrome.runtime.getURL resolves."
- "Onboarding test covers BOTH first-install AND subsequent-install paths via two synthesized onInstalled events with different reason fields."
artifacts:
- path: "src/welcome/welcome.html"
provides: "Welcome page markup with a single Start button, brief explainer paragraph, no fancy framework (vanilla DOM per project style)."
min_lines: 25
- path: "src/welcome/welcome.ts"
provides: "Click-handler that sends REQUEST_PERMISSIONS to SW; transitions DOM to confirmation state on success; surfaces error on failure."
min_lines: 30
- path: "src/welcome/welcome.css"
provides: "Minimal styling (~30 LOC) consistent with the project's existing popup style.css aesthetic."
min_lines: 20
- path: "src/background/index.ts"
provides: "onInstalled handler extended: if reason === 'install' AND chrome.storage.local does not have 'onboarding-completed' flag, open welcome tab via chrome.tabs.create + set flag. Existing IDB cleanup + initialize() call preserved."
contains: "src/welcome/welcome.html"
- path: "manifest.json"
provides: "web_accessible_resources array with entry for src/welcome/welcome.html. 'storage' permission verified present."
contains: "web_accessible_resources"
- path: "tests/background/onboarding.test.ts"
provides: "RED-to-GREEN tests pinning: (a) first install creates welcome tab; (b) update/chrome_update does NOT create welcome tab; (c) repeated install when flag already set does NOT create welcome tab."
key_links:
- from: "src/background/index.ts onInstalled handler"
to: "chrome.tabs.create"
via: "chrome.tabs.create({url: chrome.runtime.getURL('src/welcome/welcome.html')})"
pattern: "chrome.tabs.create"
- from: "src/welcome/welcome.ts click handler"
to: "chrome.runtime.sendMessage"
via: "{type: 'REQUEST_PERMISSIONS'} — kicks the existing SW startVideoCapture flow"
pattern: "REQUEST_PERMISSIONS"
- from: "manifest.json web_accessible_resources"
to: "src/welcome/welcome.html"
via: "resources array entry making the page extension-accessible"
pattern: "src/welcome/welcome.html"
---
<objective>
First-install operator-friendly activation. When the extension is installed (typically from the Chrome Web Store one day, but also from a local Load Unpacked), a welcome tab opens automatically with a brief explainer + a single 'Start Mokosh' button. Operator reads the explainer + clicks → recording starts immediately (consuming the install-time click as the getDisplayMedia user gesture).
This complements Plan 01-09's toolbar-onClicked + onStartup notification flow:
- Plan 01-09 makes Chrome-startup-time activation easy (notification → click → picker).
- Plan 01-10 makes install-time activation easy (welcome tab → click → picker).
- Both consume the same RECORDING_ERROR/RECORDING_STARTED surface; both reuse the existing src/background/index.ts startVideoCapture path.
Output:
- src/welcome/welcome.{html,ts,css} — 3 new files for the onboarding page (vanilla DOM per project style; the existing popup at src/popup/index.html is the analog).
- src/background/index.ts — onInstalled handler extended with first-install detection (chrome.storage.local flag) + chrome.tabs.create call.
- manifest.json — adds web_accessible_resources entry pointing at the welcome page so chrome.tabs.create with chrome.runtime.getURL resolves. (storage permission already present per current manifest; verify it's not removed.)
- tests/background/onboarding.test.ts — pins the first-install vs subsequent-install vs already-completed contracts.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/REQUIREMENTS.md
@.planning/phases/01-stabilize-video-pipeline/01-CONTEXT.md
@.planning/phases/01-stabilize-video-pipeline/01-08-PLAN.md
@.planning/phases/01-stabilize-video-pipeline/01-09-PLAN.md
@src/background/index.ts
@src/popup/index.html
@src/popup/index.ts
@src/popup/style.css
@manifest.json
<interfaces>
Key Chrome API surfaces.
chrome.runtime.onInstalled.addListener fires with {reason: 'install' | 'update' | 'chrome_update' | 'shared_module_update'}. The handler in src/background/index.ts line 724 already exists for IDB cleanup; this plan extends it.
chrome.storage.local.get + .set pattern:
```
const { 'onboarding-completed': onboardingCompleted } = await chrome.storage.local.get('onboarding-completed');
if (details.reason === 'install' && onboardingCompleted !== true) {
await chrome.tabs.create({ url: chrome.runtime.getURL('src/welcome/welcome.html') });
await chrome.storage.local.set({ 'onboarding-completed': true });
}
```
chrome.tabs.create signature:
```
chrome.tabs.create({ url: string, active?: boolean }): Promise<chrome.tabs.Tab>
```
manifest.json web_accessible_resources shape (MV3):
```
"web_accessible_resources": [
{
"resources": ["src/welcome/welcome.html"],
"matches": ["<all_urls>"]
}
]
```
Without this entry, chrome.runtime.getURL returns a path but loading it via chrome.tabs.create fails with a permission error in MV3.
Welcome page structure follows the existing popup pattern (src/popup/index.html + src/popup/style.css):
- HTML: container div, h1, p explainer, button. Russian copy consistent with popup ('Сохранить отчёт об ошибке' precedent).
- TS: vanilla DOM (no framework); attach addEventListener('click', handler) to the button; call chrome.runtime.sendMessage({type: 'REQUEST_PERMISSIONS'}); on success update the DOM to show confirmation; on failure show error.
- CSS: minimal, consistent palette with popup's style.css.
The existing src/background/index.ts onInstalled handler at line 724:
```
chrome.runtime.onInstalled.addListener((details) => {
logger.log('Extension installed/updated:', details.reason);
try {
indexedDB.deleteDatabase('VideoRecorderDB');
logger.log('Cleaned up orphaned VideoRecorderDB (if present)');
} catch (e) {
logger.warn('IDB cleanup failed:', e);
}
initialize();
});
```
This plan extends it WITHOUT touching the IDB cleanup or initialize() call — adds the onboarding check as an additional step (sequentially after the existing logic).
Note: the existing onInstalled handler is SYNCHRONOUS (no async/await). Wrapping the new onboarding logic in a self-executing async IIFE preserves the handler's sync signature while allowing await for chrome.storage.local + chrome.tabs.create.
Vite/crxjs note: src/welcome/welcome.html needs to be declared as a rollupOptions.input entry in vite.config.ts so the build bundles it into dist/. The existing vite.config.ts already has src/offscreen/index.html as an input; add src/welcome/welcome.html alongside. Otherwise the welcome page won't appear in dist/.
Existing tests/background/request-id-protocol.test.ts chrome stub pattern includes chrome.runtime.onInstalled.addListener as a vi.fn(); extend with chrome.tabs.create + chrome.storage.local.{get, set} for this test file.
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: RED tests — onInstalled first-install creates welcome tab; subsequent-install does not; already-completed flag suppresses.</name>
<read_first>
- tests/background/request-id-protocol.test.ts lines 53-200 (chrome stub scaffold)
- src/background/index.ts lines 724-737 (existing onInstalled handler)
- manifest.json (current shape — confirms storage permission already present)
</read_first>
<files>tests/background/onboarding.test.ts</files>
<behavior>
Three RED tests:
- Test A: when onInstalled fires with reason 'install' AND chrome.storage.local has no 'onboarding-completed' flag, chrome.tabs.create is called exactly once with a URL containing 'src/welcome/welcome.html'. After the call, chrome.storage.local.set is called with `{'onboarding-completed': true}`. **I-02 fix (2026-05-16 checker pass):** additionally assert `expect(chromeStub.storage.local.get).toHaveBeenCalledWith('onboarding-completed')` — this pins the EXACT key name the handler reads from storage. Without this assertion, a refactor that renamed the constant to e.g. `'mokosh-onboarding-done'` would still pass Test A's set-side assertion (because both the read and write would use the new name in lock-step) while silently breaking compatibility with any installed user's storage from the prior schema. One additional line of test code; catches a real cross-version compatibility bug class.
- Test B: when onInstalled fires with reason 'update', chrome.tabs.create is NOT called.
- Test C: when onInstalled fires with reason 'install' BUT chrome.storage.local already has 'onboarding-completed': true, chrome.tabs.create is NOT called.
</behavior>
<action>
1. Create tests/background/onboarding.test.ts.
2. Copy the chrome stub from tests/background/request-id-protocol.test.ts. Extend with:
- chrome.tabs: { create: vi.fn().mockResolvedValue({ id: 99, url: 'chrome-extension://...' }) }
- chrome.storage: { local: { get: vi.fn(), set: vi.fn().mockResolvedValue(undefined) } }
- chrome.runtime.onInstalled: { addListener: vi.fn(), _callbacks: [] } — and have addListener push to _callbacks for test-driven invocation
- chrome.runtime.getURL: (path) => 'chrome-extension://test-id/' + path
3. Test A:
- chromeStub.storage.local.get.mockResolvedValue({}); // empty — no flag set
- vi.resetModules(); set globalThis.chrome; await import('../../src/background/index');
- Synthesize: await chromeStub.runtime.onInstalled._callbacks[0]({ reason: 'install' });
- Drain microtasks via await Promise.resolve a few times.
- Assert chrome.tabs.create called exactly once with object whose url contains 'src/welcome/welcome.html'.
- Assert chrome.storage.local.set called with {'onboarding-completed': true}.
- **I-02 fix:** assert `expect(chromeStub.storage.local.get).toHaveBeenCalledWith('onboarding-completed')` — pins the exact storage key the handler reads. (One line of test code beyond the existing set-side check.)
4. Test B:
- chromeStub.storage.local.get.mockResolvedValue({});
- vi.resetModules(); set globalThis.chrome; await import.
- Synthesize: await chromeStub.runtime.onInstalled._callbacks[0]({ reason: 'update' });
- Drain microtasks.
- Assert chrome.tabs.create NOT called.
- Assert chrome.storage.local.set NOT called.
5. Test C:
- chromeStub.storage.local.get.mockResolvedValue({ 'onboarding-completed': true });
- vi.resetModules(); set globalThis.chrome; await import.
- Synthesize: await chromeStub.runtime.onInstalled._callbacks[0]({ reason: 'install' });
- Drain microtasks.
- Assert chrome.tabs.create NOT called.
6. Run npx vitest run tests/background/onboarding.test.ts — all 3 must be RED today (the new onInstalled extension does not exist yet).
7. DO NOT modify src/background/index.ts in this task.
</action>
<verify>
<automated>npx vitest run tests/background/onboarding.test.ts</automated>
</verify>
<acceptance_criteria>
- tests/background/onboarding.test.ts exists with 3 tests; all 3 RED.
- Baseline (Plan 01-09 final: 16 files / 76 tests / all GREEN) + 3 new RED here = 17 files / 79 tests / 3 failed | 76 passed.
- npx tsc --noEmit exit 0.
</acceptance_criteria>
<done>3 RED tests pin the onboarding routing contract; baseline preserved.</done>
</task>
<task type="auto">
<name>Task 2: Create welcome page assets src/welcome/welcome.{html,ts,css}; register vite entry; add web_accessible_resources to manifest.</name>
<read_first>
- src/popup/index.html (style analog)
- src/popup/style.css (palette + sizing reference)
- src/popup/index.ts (vanilla DOM + chrome.runtime.sendMessage pattern)
- vite.config.ts (where to add rollupOptions.input entry)
- manifest.json (current web_accessible_resources is ABSENT — this task adds it)
</read_first>
<files>src/welcome/welcome.html, src/welcome/welcome.ts, src/welcome/welcome.css, vite.config.ts, manifest.json</files>
<action>
1. Create src/welcome/welcome.html (Russian per project provenance; matches popup language):
```
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Добро пожаловать в Mokosh</title>
<link rel="stylesheet" href="welcome.css">
</head>
<body>
<main class="welcome">
<h1>Добро пожаловать в Mokosh</h1>
<p>
Расширение записывает последние 30 секунд экрана и 10 минут логов
вашего браузера, чтобы при возникновении бага вы могли одним
кликом сохранить полный отчёт для службы поддержки.
</p>
<p>
Нажмите кнопку ниже, чтобы выбрать экран и начать запись.
Запись идёт фоном; ваши данные не отправляются никуда —
только сохраняются локально по вашему запросу.
</p>
<button id="startButton" class="start-button" type="button">
Начать запись
</button>
<p id="statusMessage" class="status-message"></p>
</main>
<script type="module" src="welcome.ts"></script>
</body>
</html>
```
2. Create src/welcome/welcome.ts (vanilla DOM; absolute imports per project style). **W-06 fix (2026-05-16 checker pass): use the centralized `Logger` class from `src/shared/logger.ts` instead of an inline `console.log` wrapper.** This matches the background + offscreen + popup convention (popup currently has a bespoke inline `function log` per `src/popup/index.ts:18` — that's a known stylistic divergence the popup hasn't yet been refactored against; the welcome page lands clean from day one). The `Logger` class is the SW logger by published shape (prefix `[SW:<context>]`) — the welcome page is NOT an SW, so the SW-prefix is semantically loose; if the executor finds this jarring during implementation, they MAY add a new `WelcomeLogger` class to `src/shared/logger.ts` mirroring the `OffscreenLogger`/`ContentLogger` pattern (prefix `[WC:<context>]`) and use that here instead. EITHER choice satisfies W-06's "use the centralized logger" requirement; the inline `function log` form is what the checker rejected.
```
// src/welcome/welcome.ts — onboarding click-handler (Plan 01-10 D-17-onboarding).
//
// Sends REQUEST_PERMISSIONS to the SW which routes through the same
// startVideoCapture path as the toolbar onClicked handler (Plan 01-09
// D-16-toolbar). The button click counts as the user gesture for
// getDisplayMedia.
// W-06 fix: use the centralized Logger from src/shared/logger.ts
// instead of an inline console.log wrapper. The Logger class emits
// `[SW:<context>] <ISO timestamp>` lines; the prefix is semantically
// loose for a welcome page (not an SW) but matches the background +
// offscreen logger discipline. If the executor opts to add a new
// WelcomeLogger class to src/shared/logger.ts (prefix `[WC:Welcome]`)
// and import that instead, that ALSO satisfies W-06.
import { Logger } from '../shared/logger';
const logger = new Logger('Welcome');
const startButton = document.getElementById('startButton') as HTMLButtonElement | null;
const statusMessage = document.getElementById('statusMessage') as HTMLParagraphElement | null;
async function onStart(): Promise<void> {
if (startButton === null || statusMessage === null) {
return;
}
startButton.disabled = true;
statusMessage.textContent = 'Открываем выбор источника...';
statusMessage.className = 'status-message';
try {
const response = await chrome.runtime.sendMessage({
type: 'REQUEST_PERMISSIONS',
});
logger.log('REQUEST_PERMISSIONS response:', response);
if (response?.granted === true) {
statusMessage.textContent = 'Запись начата. Эту вкладку можно закрыть.';
statusMessage.className = 'status-message success';
startButton.textContent = 'Запись активна';
} else {
startButton.disabled = false;
statusMessage.textContent = 'Не удалось начать запись. Попробуйте снова.';
statusMessage.className = 'status-message error';
}
} catch (err) {
logger.warn('Start failed:', err);
startButton.disabled = false;
statusMessage.textContent = 'Ошибка: ' + ((err as Error)?.message ?? String(err));
statusMessage.className = 'status-message error';
}
}
function init(): void {
if (startButton !== null) {
startButton.addEventListener('click', onStart);
}
}
document.addEventListener('DOMContentLoaded', init);
```
3. Create src/welcome/welcome.css (consistent palette with src/popup/style.css):
```
html, body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f5f5f5;
color: #1f1f1f;
}
.welcome {
max-width: 600px;
margin: 60px auto;
padding: 32px;
background: #ffffff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.welcome h1 {
margin: 0 0 16px;
font-size: 24px;
font-weight: 600;
}
.welcome p {
line-height: 1.5;
margin: 0 0 16px;
}
.start-button {
display: block;
width: 100%;
padding: 12px 16px;
margin: 24px 0 16px;
font-size: 16px;
font-weight: 500;
background: #00C853;
color: #ffffff;
border: none;
border-radius: 6px;
cursor: pointer;
}
.start-button:hover:not(:disabled) {
background: #00B248;
}
.start-button:disabled {
background: #BDBDBD;
cursor: not-allowed;
}
.status-message {
min-height: 1.4em;
font-size: 14px;
color: #616161;
}
.status-message.success {
color: #00C853;
}
.status-message.error {
color: #D32F2F;
}
```
4. Update vite.config.ts — add 'src/welcome/welcome.html' to rollupOptions.input alongside the existing offscreen entry:
```
rollupOptions: {
input: {
offscreen: 'src/offscreen/index.html',
welcome: 'src/welcome/welcome.html',
},
},
```
5. Update manifest.json — add a web_accessible_resources array:
```
"web_accessible_resources": [
{
"resources": ["src/welcome/welcome.html"],
"matches": ["<all_urls>"]
}
]
```
Insert this after the "host_permissions" block. Confirm 'storage' is already in permissions (it IS per current manifest line 11; do not duplicate).
6. Run npm run build — exit 0; confirm dist/src/welcome/welcome.html exists and the new web_accessible_resources entry is in dist/manifest.json.
7. Run npx tsc --noEmit — exit 0.
8. Run npx vitest run — baseline preserved (17 files / 76 GREEN + 3 RED from Task 1; the 3 RED stay RED).
</action>
<verify>
<automated>npm run build && npx tsc --noEmit && test -f dist/src/welcome/welcome.html && grep -q web_accessible_resources dist/manifest.json</automated>
</verify>
<acceptance_criteria>
- src/welcome/welcome.{html,ts,css} exist with the contents above (verbatim or stylistically equivalent).
- vite.config.ts has the welcome input entry.
- manifest.json has the web_accessible_resources block with welcome.html.
- npm run build exit 0; dist/ contains src/welcome/welcome.html and a manifest with web_accessible_resources.
- npx tsc --noEmit exit 0.
- Baseline preserved (17 files / 79 tests / 3 RED from Task 1 still RED; 76 GREEN).
</acceptance_criteria>
<done>Welcome page assets staged + build pipeline picks them up + manifest declares them accessible; ready for the SW handler in Task 3.</done>
</task>
<task type="auto" tdd="true">
<name>Task 3: GREEN — extend onInstalled in src/background/index.ts with first-install welcome-tab logic; drives Task 1 tests to GREEN.</name>
<read_first>
- tests/background/onboarding.test.ts (contracts from Task 1)
- src/background/index.ts lines 724-737 (existing onInstalled handler)
</read_first>
<files>src/background/index.ts</files>
<action>
1. Define a constant near the top of the file (alongside other top-level constants like VIDEO_MIME_FALLBACK):
const ONBOARDING_FLAG = 'onboarding-completed';
const WELCOME_PATH = 'src/welcome/welcome.html';
2. Extract a helper function (placed near the other helpers, e.g. just below ensureOffscreen at line 86):
async function openWelcomeIfFirstInstall(details: chrome.runtime.InstalledDetails): Promise<void> {
if (details.reason !== 'install') {
return;
}
try {
const stored = await chrome.storage.local.get(ONBOARDING_FLAG);
if (stored[ONBOARDING_FLAG] === true) {
logger.log('Onboarding already completed; skipping welcome tab.');
return;
}
const url = chrome.runtime.getURL(WELCOME_PATH);
await chrome.tabs.create({ url });
await chrome.storage.local.set({ [ONBOARDING_FLAG]: true });
logger.log('Welcome tab opened and onboarding flag set.');
} catch (err) {
logger.warn('openWelcomeIfFirstInstall failed:', err);
}
}
Document with a JSDoc header per project style; cite Plan 01-10 **D-17-onboarding** (this plan's CONTEXT amendment marker — appended at plan-creation time alongside D-14-remux / D-15-display-surface / D-16-toolbar). **B-02 fix (2026-05-16 checker pass):** the original draft of this line cited bare 'D-16' which is ambiguous — the historical decisions block (CONTEXT.md line 100) has `**D-16:** Video buffer ownership moves to the offscreen document`, completely unrelated to the toolbar UX. To disambiguate per the D-17-port-lifecycle / D-17-onboarding precedent, all amendment-block markers carry a `-suffix`: D-14-remux (remux helper), D-15-display-surface (whole-desktop constraint), D-16-toolbar (toolbar+badge+notifications), D-17-onboarding (this plan's welcome-tab). Citing D-17-onboarding here points the JSDoc reader at the right amendment block on CONTEXT.md.
3. Modify the existing onInstalled handler (line 724) to invoke the helper. The existing handler is synchronous; wrap the new call in a fire-and-forget pattern OR convert the listener to async — both are valid for chrome.runtime.onInstalled (Chrome 91+ accepts async listeners; the IDB cleanup is sync and stays at the top, and the welcome flow is async at the bottom):
chrome.runtime.onInstalled.addListener((details) => {
logger.log('Extension installed/updated:', details.reason);
try {
indexedDB.deleteDatabase('VideoRecorderDB');
logger.log('Cleaned up orphaned VideoRecorderDB (if present)');
} catch (e) {
logger.warn('IDB cleanup failed:', e);
}
initialize();
// Plan 01-10: open welcome tab on first install. Fire-and-forget;
// the helper logs its own errors.
openWelcomeIfFirstInstall(details).catch((err) => {
logger.warn('openWelcomeIfFirstInstall threw:', err);
});
});
4. Run npx vitest run tests/background/onboarding.test.ts — all 3 must flip GREEN.
5. Run full suite — 17 files / 79 tests / all GREEN.
6. Run npx tsc --noEmit — exit 0.
7. Run npm run build — exit 0.
Naming/style: ONBOARDING_FLAG + WELCOME_PATH SCREAMING_SNAKE per project rule for true constants. openWelcomeIfFirstInstall — full-word camelCase. No 'continue'; if-else chains. No 'as any'. The chrome.storage.local.get key-name access is type-safe via dynamic indexing (acceptable per @types/chrome's chrome.storage.local signature: returns Record<string, unknown>).
</action>
<verify>
<automated>npx vitest run tests/background/onboarding.test.ts</automated>
</verify>
<acceptance_criteria>
- openWelcomeIfFirstInstall helper exists in src/background/index.ts with the documented behavior.
- onInstalled handler invokes the helper.
- All 3 onboarding tests GREEN.
- Full suite 17 files / 79 tests / all GREEN.
- npx tsc --noEmit exit 0.
- npm run build exit 0.
</acceptance_criteria>
<done>onInstalled extended; tests GREEN; welcome flow wired end-to-end at the SW layer.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 4: Operator empirical check — first install opens welcome tab; click triggers picker; recording starts; tests confirm second install does NOT re-open welcome.</name>
<files>(operator-driven; no specific source file modified by this checkpoint)</files>
<action>See <how-to-verify> below — operator-driven empirical check; the executor agent must not bypass this checkpoint by stubbing.</action>
<verify>
<automated>echo "checkpoint:human-verify — see how-to-verify section; resume signal is the gate"</automated>
</verify>
<done>Operator types "approved" after running the how-to-verify steps. See <resume-signal> for the exact gate.</done>
<what-built>
Tasks 1-3 landed: src/welcome/* page bundle, manifest web_accessible_resources, onInstalled handler extended with chrome.storage.local-gated welcome-tab opening. The 3 unit tests are GREEN. This checkpoint validates real Chrome behavior end-to-end.
</what-built>
<how-to-verify>
1. Build: npm run build (exit 0).
2. Wipe smoke profile: rm -rf /tmp/mokosh-smoke-profile (or KEEP_PROFILE=0 ./smoke.sh which does the wipe).
3. Run smoke: KEEP_PROFILE=0 ./smoke.sh. Chrome launches with fresh profile.
4. Load Unpacked → select dist/. THE WELCOME TAB SHOULD AUTOMATICALLY OPEN within ~1 second after the extension loads. The tab URL should look like chrome-extension://<id>/src/welcome/welcome.html.
5. Confirm the welcome page renders: title 'Добро пожаловать в Mokosh', explainer paragraphs, big green 'Начать запись' button, empty status message line.
6. Click 'Начать запись'. The button disables; status message shows 'Открываем выбор источника...'; Chrome's screen-share picker appears (monitor-only per Plan 01-09).
7. Pick 'Entire screen' and accept. Status message transitions to 'Запись начата. Эту вкладку можно закрыть.' The toolbar badge transitions to REC (green) per Plan 01-09.
8. Close the welcome tab.
9. Now reload the extension at chrome://extensions (toggle off then on). Observe: the welcome tab does NOT open this time (because chrome.storage.local has 'onboarding-completed' === true from the first install). Only the existing toolbar/badge behavior applies.
10. To re-validate the onInstalled='install' path: wipe the profile (Cmd+Q Chrome → rm -rf /tmp/mokosh-smoke-profile → relaunch smoke.sh → Load Unpacked again). Welcome tab opens again because storage.local was wiped with the profile.
11. If step 4 (welcome tab opens), step 6 (picker appears on click), step 7 (recording starts), or step 9 (re-load does NOT re-open) fails: document the failure mode + Chrome version + the SW console errors. Iterate on Task 2 (asset bundling) or Task 3 (SW handler) accordingly.
</how-to-verify>
<resume-signal>
Type 'approved' after steps 4, 6, 7, 9 all PASS. If any step fails, paste the failure diagnostic.
</resume-signal>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| welcome page <-> SW | Welcome page (a same-origin extension page) sends REQUEST_PERMISSIONS via chrome.runtime.sendMessage. T-1-NEW-05-01 sender-id check in the SW's onMessage listener (line 635) already validates sender.id === chrome.runtime.id; no new boundary. |
| chrome.storage.local <-> SW | Storage flag is non-secret (boolean true); even if leaked, the only effect is suppressing the welcome tab on future installs. No PII; no sensitive content. |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-1-10-01 | Tampering | adversary clears onboarding-completed flag to spam welcome tabs | accept | Welcome tab is non-destructive; the worst case is an extra tab on each install which the operator can close. No data exfiltration path. |
| T-1-10-02 | Information Disclosure | welcome.html leaking via web_accessible_resources fingerprinting (extensions can be enumerated by sites probing chrome-extension:// URLs in web_accessible_resources) | accept | Extension identifier is already discoverable through chrome.runtime.getURL exposure on any extension page; matches:[\"<all_urls>\"] is the standard pattern for welcome flows. Phase 5 hardening could narrow matches to a specific install-confirmation domain if needed; out of scope. |
| T-1-10-03 | Denial of Service | adversary controls chrome.storage.local quota via web requests | mitigate | Single boolean flag uses ~50 bytes; storage.local quota is 10 MB; not exploitable. |
| T-1-10-04 | Elevation of Privilege | welcome page tricks SW into bypassing checkpoints | mitigate | Welcome page sends only REQUEST_PERMISSIONS — same route as the popup. The SW's existing sender-id check + the chrome.action user-gesture model both still apply. No new elevation path. |
</threat_model>
<verification>
- npx vitest run shows 17 files / 79 tests / all GREEN.
- npx tsc --noEmit exit 0.
- npm run build exit 0; dist/src/welcome/welcome.html exists; dist/manifest.json contains web_accessible_resources with welcome.html.
- Operator empirical: first install opens welcome tab; click triggers picker; recording starts; reload does NOT re-open welcome.
- grep -n "chrome.tabs.create" src/background/index.ts returns at least one match.
- grep -n "welcome.html" manifest.json returns at least one match.
- grep -n "openWelcomeIfFirstInstall" src/background/index.ts returns at least one match.
</verification>
<success_criteria>
Plan 01-10 is complete when:
1. The 3 onboarding tests are GREEN.
2. All 76 baseline GREEN tests from Plan 01-09 remain GREEN.
3. Operator runs the Task 4 checkpoint and confirms first install opens welcome tab, click starts recording, reload does NOT re-open.
4. tsc + build clean; manifest + vite + welcome assets all consistent.
</success_criteria>
<output>
After completion, create .planning/phases/01-stabilize-video-pipeline/01-10-SUMMARY.md per the standard template. Cite: the 3 new tests landed GREEN; new src/welcome/ page bundle; manifest web_accessible_resources delta; onInstalled extension; first-install vs subsequent-install behavior confirmed by Task 4 operator check.
</output>