Compare commits

10 Commits

Author SHA1 Message Date
acb9033293 docs(01): record Phase 1 planning complete (7 plans, 7 waves)
After gsd-plan-phase 1: 7 plans across 7 waves. All gates pass:
- Plan-checker (sonnet) VERIFICATION PASSED on iteration 1
- Decision coverage gate (gsd-sdk): 19/19 decisions covered
- Requirements coverage: REQ-video-ring-buffer in all plans
- Security threat model: T-1-01/02/04 mitigated; T-1-03 accepted residual

Known non-blocking gaps:
- gsd-sdk roadmap.annotate-dependencies failed (t.trim is not a function);
  ROADMAP plan-list annotations skipped. Phase 1 plan-list in ROADMAP.md
  remains accurate; this is a cosmetic nice-to-have for cross-cutting
  constraint visibility.
- 1 plan-checker warning (stale wave prose in Plan 03/04 objectives) was
  fixed during decision-coverage revision.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:09:09 +02:00
0811c6a292 docs(01-01): cite D-05 in must_haves per coverage gate .planning/phases/01-stabilize-video-pipeline/01-01-PLAN.md 2026-05-15 17:07:49 +02:00
576280f6aa docs(01): mark Open Questions RESOLVED in research per checker iteration 1
Renames "## Open Questions" header to "## Open Questions (RESOLVED)" and
adds inline RESOLVED markers to each of the three questions:
- Q1 (MediaRecorder timeslice cluster alignment) → D-12 ffprobe gate
  (Plan 03 Task 2 + Plan 07 Task 1) + D-13 fallback (pre-staged skeleton
  in src/offscreen/recorder.ts per Plan 03)
- Q2 (5-minute port lifetime cap) → Plan 04's 290 s pre-emptive reconnect
  plus synchronous onDisconnect → connectPort reconnect path
- Q3 (crxjs path-emit behavior) → Plan 06 Task 2 runtime verification +
  conditional src/background/index.ts edit

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:50:40 +02:00
519a0d8a99 docs(01): revise plan 07 wave + ffprobe verify guard per checker iteration 1
Two changes:
1. wave: 3 → 6 (cascade: max(wave(05)=4, wave(06)=5)+1 = 6).
2. Task 1 <automated> verify now prefixes the ffprobe invocation with
   test -f tests/fixtures/last_30sec.webm && which ffprobe so the gate
   fails fast with a clear signal if the human checkpoint never produced
   the fixture (instead of ffprobe blowing up with a cryptic file-not-found).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:50:32 +02:00
61c3e03069 docs(01): revise plan 06 depends_on per checker iteration 1
Three changes to resolve the blocker:
1. depends_on: ["03"] → ["03", "05"] — Plan 06 Task 2 conditionally edits
   src/background/index.ts which Plan 05 writes; the original wave 2
   collocation with Plan 05 was a same-wave file conflict.
2. wave: 2 → 5 (cascade: max(wave(03)=2, wave(05)=4)+1 = 5).
3. files_modified gains src/background/index.ts (Task 2 path-adjustment
   edit is now declared in frontmatter so the executor sees the contract).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:50:26 +02:00
51890b0bc4 docs(01): revise plan 05 wave per checker iteration 1 cascade
Plan 05 depends_on: ["03", "04"], so wave must be max(2, 3)+1 = 4, not 2
(cascade from Plan 04 wave change).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:50:19 +02:00
e55c1ae5d6 docs(01): revise plan 04 wave per checker iteration 1
Plan 04 depends_on: ["02", "03"], so wave must be max(1, 2)+1 = 3, not 1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:50:10 +02:00
b8219af5b9 docs(01): revise plan 03 wave + OffscreenLogger strict-mode per checker iteration 1
Two changes:
1. wave: 1 → 2 (cascade after Plan 02 wave fix)
2. OffscreenLogger: ...args: any[] → ...args: unknown[] for strict-mode
   hygiene. Existing Logger / ContentLogger are left on the legacy any[]
   pattern (refactoring is out of Phase 1 scope) — divergence documented
   via style_divergence_note frontmatter field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:50:05 +02:00
c24fcda818 docs(01): revise plan 02 wave per checker iteration 1
Plan 02 depends_on: ["01"], so wave must be max(wave(01)=0)+1 = 1, not 0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:49:59 +02:00
74ff472811 docs(01): add scope_exception to plan 01 per checker iteration 1
Documents that all 6 tasks are doc-text-only edits with no TypeScript
compilation — cognitive load is substantially lower than code plans of
equal task count. Avoids fragmenting the atomic doc-cascade by splitting
into 01-01a / 01-01b.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:49:54 +02:00
9 changed files with 66 additions and 48 deletions

