---
phase: 01-stabilize-video-pipeline
plan: 06
type: execute
wave: 5
depends_on: ["03", "05"]
files_modified:
  - vite.config.ts
  - offscreen/index.ts
  - offscreen/index.html
  - src/background/index.ts
autonomous: true
requirements:
  - REQ-video-ring-buffer
requirements_addressed:
  - REQ-video-ring-buffer

must_haves:
  truths:
    - "`vite.config.ts` no longer contains the `copy-offscreen` inline plugin — the entire 200+-line block including IndexedDB plumbing, codec fallback chain, and `mediaRecorder` shadow is GONE per D-08 (orphan dead-code deletion plus inline-plugin deletion in lockstep)"
    - "`vite.config.ts` declares the offscreen entry via `rollupOptions.input` per RESEARCH.md Example B; crxjs picks up the new TS entry through the HTML reference and the runtime path remains `offscreen/index.html` resolvable via `chrome.runtime.getURL(...)` per D-07"
    - "Top-level `offscreen/index.ts` is DELETED (dead code per audit P2 #18; D-08 explicit DELETE target)"
    - "Top-level `offscreen/index.html` is DELETED (REPLACED by the crxjs-managed `src/offscreen/index.html` from Plan 03, per D-07 — the runtime URL semantics survive the source-tree move)"
    - "`npm run build` exits 0; `dist/` contains a bundled offscreen HTML at the path the SW's `chrome.runtime.getURL` argument expects (D-07 runtime-path contract)"
    - "No mention of `VideoRecorderDB`, `openIndexedDB`, `chromeMediaSource`, or `copy-offscreen` remains in `vite.config.ts`"
  artifacts:
    - path: "vite.config.ts"
      provides: "Minimal vite config with crxjs and offscreen entry"
      min_lines: 15
      contains: "rollupOptions"
    - path: "dist/manifest.json"
      provides: "Build output (artifact of npm run build)"
      contains: "manifest_version"
    - path: "dist/src/offscreen/index.html"
      provides: "Bundled offscreen HTML at the path SW.ensureOffscreen expects (or wherever crxjs emits it — actual path verified at build time)"
      contains: ""
  key_links:
    - from: "vite.config.ts (rollupOptions.input.offscreen)"
      to: "src/offscreen/index.html"
      via: "rollup input declaration"
      pattern: "src/offscreen/index.html"
    - from: "vite.config.ts"
      to: "@crxjs/vite-plugin"
      via: "crx({ manifest, contentScripts: { injectCss: false } })"
      pattern: "crx("
---

<objective>
Collapse the build pipeline. DELETE the 200+-line `copy-offscreen` inline
Vite plugin block in `vite.config.ts` and the dead `offscreen/index.ts` +
`offscreen/index.html` at the repo root. The crxjs plugin already handles
bundling; we declare the new offscreen entry via `rollupOptions.input`
pointing at the `src/offscreen/index.html` Plan 03 created.

Purpose: D-08 says delete the inline plugin (which is the audit's P0 #1 root
cause). D-07 says crxjs picks up the new TS entry through the HTML
reference. The execution path is "minimal vite.config.ts + the existing
crx() invocation + a single rollupOptions.input line — that's it."

The plan also includes a build-time pathing verification — after the first
`npm run build`, the executor inspects `dist/` and confirms the bundled
offscreen HTML lands at the SAME path the SW's
`chrome.runtime.getURL(...)` argument in Plan 05 expects. If crxjs strips
the `src/` prefix (which it sometimes does), the SW URL gets adjusted by a
follow-up Edit. RESEARCH.md Pitfall 5 calls this out explicitly.

Output: A ~25-line `vite.config.ts`, no top-level `offscreen/` directory,
and a clean `npm run build` whose output passes the path-matching check.
</objective>

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

<context>
@.planning/phases/01-stabilize-video-pipeline/01-CONTEXT.md
@.planning/phases/01-stabilize-video-pipeline/01-RESEARCH.md
@.planning/phases/01-stabilize-video-pipeline/01-PATTERNS.md
@vite.config.ts
@offscreen/index.ts
@offscreen/index.html
@src/offscreen/index.html
@manifest.json
</context>