View File

@@ -2,14 +2,14 @@
gsd_state_version: 1.0 gsd_state_version: 1.0
milestone: v2.0.0 milestone: v2.0.0
milestone_name: milestone milestone_name: milestone
status: planning status: executing
stopped_at: Phase 1 context gathered stopped_at: Phase 1 context gathered
last_updated: "2026-05-15T13:40:45.486Z" last_updated: "2026-05-15T15:08:45.135Z"
last_activity: 2026-05-15 — bootstrap from intel synthesis (PROJECT.md, last_activity: 2026-05-15 -- Phase 1 planning complete
progress: progress:
total_phases: 5 total_phases: 5
completed_phases: 0 completed_phases: 0
total_plans: 0 total_plans: 7
completed_plans: 0 completed_plans: 0
percent: 0 percent: 0
--- ---
@@ -29,8 +29,8 @@ no server, no password leaks.
Phase: 1 of 5 (Stabilize video pipeline) Phase: 1 of 5 (Stabilize video pipeline)
Plan: 0 of TBD in current phase Plan: 0 of TBD in current phase
Status: Ready to plan Status: Ready to execute
Last activity: 2026-05-15 — bootstrap from intel synthesis (PROJECT.md, Last activity: 2026-05-15 -- Phase 1 planning complete
REQUIREMENTS.md, ROADMAP.md, STATE.md written) REQUIREMENTS.md, ROADMAP.md, STATE.md written)
Progress: [░░░░░░░░░░] 0% Progress: [░░░░░░░░░░] 0%

View File

@@ -12,6 +12,7 @@ files_modified:
- .planning/intel/constraints.md - .planning/intel/constraints.md
- manifest.json - manifest.json
autonomous: true autonomous: true
scope_exception: "doc-text-only edits with no TypeScript compilation; cognitive load substantially lower than code plans of equal task count"
requirements: requirements:
- REQ-video-ring-buffer - REQ-video-ring-buffer
requirements_addressed: requirements_addressed:
@@ -24,7 +25,7 @@ must_haves:
- "ROADMAP.md Phase 1 Success Criterion #2 no longer references tab re-attach" - "ROADMAP.md Phase 1 Success Criterion #2 no longer references tab re-attach"
- "intel/decisions.md DEC-003 and DEC-010 carry an Amendment block" - "intel/decisions.md DEC-003 and DEC-010 carry an Amendment block"
- "intel/constraints.md CON-tab-capture-binding and CON-service-worker-keepalive are RETIRED with a CON-display-capture-binding added" - "intel/constraints.md CON-tab-capture-binding and CON-service-worker-keepalive are RETIRED with a CON-display-capture-binding added"
- "manifest.json permissions list contains desktopCapture (not tabCapture) and drops the now-unused alarms entry" - "manifest.json permissions list contains desktopCapture (not tabCapture) and drops the now-unused alarms entry (D-05, D-A6)"
- "Every code-touching plan (02-07) sees a consistent doc baseline before it runs" - "Every code-touching plan (02-07) sees a consistent doc baseline before it runs"
artifacts: artifacts:
- path: ".planning/PROJECT.md" - path: ".planning/PROJECT.md"

View File

@@ -2,7 +2,7 @@
phase: 01-stabilize-video-pipeline phase: 01-stabilize-video-pipeline
plan: 02 plan: 02
type: execute type: execute
wave: 0 wave: 1
depends_on: ["01"] depends_on: ["01"]
files_modified: files_modified:
- package.json - package.json

View File

@@ -2,7 +2,7 @@
phase: 01-stabilize-video-pipeline phase: 01-stabilize-video-pipeline
plan: 03 plan: 03
type: tdd type: tdd
wave: 1 wave: 2
depends_on: ["02"] depends_on: ["02"]
files_modified: files_modified:
- src/offscreen/recorder.ts - src/offscreen/recorder.ts
@@ -10,6 +10,7 @@ files_modified:
- src/shared/logger.ts - src/shared/logger.ts
- src/shared/types.ts - src/shared/types.ts
autonomous: true autonomous: true
style_divergence_note: "OffscreenLogger uses `...args: unknown[]` (strict-mode hygiene) where existing Logger / ContentLogger use `...args: any[]` (project convention). Refactoring the existing two for consistency is OUT OF SCOPE for Phase 1; they remain on the legacy pattern."
requirements: requirements:
- REQ-video-ring-buffer - REQ-video-ring-buffer
requirements_addressed: requirements_addressed:
@@ -17,12 +18,14 @@ requirements_addressed:
must_haves: must_haves:
truths: truths:
- "`src/offscreen/recorder.ts` exists and exports the symbols Plan 02 tests against: addChunk, trimAged, getBuffer, resetBuffer, assertCodecSupported, VIDEO_BUFFER_DURATION_MS" - "`src/offscreen/recorder.ts` exists at the canonical source path as a real TypeScript module — strict type-check, source maps, IDE support (D-06)"
- "`src/offscreen/recorder.ts` exports the symbols Plan 02 tests against: addChunk, trimAged, getBuffer, resetBuffer, assertCodecSupported, VIDEO_BUFFER_DURATION_MS"
- "Running `npx vitest run tests/offscreen/ring-buffer.test.ts tests/offscreen/codec-check.test.ts` exits 0 with all tests green" - "Running `npx vitest run tests/offscreen/ring-buffer.test.ts tests/offscreen/codec-check.test.ts` exits 0 with all tests green"
- "Buffer holds at most: 1 header chunk + every chunk with arrival timestamp newer than now-30_000ms" - "Buffer holds at most: 1 header chunk + every chunk with arrival timestamp newer than now-30_000ms"
- "Codec strictly bound to `video/webm;codecs=vp9` at 400 000 bps with `MediaRecorder.isTypeSupported` gate; no fallback chain (D-20)" - "Codec strictly bound to `video/webm;codecs=vp9` at 400 000 bps with `MediaRecorder.isTypeSupported` gate; no fallback chain (D-20)"
- "`MediaRecorder.start(2000)` is called on session start (timeslice = 2000 ms per SPEC §4.1)" - "Capture is `navigator.mediaDevices.getDisplayMedia()` invoked from inside the offscreen document (D-01); SW-side `chrome.tabCapture.getMediaStreamId` is removed in Plan 05"
- "On `MediaStreamTrack.onended`, the buffer is cleared and a `RECORDING_ERROR` of `'user-stopped-sharing'` is emitted to SW" - "Single continuous `MediaRecorder` runs for the whole session with `mediaRecorder.start(2000)` so chunks land on cluster boundaries per SPEC §4.1 (D-09)"
- "One-time source picker fires on session start (operator picks screen/window once); on `MediaStreamTrack.onended` the buffer is cleared and a `RECORDING_ERROR` of `'user-stopped-sharing'` is emitted to SW so the popup can re-prompt next interaction (D-03)"
- "Restart-segments fallback (D-13) is pre-staged as a commented-out skeleton at the bottom of recorder.ts so Plan 07's fallback path doesn't require a re-plan" - "Restart-segments fallback (D-13) is pre-staged as a commented-out skeleton at the bottom of recorder.ts so Plan 07's fallback path doesn't require a re-plan"
- "`src/offscreen/index.html` exists at the source path and references `./recorder.ts`" - "`src/offscreen/index.html` exists at the source path and references `./recorder.ts`"
- "`src/shared/logger.ts` has an `OffscreenLogger` class with `[OS:...]` prefix" - "`src/shared/logger.ts` has an `OffscreenLogger` class with `[OS:...]` prefix"
@@ -71,8 +74,10 @@ handshake — that is Plan 04. To keep this plan inside its context budget, the
port-side code in `recorder.ts` is left as a small import-time placeholder port-side code in `recorder.ts` is left as a small import-time placeholder
that Plan 04 fills in. The Plan-02 port + handshake tests therefore remain that Plan 04 fills in. The Plan-02 port + handshake tests therefore remain
RED until Plan 04 lands, which is the intended choreography per the RED until Plan 04 lands, which is the intended choreography per the
wave-1 dependency graph (Plans 03 and 04 run in parallel; Plan 02 wrote both sequential wave structure (Plan 03 lands in wave 2; Plan 04 follows in wave
sets of RED tests in advance). 3 and refactors the bootstrap section of the same file). Plan 02 wrote both
sets of RED tests in advance so Plan 03 and Plan 04 each have a discrete
RED→GREEN cycle to flip.
Purpose: REQ-video-ring-buffer's load-bearing logic lives here. The ring Purpose: REQ-video-ring-buffer's load-bearing logic lives here. The ring
buffer is a pure function — exactly the TDD sweet spot. The remaining buffer is a pure function — exactly the TDD sweet spot. The remaining
@@ -154,7 +159,7 @@ export interface VideoChunk {
| Threat ID | Category | Component | Disposition | Mitigation Plan | | Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------| |-----------|----------|-----------|-------------|-----------------|
| T-1-01 | Tampering — codec downgrade | `MediaRecorder` constructor | mitigate | `assertCodecSupported()` calls `MediaRecorder.isTypeSupported('video/webm;codecs=vp9')` BEFORE constructing the recorder; if it returns false, throws an Error and emits `RECORDING_ERROR` to SW. No vp8 / h264 / default fallback chain. The strict mode covers `MediaRecorder` itself being absent (the codec-check test mocks both cases). Grep gate: `grep -v '^#' src/offscreen/recorder.ts \| grep -cE 'codecs=(vp8\|h264)'` returns 0 (no fallback codec strings in the module). | | T-1-01 | Tampering — codec downgrade | `MediaRecorder` constructor | mitigate | `assertCodecSupported()` calls `MediaRecorder.isTypeSupported('video/webm;codecs=vp9')` BEFORE constructing the recorder; if it returns false, throws an Error and emits `RECORDING_ERROR` to SW. No vp8 / h264 / default fallback chain. The strict mode covers `MediaRecorder` itself being absent (the codec-check test mocks both cases). Grep gate: `grep -v '^#' src/offscreen/recorder.ts \| grep -cE 'codecs=(vp8\|h264)'` returns 0 (no fallback codec strings in the module). |
| T-1-03 | Information Disclosure — captured stream contains other apps' content | `getDisplayMedia` stream | accept | This is the documented residual risk per CONTEXT.md D-04. The Chrome "Sharing your screen" indicator is the user-facing signal; the operator chose to share. No code-level mitigation is possible (the API is supposed to capture the screen). Documented as accepted risk in 01-RESEARCH.md §"Security Domain". | | T-1-03 | Information Disclosure — captured stream contains other apps' content | `getDisplayMedia` stream | accept | This is the documented residual risk per CONTEXT.md D-04 — the operator opted into the "Sharing your screen" indicator as the cost of the broader capture coverage. The Chrome "Sharing your screen" indicator is the user-facing signal; the operator chose to share. No code-level mitigation is possible (the API is supposed to capture the screen). Documented as accepted risk in 01-RESEARCH.md §"Security Domain". |
| T-1-NEW-03-01 | DoS — unbounded buffer growth on a stuck timestamp | `addChunk` + `trimAged` | mitigate | The trim function uses arrival timestamp; if a clock anomaly produces a negative or stuck `now`, the buffer is bounded above by SPEC §10 #9 (50 MB RAM ceiling) anyway. Defensive belt: the recorder also exposes `getBuffer().length` to SW so a healthchecker can observe growth. No active rate-limit needed for Phase 1. | | T-1-NEW-03-01 | DoS — unbounded buffer growth on a stuck timestamp | `addChunk` + `trimAged` | mitigate | The trim function uses arrival timestamp; if a clock anomaly produces a negative or stuck `now`, the buffer is bounded above by SPEC §10 #9 (50 MB RAM ceiling) anyway. Defensive belt: the recorder also exposes `getBuffer().length` to SW so a healthchecker can observe growth. No active rate-limit needed for Phase 1. |
</threat_model> </threat_model>
@@ -519,20 +524,20 @@ export class OffscreenLogger {
this.context = context; this.context = context;
} }
private logWithLevel(level: 'log' | 'warn' | 'error', ...args: any[]) { private logWithLevel(level: 'log' | 'warn' | 'error', ...args: unknown[]) {
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
console[level](`[OS:${this.context}] ${timestamp}`, ...args); console[level](`[OS:${this.context}] ${timestamp}`, ...args);
} }
log(...args: any[]) { log(...args: unknown[]) {
this.logWithLevel('log', ...args); this.logWithLevel('log', ...args);
} }
warn(...args: any[]) { warn(...args: unknown[]) {
this.logWithLevel('warn', ...args); this.logWithLevel('warn', ...args);
} }
error(...args: any[]) { error(...args: unknown[]) {
this.logWithLevel('error', ...args); this.logWithLevel('error', ...args);
} }
} }