<interfaces>
crxjs entry contract (per RESEARCH.md §"Pitfall 5" and discussion #919):
- `rollupOptions.input` declares an HTML entry; crxjs bundles the HTML and emits its referenced TS module into `dist/assets/`.
- The runtime URL that the SW passes to `chrome.offscreen.createDocument` MUST match the bundled HTML path. If `input: { offscreen: 'src/offscreen/index.html' }`, the runtime URL is typically `chrome.runtime.getURL('src/offscreen/index.html')` (crxjs preserves the input key as the output path), but real-world projects have reported the `src/` prefix being stripped. The executor MUST verify after the first `npm run build` and adjust Plan 05's `ensureOffscreen` call if needed.

Plan 05's `ensureOffscreen` currently calls:
```typescript
const url = chrome.runtime.getURL('offscreen/index.html');
```
(this is leftover from the pre-amendment manifest entry that has the old root-level `offscreen/` dir). Plan 06 verifies the post-build emit path and updates this string to match. If the emit path is `src/offscreen/index.html`, the call becomes `getURL('src/offscreen/index.html')`. Plan 05 wrote the path AS-IS — Plan 06 is the right place to do the binding verification because it owns the build pipeline.
</interfaces>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| build-time vite plugin → bundled output | A malicious build plugin could inject code; we're DELETING our inline plugin (the audit's P0 #1) which is itself an attack-surface reduction |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-1-NEW-06-01 | Tampering — string-injected code via inline plugin | `vite.config.ts:13-216` `copy-offscreen` plugin's `this.emitFile({ source: \`<JS-as-string>\` })` | mitigate | DELETE the entire inline plugin (D-08). The replacement is a `src/offscreen/recorder.ts` real module + a `src/offscreen/index.html` declared in `rollupOptions.input`. No more long template-literal JS in `vite.config.ts`. Grep gate: `grep -v '^#' vite.config.ts \| grep -c "this.emitFile"` returns 0. |
| T-1-NEW-06-02 | Tampering — orphaned root-level offscreen | `offscreen/index.ts` + `offscreen/index.html` (dead code) | mitigate | DELETE both files (D-07 / D-08 — runtime path is preserved via the crxjs-managed `src/offscreen/index.html`). After this plan, a `find offscreen/ -type f` produces no output. Grep gate: `[ ! -d offscreen ]` exits 0. |
</threat_model>

<tasks>

<task type="auto">
  <name>Task 1: DELETE — top-level offscreen/ directory and inline copy-offscreen plugin</name>
  <files>vite.config.ts, offscreen/index.ts, offscreen/index.html</files>
  <read_first>
    - vite.config.ts (lines 1-227 — full file; the inline plugin spans lines 13-216)
    - offscreen/index.ts (60 lines — dead code)
    - offscreen/index.html (10 lines — replaced by src/offscreen/index.html from Plan 03)
    - .planning/phases/01-stabilize-video-pipeline/01-PATTERNS.md §`vite.config.ts` (lines 399-436)
    - .planning/phases/01-stabilize-video-pipeline/01-RESEARCH.md §"Example B" (lines 895-915)
  </read_first>
  <action>
Three deletions and one rewrite.

(1) Delete `offscreen/index.ts` (the orphan dead-code file):

```bash
rm offscreen/index.ts
```

(2) Delete `offscreen/index.html` (it is REPLACED by the new `src/offscreen/index.html` that Plan 03 created — the path was deliberately moved into the source tree):

```bash
rm offscreen/index.html
```

(3) Remove the now-empty `offscreen/` directory (verify it is empty first; if anything else lives there, STOP and surface it):

```bash
[ -d offscreen ] && rmdir offscreen
```

(4) REWRITE `vite.config.ts` to be the minimal RESEARCH.md Example B form. Use the Write tool (NOT Edit — the file is being replaced). VERBATIM content:

```typescript
import { defineConfig } from 'vite';
import { crx } from '@crxjs/vite-plugin';
import manifest from './manifest.json';

export default defineConfig({
  plugins: [
    crx({
      manifest,
      contentScripts: {
        injectCss: false,
      },
    }),
  ],
  build: {
    rollupOptions: {
      input: {
        offscreen: 'src/offscreen/index.html',
      },
    },
  },
});
```

The deleted content (the entire `copy-offscreen` plugin block at lines 13-216 plus the surrounding `build.rollupOptions.output.manualChunks` shape at lines 218-226) does not survive any portion in the rewrite.

After these edits, run:

```bash
npx tsc --noEmit
```

It MUST exit 0 (the previous TS errors about `(error as any).message?.includes` etc. were inside `src/background/index.ts`; Plan 05 already cleared those. `vite.config.ts` is type-checked via Vite, not `tsc --noEmit`; the `npx tsc --noEmit` check here is a regression guard for the rest of the codebase).
  </action>
  <verify>
    <automated>[ ! -f offscreen/index.ts ] && [ ! -f offscreen/index.html ] && [ ! -d offscreen ] && [ "$(grep -c 'copy-offscreen' vite.config.ts)" -eq 0 ] && npx tsc --noEmit</automated>
  </verify>
  <acceptance_criteria>
    - `test ! -f offscreen/index.ts` exits 0 (file deleted)
    - `test ! -f offscreen/index.html` exits 0 (file deleted)
    - `test ! -d offscreen` exits 0 (directory removed)
    - `grep -c "copy-offscreen" vite.config.ts` returns 0
    - `grep -c "this.emitFile" vite.config.ts` returns 0 (T-1-NEW-06-01 grep gate)
    - `grep -c "VideoRecorderDB" vite.config.ts` returns 0
    - `grep -c "openIndexedDB" vite.config.ts` returns 0
    - `grep -c "chromeMediaSource" vite.config.ts` returns 0
    - `grep -c "rollupOptions" vite.config.ts` returns 1
    - `grep -c "src/offscreen/index.html" vite.config.ts` returns 1
    - `wc -l vite.config.ts` returns ≤ 30
    - `npx tsc --noEmit` exits 0
    - `npx vitest run` exits 0 (regression guard)
  </acceptance_criteria>
  <done>Inline plugin gone. Root-level offscreen/ gone. vite.config.ts is ~22 lines.</done>
</task>

<task type="auto">
  <name>Task 2: BUILD VERIFY — run npm run build and reconcile the offscreen path</name>
  <files>src/background/index.ts (only if path-adjustment needed)</files>
  <read_first>
    - vite.config.ts (post-Task-1 state)
    - src/offscreen/index.html (from Plan 03)
    - src/background/index.ts (line 85 area — the `chrome.runtime.getURL('offscreen/index.html')` call inherited from before Plan 05; verify with `grep -n "getURL" src/background/index.ts`)
    - .planning/phases/01-stabilize-video-pipeline/01-RESEARCH.md §"Pitfall 5" (lines 838-856)
  </read_first>
  <action>
Run the build:

```bash
rm -rf dist
npm run build 2>&1 | tee /tmp/01-06-build.log
```

The build MUST exit 0. If it doesn't:
- Check the log for syntax/type errors and STOP — do not proceed to the rest of this task.
- Common failure modes: `chrome.offscreen.Reason.DISPLAY_MEDIA` not in current `@types/chrome` (fall back to the cast Plan 05 documented); the manifest reference to `desktopCapture` not yet landed (Plan 01 should have committed it — re-check); rollupOptions.input pointing at a non-existent file.

If the build succeeds, verify the dist layout:

```bash
ls -la dist/
ls -la dist/src/offscreen/ 2>/dev/null
ls -la dist/offscreen/ 2>/dev/null
```

There will be ONE of two outcomes:

**Outcome A — crxjs preserved `src/`**: `dist/src/offscreen/index.html` exists. In this case, the SW URL needs to be `chrome.runtime.getURL('src/offscreen/index.html')`.

**Outcome B — crxjs stripped `src/`**: `dist/offscreen/index.html` exists. In this case, the SW URL needs to be `chrome.runtime.getURL('offscreen/index.html')` (which is what the current Plan-05 file already has by accident — leftover from the pre-amendment code).

Identify which outcome obtains. Then edit `src/background/index.ts` line ~85 (inside `ensureOffscreen`):

```typescript
    const url = chrome.runtime.getURL('<PATH>');
```

If Outcome A: `<PATH>` = `'src/offscreen/index.html'`.

If Outcome B: `<PATH>` = `'offscreen/index.html'` (leave unchanged).

Either way, after the edit (or no-edit), re-run:

```bash
npx tsc --noEmit
npm run build
ls -la dist/manifest.json
node -e "const m=require('./dist/manifest.json'); console.log('permissions:', m.permissions.join(','))"
```

The last line MUST print a comma-separated permission list that includes `desktopCapture` and does NOT include `tabCapture` (Plan 01 should already have ensured this in `manifest.json`; the `dist/manifest.json` is the crxjs-bundled output that propagates the manifest as-is).

Final layout verification:

```bash
find dist -type f -name "*.html" -o -name "*.js" | sort
```

The output should contain (in some order):
- `dist/manifest.json`
- `dist/src/popup/index.html` (or `dist/popup/index.html` — same Outcome-A/B rule)
- `dist/<offscreen path>/index.html` (whichever outcome)
- One or more `dist/assets/*.js` files (bundled SW, content script, offscreen TS, popup TS)

If the manifest.json's `background.service_worker` field doesn't resolve to an emitted file, STOP and audit. crxjs handles this automatically — there's only a problem if the manifest entry was hand-broken.
  </action>
  <verify>
    <automated>npm run build && ls dist/manifest.json && [ -f dist/src/offscreen/index.html ] || [ -f dist/offscreen/index.html ]</automated>
  </verify>
  <acceptance_criteria>
    - `npm run build` exits 0 (with NO TypeScript errors in /tmp/01-06-build.log)
    - `dist/manifest.json` exists
    - Either `dist/src/offscreen/index.html` OR `dist/offscreen/index.html` exists
    - `dist/manifest.json` permissions list contains `"desktopCapture"` and does NOT contain `"tabCapture"`
    - `src/background/index.ts` `chrome.runtime.getURL(...)` argument matches whichever Outcome (A or B) the build produced
    - `dist/assets/` contains at least one `.js` file (the bundled SW / content / popup / offscreen scripts)
  </acceptance_criteria>
  <done>Build succeeds. SW URL string matches the dist layout. Plan 07 can load `dist/` into Chrome and the offscreen will resolve correctly.</done>
</task>

</tasks>

<verification>
After both tasks land:

1. `npm run build` — exits 0, clean output.
2. `dist/` contains a loadable extension (manifest.json + at least one HTML offscreen page + at least one JS bundle in assets/).
3. `npx tsc --noEmit && npx vitest run` — both exit 0 (regression guard for the rest of the codebase).
4. `wc -l vite.config.ts` — ≤ 30 lines.
5. No top-level `offscreen/` directory.
6. The SW URL string in `src/background/index.ts` matches the actual `dist/` emit path (Outcome A or B).

Commit cadence: TWO commits.
- Task 1: ONE commit (`refactor(01-06): delete inline copy-offscreen plugin and orphan offscreen/ directory`).
- Task 2: ONE commit if path adjustment was needed (`fix(01-06): align ensureOffscreen URL with crxjs emit path`); ZERO commits if no path adjustment was needed (note in SUMMARY which Outcome obtained).
</verification>

<success_criteria>
- `vite.config.ts` is ~22 lines, contains crx() and rollupOptions only
- `offscreen/` top-level directory is GONE
- `dist/manifest.json` carries the post-Plan-01 amended permissions (desktopCapture, no tabCapture, no alarms)
- The SW URL string and the bundled HTML path match
- `npm run build` clean
</success_criteria>

<output>
After completion, create `.planning/phases/01-stabilize-video-pipeline/01-06-SUMMARY.md` with:
- Pre / post line count for vite.config.ts (was 227, now ~22)
- Confirmation that `offscreen/` directory is gone (output of `find offscreen/ 2>&1 || echo absent`)
- The outcome (A or B) for crxjs's emit path, and the exact SW URL string committed
- Output of `find dist -type f | sort` after the post-Task-2 build, so Plan 07's manual smoke test knows what to load
- Two commit SHAs (or one if Task 2 needed no path adjustment)
- Note: "Plan 07 loads `dist/` unpacked into Chrome ≥ 116 and runs the ffprobe acceptance gate (D-12)."
</output>