View File

@@ -2,7 +2,7 @@
phase: 01-stabilize-video-pipeline phase: 01-stabilize-video-pipeline
plan: 04 plan: 04
type: tdd type: tdd
wave: 1 wave: 3
depends_on: ["02", "03"] depends_on: ["02", "03"]
files_modified: files_modified:
- src/offscreen/recorder.ts - src/offscreen/recorder.ts
@@ -14,9 +14,9 @@ requirements_addressed:
must_haves: must_haves:
truths: truths:
- "On module import, the offscreen opens exactly one port via `chrome.runtime.connect({ name: 'video-keepalive' })` AND emits exactly one `OFFSCREEN_READY` message via `chrome.runtime.sendMessage`" - "On module import, the offscreen opens exactly one port via `chrome.runtime.connect({ name: 'video-keepalive' })` AND emits exactly one `OFFSCREEN_READY` message via `chrome.runtime.sendMessage` — this is the long-lived port keepalive that replaces `chrome.alarms` for SW idle-timer resets (D-17)"
- "Both the port-connect and the OFFSCREEN_READY send happen AFTER `chrome.runtime.onMessage.addListener` registration (Pattern 4 ordering)" - "Both the port-connect and the OFFSCREEN_READY send happen AFTER `chrome.runtime.onMessage.addListener` registration (Pattern 4 ordering)"
- "When the open port fires its registered `onDisconnect` listener, the module synchronously reconnects (Pitfall 4 mitigation)" - "When the open port fires its registered `onDisconnect` listener, the module synchronously reconnects (Pitfall 4 mitigation; preserves the D-17 contract across the ~5 min port-lifetime cap)"
- "The port emits a `{ type: 'PING' }` postMessage on a ≤ 25 s interval (RESEARCH.md Pattern 5)" - "The port emits a `{ type: 'PING' }` postMessage on a ≤ 25 s interval (RESEARCH.md Pattern 5)"
- "Pre-emptive reconnect runs every 290 s (belt-and-braces against the ~5 min port-lifetime cap)" - "Pre-emptive reconnect runs every 290 s (belt-and-braces against the ~5 min port-lifetime cap)"
- "The port handles incoming `REQUEST_BUFFER` messages by responding with `{ type: 'BUFFER', chunks: <getBuffer()> }`" - "The port handles incoming `REQUEST_BUFFER` messages by responding with `{ type: 'BUFFER', chunks: <getBuffer()> }`"
@@ -51,10 +51,12 @@ a contract that survives SW unloads, port 5-min cap, and offscreen
bootstrap races. bootstrap races.
Output: Updated `src/offscreen/recorder.ts` — only this single file is Output: Updated `src/offscreen/recorder.ts` — only this single file is
touched. Plan 04 is an isolated unit-of-work; Plan 05 picks up the touched. Plan 04 is an isolated unit-of-work; Plan 05 (the SW-side
SW-side `onConnect` handler in parallel (Plan 04 runs alongside Plan 05 in `onConnect` counterparty) follows in wave 4 — Plan 04 lands first in wave 3
Wave 1 with NO file overlap — Plan 04 owns offscreen-side, Plan 05 owns so the SW-side host has a well-defined offscreen-side contract to bind
SW-side). against. Plan 04 owns the offscreen-side; Plan 05 owns the SW-side; the two
files do not overlap (Plan 04 touches only `src/offscreen/recorder.ts`,
Plan 05 touches only `src/background/index.ts`).
</objective> </objective>
<execution_context> <execution_context>

View File

@@ -2,7 +2,7 @@
phase: 01-stabilize-video-pipeline phase: 01-stabilize-video-pipeline
plan: 05 plan: 05
type: execute type: execute
wave: 2 wave: 4
depends_on: ["03", "04"] depends_on: ["03", "04"]
files_modified: files_modified:
- src/background/index.ts - src/background/index.ts
@@ -14,13 +14,14 @@ requirements_addressed:
must_haves: must_haves:
truths: truths:
- "`src/background/index.ts` no longer contains `addVideoChunkFromBlob`, `cleanupVideoBuffer`, `setupKeepalive`, `loadChunkFromIndexedDB`, `openIndexedDB`, or any `chrome.alarms` reference (buffer ownership moves to offscreen per D-16/D-19)" - "`src/background/index.ts` no longer contains `addVideoChunkFromBlob`, `cleanupVideoBuffer`, `setupKeepalive`, `loadChunkFromIndexedDB`, `openIndexedDB`, or any `chrome.alarms` reference buffer ownership moves to offscreen per D-16/D-19, and the alarms-driven keepalive is DELETED per D-18 (it never actually reset the SW idle timer; the long-lived port does)"
- "`src/background/index.ts` no longer calls `chrome.tabCapture.getMediaStreamId` (D-01 amendment)" - "`src/background/index.ts` no longer calls `chrome.tabCapture.getMediaStreamId` (D-01 amendment); video acquisition is now `getDisplayMedia` invoked from the offscreen module"
- "`src/background/index.ts` no longer handles `VIDEO_CHUNK` or `VIDEO_CHUNK_SAVED` (deleted Message types in Plan 03)" - "`src/background/index.ts` no longer handles `VIDEO_CHUNK` or `VIDEO_CHUNK_SAVED` (deleted Message types in Plan 03)"
- "SW has an `onConnect` listener that filters `port.name === 'video-keepalive'` and validates `port.sender?.id === chrome.runtime.id` (T-1-04 mitigation)" - "`src/background/index.ts` no longer contains any `chrome.tabs.onActivated` handler tied to the recording lifecycle (D-14: tab-switch re-attach is non-applicable under `getDisplayMedia`; D-15: operator tab-switching no longer interrupts recording, the buffer keeps filling regardless of active tab)"
- "SW has an `onConnect` listener that filters `port.name === 'video-keepalive'` and validates `port.sender?.id === chrome.runtime.id` (T-1-04 mitigation; this is the SW-side counterparty of the long-lived port keepalive per D-17)"
- "SW has an `onMessage` `OFFSCREEN_READY` case that resolves a pending readiness Promise (Pattern 4 SW side)" - "SW has an `onMessage` `OFFSCREEN_READY` case that resolves a pending readiness Promise (Pattern 4 SW side)"
- "SW's `SAVE_ARCHIVE` and `GET_VIDEO_BUFFER` handlers fetch the buffer via the port (`REQUEST_BUFFER` → wait for `BUFFER`) instead of holding their own `videoBuffer` array" - "SW's `SAVE_ARCHIVE` and `GET_VIDEO_BUFFER` handlers fetch the buffer via the port (`REQUEST_BUFFER` → wait for `BUFFER`) instead of holding their own `videoBuffer` array"
- "SW's `ensureOffscreen` uses `chrome.offscreen.Reason.DISPLAY_MEDIA` (not `USER_MEDIA`)" - "SW's `ensureOffscreen` uses `chrome.offscreen.Reason.DISPLAY_MEDIA` (not `USER_MEDIA`) per D-02"
- "SW's `onInstalled` listener calls `indexedDB.deleteDatabase('VideoRecorderDB')` once as a cleanup pass (RESEARCH.md Runtime State Inventory)" - "SW's `onInstalled` listener calls `indexedDB.deleteDatabase('VideoRecorderDB')` once as a cleanup pass (RESEARCH.md Runtime State Inventory)"
- "`npx tsc --noEmit` exits 0" - "`npx tsc --noEmit` exits 0"
artifacts: artifacts:
@@ -49,9 +50,11 @@ ring-buffer state and helpers (now owned by offscreen per D-16), DELETE the
chrome.alarms keepalive (D-18), DELETE the IndexedDB code path (D-19), chrome.alarms keepalive (D-18), DELETE the IndexedDB code path (D-19),
DELETE the `chrome.tabCapture.getMediaStreamId` call (D-01 amendment), DELETE the `chrome.tabCapture.getMediaStreamId` call (D-01 amendment),
DELETE the `VIDEO_CHUNK` / `VIDEO_CHUNK_SAVED` message handlers (their DELETE the `VIDEO_CHUNK` / `VIDEO_CHUNK_SAVED` message handlers (their
message types were removed in Plan 03), and WIRE the SW-side `onConnect` message types were removed in Plan 03), DELETE any `chrome.tabs.onActivated`
handler against the `'video-keepalive'` port that Plan 04 opens from the re-attach plumbing (D-14: not applicable under the new capture API; D-15:
offscreen. operator tab switches no longer interrupt the recording), and WIRE the
SW-side `onConnect` handler against the `'video-keepalive'` port that
Plan 04 opens from the offscreen.
Purpose: REQ-video-ring-buffer's data flow on export is `popup → Purpose: REQ-video-ring-buffer's data flow on export is `popup →
SAVE_ARCHIVE → SW → REQUEST_BUFFER (via port) → offscreen → BUFFER (via SAVE_ARCHIVE → SW → REQUEST_BUFFER (via port) → offscreen → BUFFER (via
@@ -200,10 +203,12 @@ If `chrome.offscreen.Reason.DISPLAY_MEDIA` is NOT in the current `@types/chrome`
(13) Delete `openIndexedDB` function — currently lines 507-520. (13) Delete `openIndexedDB` function — currently lines 507-520.
(14) Verify no `chrome.tabs.onActivated` listener exists in the file (D-14 / D-15: tab-switch handling is non-applicable under the new capture API). Run `grep -n "chrome.tabs.onActivated" src/background/index.ts`. If the grep returns any hits, DELETE those lines (the entire listener callback block). If the grep returns nothing, log "D-14/D-15 satisfied: no tab-switch handler found in SW" in the task summary.
After ALL these deletions, run `npx tsc --noEmit`. It MUST exit 0. If `VideoChunk` is reported as unused after the deletes, that indicates a function that needs it was inadvertently lost; STOP and audit. After ALL these deletions, run `npx tsc --noEmit`. It MUST exit 0. If `VideoChunk` is reported as unused after the deletes, that indicates a function that needs it was inadvertently lost; STOP and audit.
</action> </action>
<verify> <verify>
<automated>npx tsc --noEmit && [ $(grep -cE "addVideoChunkFromBlob|cleanupVideoBuffer|setupKeepalive|loadChunkFromIndexedDB|openIndexedDB|getMediaStreamId|chrome\.alarms" src/background/index.ts) -eq 0 ]</automated> <automated>npx tsc --noEmit && [ $(grep -cE "addVideoChunkFromBlob|cleanupVideoBuffer|setupKeepalive|loadChunkFromIndexedDB|openIndexedDB|getMediaStreamId|chrome\.alarms|chrome\.tabs\.onActivated" src/background/index.ts) -eq 0 ]</automated>
</verify> </verify>
<acceptance_criteria> <acceptance_criteria>
- `npx tsc --noEmit` exits 0 - `npx tsc --noEmit` exits 0
@@ -215,6 +220,7 @@ After ALL these deletions, run `npx tsc --noEmit`. It MUST exit 0. If `VideoChun
- `grep -v '^#' src/background/index.ts | grep -c "getMediaStreamId"` returns 0 - `grep -v '^#' src/background/index.ts | grep -c "getMediaStreamId"` returns 0
- `grep -v '^#' src/background/index.ts | grep -c "VIDEO_CHUNK_SAVED"` returns 0 - `grep -v '^#' src/background/index.ts | grep -c "VIDEO_CHUNK_SAVED"` returns 0
- `grep -v '^#' src/background/index.ts | grep -c "chrome.alarms"` returns 0 - `grep -v '^#' src/background/index.ts | grep -c "chrome.alarms"` returns 0
- `grep -v '^#' src/background/index.ts | grep -c "chrome.tabs.onActivated"` returns 0 (D-14/D-15 mitigation)
- `grep -c "DISPLAY_MEDIA" src/background/index.ts` returns 1 - `grep -c "DISPLAY_MEDIA" src/background/index.ts` returns 1
- `grep -c "as any" src/background/index.ts` returns 0 (CLAUDE.md rule) - `grep -c "as any" src/background/index.ts` returns 0 (CLAUDE.md rule)
- File line count reduced from 536 to roughly 380-400 lines (allow ±40) - File line count reduced from 536 to roughly 380-400 lines (allow ±40)
@@ -435,7 +441,7 @@ Commit cadence: TWO commits.
</verification> </verification>
<success_criteria> <success_criteria>
- `src/background/index.ts` carries no buffer state, no alarms, no IndexedDB plumbing, no `tabCapture` calls - `src/background/index.ts` carries no buffer state, no alarms, no IndexedDB plumbing, no `tabCapture` calls, no `chrome.tabs.onActivated` re-attach plumbing
- SW has `onConnect` handler matching the offscreen's port (Plan 04 counterparty) - SW has `onConnect` handler matching the offscreen's port (Plan 04 counterparty)
- SW has `OFFSCREEN_READY` handshake handler resolving a readiness Promise - SW has `OFFSCREEN_READY` handshake handler resolving a readiness Promise
- T-1-04 mitigations in place on BOTH onConnect (sender + port name) and onMessage (sender) - T-1-04 mitigations in place on BOTH onConnect (sender + port name) and onMessage (sender)

View File

@@ -2,12 +2,13 @@
phase: 01-stabilize-video-pipeline phase: 01-stabilize-video-pipeline
plan: 06 plan: 06
type: execute type: execute
wave: 2 wave: 5
depends_on: ["03"] depends_on: ["03", "05"]
files_modified: files_modified:
- vite.config.ts - vite.config.ts
- offscreen/index.ts - offscreen/index.ts
- offscreen/index.html - offscreen/index.html
- src/background/index.ts
autonomous: true autonomous: true
requirements: requirements:
- REQ-video-ring-buffer - REQ-video-ring-buffer
@@ -16,11 +17,11 @@ requirements_addressed:
must_haves: must_haves:
truths: truths:
- "`vite.config.ts` no longer contains the `copy-offscreen` inline plugin (the 200+-line block including IndexedDB plumbing, codec fallback chain, and `mediaRecorder` shadow is GONE)" - "`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" - "`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)" - "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)" - "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" - "`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`" - "No mention of `VideoRecorderDB`, `openIndexedDB`, `chromeMediaSource`, or `copy-offscreen` remains in `vite.config.ts`"
artifacts: artifacts:
- path: "vite.config.ts" - path: "vite.config.ts"
@@ -106,8 +107,8 @@ const url = chrome.runtime.getURL('offscreen/index.html');
| Threat ID | Category | Component | Disposition | Mitigation Plan | | 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. 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-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. After this plan, a `find offscreen/ -type f` produces no output. Grep gate: `[ ! -d offscreen ]` exits 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> </threat_model>
<tasks> <tasks>

View File

@@ -2,7 +2,7 @@
phase: 01-stabilize-video-pipeline phase: 01-stabilize-video-pipeline
plan: 07 plan: 07
type: execute type: execute
wave: 3 wave: 6
depends_on: ["05", "06"] depends_on: ["05", "06"]
files_modified: files_modified:
- tests/fixtures/last_30sec.webm - tests/fixtures/last_30sec.webm
@@ -139,7 +139,7 @@ The full Phase-1 Mokosh build:
- Archive is delivered via `chrome.downloads` - Archive is delivered via `chrome.downloads`
</what-built> </what-built>
<verify> <verify>
<automated>which ffprobe && ffprobe -v error -f matroska -i tests/fixtures/last_30sec.webm; test $? -eq 0</automated> <automated>test -f tests/fixtures/last_30sec.webm && which ffprobe && ffprobe -v error -f matroska -i tests/fixtures/last_30sec.webm; test $? -eq 0</automated>
</verify> </verify>
<how-to-verify> <how-to-verify>
**Pre-flight (automated, do these in a bash shell before opening Chrome):** **Pre-flight (automated, do these in a bash shell before opening Chrome):**

View File

@@ -1017,10 +1017,11 @@ stream.getTracks().forEach((track) => {
**If this table contains items:** The planner should treat them as **If this table contains items:** The planner should treat them as
candidates for user verification during `/gsd-plan-phase` review. candidates for user verification during `/gsd-plan-phase` review.
## Open Questions ## Open Questions (RESOLVED)
1. **Will `MediaRecorder.start(2000)` produce ffprobe-clean WebM on a 1. **Will `MediaRecorder.start(2000)` produce ffprobe-clean WebM on a
typical screen-cap?** typical screen-cap?**
- **RESOLVED:** Cluster-boundary alignment is resolved by the D-12 ffprobe acceptance gate (enforced in Plan 03 Task 2 verify path + Plan 07 Task 1) and the D-13 restart-segments fallback (pre-staged as a commented skeleton in `src/offscreen/recorder.ts` per Plan 03; activated by re-plan after Plan 07 if the gate fails).
- What we know: Cluster boundaries align with keyframes; Chrome - What we know: Cluster boundaries align with keyframes; Chrome
keyframes appear every ~3-5 s by default (vp9 `kf_max_dist=100` on keyframes appear every ~3-5 s by default (vp9 `kf_max_dist=100` on
a 30 fps stream); timeslice does NOT force keyframes. a 30 fps stream); timeslice does NOT force keyframes.
@@ -1034,6 +1035,7 @@ candidates for user verification during `/gsd-plan-phase` review.
this in the success criteria. this in the success criteria.
2. **Does the 5-minute port lifetime kill the recording session?** 2. **Does the 5-minute port lifetime kill the recording session?**
- **RESOLVED:** Plan 04's 290 s pre-emptive reconnect logic plus the synchronous onDisconnect → connectPort reconnect path mitigate the cap whether it applies to port lifetime or SW lifetime; either way the offscreen reconnects within seconds and the buffer is unaffected.
- What we know: Multiple corroborating community sources cite a ~5 - What we know: Multiple corroborating community sources cite a ~5
minute hard cap on long-lived ports. minute hard cap on long-lived ports.
- What's unclear: Whether the cap applies to *port lifetime* (the - What's unclear: Whether the cap applies to *port lifetime* (the
@@ -1045,6 +1047,7 @@ candidates for user verification during `/gsd-plan-phase` review.
reconnect is still harmless. reconnect is still harmless.
3. **What's the exact crxjs path-emit behavior for the offscreen entry?** 3. **What's the exact crxjs path-emit behavior for the offscreen entry?**
- **RESOLVED:** Plan 06 Task 2 performs runtime verification — runs `npm run build`, inspects `dist/` for whichever of `dist/src/offscreen/index.html` or `dist/offscreen/index.html` was emitted, then edits `src/background/index.ts`'s `chrome.runtime.getURL(...)` argument to match (this is why Plan 06 now lists `src/background/index.ts` in files_modified per the iteration-1 dependency-correctness fix).
- What we know: The discussion #919 working answer uses - What we know: The discussion #919 working answer uses
`input: { offscreen: 'src/offscreen/offscreen.html' }` and SW `input: { offscreen: 'src/offscreen/offscreen.html' }` and SW
fetches `chrome.runtime.getURL('src/offscreen/offscreen.html')`. fetches `chrome.runtime.getURL('src/offscreen/offscreen.html')`.