Phase 3 is verification-only; /gsd-ui-phase 3 trigger on "page" keyword
is a false positive. UI-SPEC.md confirms no new user-facing UI surface
in scope; locks the Phase 1 design system (Lora + IBM Plex Sans + Loom
palette + Mokosh mark + tokens.css + 17 i18n keys) as read-only
inherited context; declares minimal probe-page conventions for
internal Puppeteer test fixtures (Plans 03-01..03-05 per D-P3-01).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User invoked /gsd-plan-phase 3 and answered both gate questions before the
workflow correctly exited at the UI Design Contract gate (per workflow rule
that manual invocations cannot nested-Skill-spawn /gsd-ui-phase due to
AskUserQuestion-in-subcontext issue #1009).
Preferences saved at .plan-phase-preferences.md for the next plan-phase
invocation (after /gsd-ui-phase 3 produces UI-SPEC.md):
- UI gate: generate UI-SPEC.md first (chosen — most canonical; verification
caveat noted for /gsd-ui-phase to consider)
- Research gate: research first (light) — scope-limited to puppeteer.Page.metrics
+ rrweb alpha-pin status (NOT rrweb v2 upgrade implementation, NOT masking)
File auto-deletes when /gsd-plan-phase 3 honors these preferences.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
state.record-session CLI incorrectly flipped status to "completed" + percent to
100% (since 18/18 currently-known plans are done — but that's a CLI inference
bug; Phase 3 + Phase 4 are still pending so milestone is NOT complete).
Restored: status=ready_to_plan, percent=50% (2/4 phases truly complete).
Phase 3 CONTEXT.md at:
.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-CONTEXT.md
DISCUSSION-LOG.md sibling captures the alternatives considered.
5 plans + 4 D-P3-* locked decisions ready for /gsd-plan-phase 3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Verifier returned human_needed with 4/5 truths VERIFIED (T1-T4) + T5 UNCERTAIN
because Plan 02-04 Task 4 contract literally typed checkpoint:human-verify gate=blocking
and the operator empirical "approved" ack wasn't on record.
T5 (operator clicks SAVE → ZIP produced in <5s with correct layout + Blob URL)
is OVERRIDDEN to VERIFIED based on:
1. User explicit delegation 2026-05-20: "why do i need to do all of this? It's on
you to test..." — established that automation covers what automation can cover.
2. New saved memory feedback-trust-harness-over-manual-uat.md (same session):
reserve operator empirical UAT for surfaces automation genuinely cannot verify
(brand judgment, ergonomics). For deep-pipeline Phase 2 work, every operator-
checklist surface IS harness-covered.
3. Harness assertion coverage of every step:
- (a) <5s latency → A25 empirical via Puppeteer
- (b) 5-entry archive layout → A28 set-equality
- (c) 8-field meta.json schema → A26 + tests/build/strict-meta-json-validation.test.ts
- (d) video playback → Phase 1 VERIFICATION.md empirical (D-13 unchanged)
- (e) blob: URL pattern → A24 empirical
4. Alpha distribution build covers real-world OS-archive-manager layer outside
in-session verification scope.
Plan 02-04 Task 4 was authored before the saved-memory principle was established;
the checkpoint contract reflects an older operating mode.
Status: passed (with 1 override applied; override_notes captured in frontmatter)
Score: 5/5
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rule 1 deviation surfaced during the first UAT harness end-to-end run:
A27.7 originally forbade ALL chrome-extension:// URLs in meta.urls. Empirical
reality: the harness environment legitimately captures chrome-extension://
URLs (the welcome.html page opens automatically on first install per Plan
01-10; the harness page itself at chrome-extension://<id>/tests/uat/
extension-page-harness.html is a real active tab). The production tracker
(src/background/tab-url-tracker.ts:79 URL_SCHEME_ALLOW) EXPLICITLY permits
the chrome-extension:// scheme.
F2's actual contract was: empty tracker → urls: [] (NOT a single fake
chrome-extension:// sentinel). With real URLs present, the F2 fallback path
is definitionally not triggered. The refined A27.7 expresses F2's actual
semantics: "empty-tracker fallback NOT triggered" — verified by
`realHttpUrls.length >= 2` (proof the tracker was populated by real
onActivated events, NOT by the F2 empty-state fallback).
This is a strict semantic improvement: the original A27.7 would have hidden
a real production regression (if the tracker started excluding chrome-extension
URLs, A27 would have continued to PASS misleadingly). The refined contract
catches the intended F2 regression (empty-tracker fallback → fake sentinel)
without false-positiving on legitimate chrome-extension active tabs.
Empirical UAT verification: 29/29 GREEN with the fix in place.
- A27.4 ✓ meta.urls contains https://example.com/
- A27.5 ✓ meta.urls contains https://www.iana.org/
- A27.7 ✓ F2 contract: real http(s) URLs present (length=2)
- A28.* ✓ 5-entry zip-layout strict
Wire A25 into the UAT harness as the binding empirical gate for
REQ-archive-export-latency / SPEC §10 #6 (5000ms hard ceiling end-to-end
from SAVE_ARCHIVE dispatch to zip-on-disk).
Architecture:
- Page-side assertA25 records t0 (performance.now) + t0Wall (Date.now)
+ tAck bookends around the chrome.runtime.sendMessage(SAVE_ARCHIVE)
call. Returns A25Result extending AssertionRecord with the 3 timing
fields + ackSuccess flag.
- Host-side driveA25(page, downloadsDir) snapshots zip dir BEFORE
page.evaluate dispatch, polls for new-or-overwritten .zip via mtime
delta (mirrors A12/A13 overwrite-aware pattern), uses page-supplied
t0Wall as the host anchor for the dispatch→file-on-disk latency
check (NOT a host-side Date.now captured before page.evaluate, which
would include setupFreshRecording + 11s segment-settle wall time and
always fail the 5s budget).
[Rule 1 - Bug] Initial implementation used host-side Date.now() captured
before page.evaluate as the latency anchor — this incorrectly included
the 11s segment-settle window in the budget. First run observed
A25.3=11188ms (FAIL). Fix: page-side captures Date.now() at the
SAVE_ARCHIVE dispatch instant (AFTER setupFreshRecording + segment-settle
complete) and returns it as t0Wall in A25Result; the driver uses this
as the canonical host anchor. Result on re-run: A25.3=61ms (GREEN, well
under 5s SLO). Documented per T-02-04-02 disposition (bracket only the
SAVE dispatch, not the broader test orchestration).
Files modified:
- tests/uat/extension-page-harness.ts (+~115 lines): assertA25 +
A25_* constants + A25Result interface
- tests/uat/lib/harness-page-driver.ts (+~95 lines): driveA25 +
A25_HOST_POLL_TIMEOUT_MS const + A25_LATENCY_CEILING_MS const
- tests/uat/harness.test.ts (+~15 lines): import driveA25, wrap with
downloadsDir, append to drivers list
Verification:
- HEADLESS=1 npm run test:uat → 26/26 GREEN
- elapsedAck=60ms, host-side delta=61ms (both well under 5000ms SLO)
- npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts
→ 13/13 GREEN (Tier-1 FORBIDDEN_HOOK_STRINGS unchanged at 12)
- npx tsc --noEmit → clean
Plan 02-04 scope: 2/3 tasks landed (A24 + A25); Task 3 adds
A26 (meta.json 8-field) + A27 (multi-tab strict) + A28 (archive-layout strict).
Wire A24 into the Plan 01-13 Approach B UAT harness as the binding empirical
gate for D-P2-01. A24 verifies end-to-end that SAVE_ARCHIVE → chrome.downloads.
download receives a `blob:` URL prefix (NOT `data:application/zip;base64,`),
closing audit P0-6 functionally. The Plan 02-02 unit tests pin the wire-format
at the SW↔offscreen boundary; A24 pins it at the chrome.downloads platform
boundary through a real Chrome instance.
Strategy: chrome.downloads.onCreated listener captures the URL cross-realm.
The plan's <action> block proposed a chrome.downloads.download monkey-patch
installed in the harness page realm — but that intercepts only same-realm
calls, missing the SW's call. The canonical cross-realm capture pattern is
chrome.downloads.onCreated (fires for any download initiated by any extension
realm, with the full DownloadItem including .url). Documented as a deviation
from the plan's pseudo-code in SUMMARY.md (Rule 1 — bug fix vs the pseudo-code
strategy; same A24 contract verified, correct mechanism).
Files modified:
- tests/uat/extension-page-harness.ts (+~150 lines): assertA24 + A24_* constants
- tests/uat/lib/harness-page-driver.ts (+~30 lines): driveA24 page.evaluate wrapper
- tests/uat/harness.test.ts (+~10 lines): import driveA24, append to drivers list
Verification:
- HEADLESS=1 npm run test:uat → 25/25 GREEN (24 baseline + A24)
- capturedUrl observed: blob:chrome-extension://lpgnfoop.../...
- npx vitest run → 171/171 GREEN (no regression)
- Tier-1 FORBIDDEN_HOOK_STRINGS gate → 13/13 GREEN (12 strings preserved)
- npx tsc --noEmit → clean
Plan 02-04 scope: 1/3 tasks landed (A24); Tasks 2-3 add A25+A26+A27+A28
(latency, meta.json shape, multi-tab strict, REQ-archive-layout strict).
SUMMARY.md documents:
- 3 RED tests in tests/background/blob-url-download.test.ts flipped GREEN
(wire-format polarity guard, 6 MB latency + wire-format, revoke lifecycle).
- 6 files modified (3 prod source + 3 test files; +518 / -35 lines).
- Wire-format extension: 3 new PortMessageType variants on keepalivePort.
- Operator-facing improvement: archives >2 MB now download successfully
(was: silent failure with data:URL Network error).
- Rule 3 deviation: extended Plan 02-01 test helpers with the offscreen-side
CREATE_DOWNLOAD_URL → DOWNLOAD_URL → REVOKE_DOWNLOAD_URL round-trip
simulation pattern + capturedArchiveBytes bytes capture. This pattern
is reusable by Plan 02-03 and was anticipated in Plan 02-01 SUMMARY.
- Forward link: Plan 02-03 (meta.urls + tab-url-tracker) is unblocked;
Plan 02-04 (UAT harness A24+) is unblocked.
Verification:
- npx tsc --noEmit: clean
- npm run build: clean
- npm run build:test: clean
- tests/background/blob-url-download.test.ts: 3/3 GREEN
- Tier-1 FORBIDDEN_HOOK_STRINGS: 13/13 GREEN (unchanged)
- Full vitest: 163 passed / 8 failed (was 159 passed / 12 failed); +4 GREEN
net delta. 8 remaining RED are exactly Plan 02-03 territory.
Production changes (src/background/index.ts):
- pendingDownloadUrlResolvers Map<requestId, resolver> routes DOWNLOAD_URL
responses back to the in-flight downloadArchive Promise; mirrors the
pendingBufferRequests pattern from the BUFFER round-trip so port
replacement mid-mint does not lose the response.
- pendingRevokes Map<downloadId, url> tracks (downloadId → minted blob:URL)
for the chrome.downloads.onChanged revoke dispatch.
- onConnect port message sink extended with DOWNLOAD_URL routing branch
(alongside existing PING/BUFFER routing).
- downloadArchive rewritten: encode archive via blobToBase64 → post
CREATE_DOWNLOAD_URL on videoPort → await DOWNLOAD_URL response (race
against 5s BLOB_URL_MINT_TIMEOUT_MS) → reject empty / non-blob: URLs
(T-02-02-03 mitigation) → call chrome.downloads.download → register
(downloadId, url) in pendingRevokes. NO data:URL fallback — typed
errors route through saveArchive's catch to RECORDING_ERROR.
- chrome.downloads.onChanged listener registered at module init:
on terminal state ('complete' / 'interrupted'), posts REVOKE_DOWNLOAD_URL
to videoPort and clears the pendingRevokes entry.
Deviation (Rule 3 — auto-fix blocking issue):
- Plan 02-01's test helpers in blob-url-download.test.ts +
meta-json-urls-schema.test.ts + strict-meta-json-validation.test.ts
modeled only the REQUEST_BUFFER → BUFFER round-trip, not the new
CREATE_DOWNLOAD_URL → DOWNLOAD_URL round-trip Plan 02-02 introduces.
Without the test-side mint simulation, the SW's downloadArchive
times out at the offscreen mint step → chrome.downloads.download
never called → ALL existing meta.json tests timeout.
- Each helper extended with a tryFireDownloadUrl block that decodes
the CREATE_DOWNLOAD_URL.dataBase64, mints a Node-native blob:URL via
URL.createObjectURL, captures the archive bytes for downstream
JSZip extraction (capturedArchiveBytes), and replies DOWNLOAD_URL.
Test 3 (revoke lifecycle) additionally shims port.postMessage to
call URL.revokeObjectURL on receipt of REVOKE_DOWNLOAD_URL — the
test-side equivalent of src/offscreen/recorder.ts handleCreateDownloadUrl.
- Pre-existing Plan-02-02-era TODO comments in both test files
explicitly anticipated this extension ("Plan 02-03 implementer will
likely need a different helper, e.g. spy on URL.createObjectURL").
Verification (full §verification block from plan):
- npx tsc --noEmit: clean
- npm run build: clean
- npx vitest run tests/background/blob-url-download.test.ts: 3/3 GREEN (was 3 RED)
- npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts: 13/13 GREEN
- npm test full suite: 163 passed / 8 failed (was 159 passed / 12 failed);
net delta +4 GREEN = 3 RED→GREEN flips + 1 ffprobe-flaky pass. 8 remaining
RED are exactly the Plan 02-03 territory (5 meta-json-urls-schema + 3
strict-meta-json-validation RED tests).
- grep -c "data:application/zip;base64," src/background/index.ts: 0 (gone)
- grep -c "blob:" src/background/index.ts: 8 (new pipeline)
- grep -c "chrome.downloads.onChanged" src/background/index.ts: 5 (listener wired)
- dist/ post-build: 0 "data:application/zip;base64," matches; 1 file with
"chrome.downloads.onChanged" (the SW chunk).
- onPortMessage gains CREATE_DOWNLOAD_URL + REVOKE_DOWNLOAD_URL branches.
- handleCreateDownloadUrl helper decodes SW-supplied base64 archive bytes
via base64ToBlob, mints a blob:URL via URL.createObjectURL, and posts
DOWNLOAD_URL{requestId,url} back on the keepalivePort. On any failure
(empty payload, decode throw, mint throw) responds with url:'' so the
SW's outer timeout / typed error path fires cleanly.
- mintedDownloadUrls Set tracks minted URLs purely as a diagnostic signal;
unknown-URL revokes get a warn but still execute (WHATWG spec: revoke
on unknown URL is a no-op).
- base64ToBlob added to the existing src/shared/binary import.
- No changes to bootstrap/connectPort/ping/segment-rotation/__MOKOSH_UAT__
test hooks. Concurrent mints are allowed (URL minting is stateless
per-Blob); only encodeAndSendBuffer needs its existing in-flight guard.
Architectural rationale (D-P2-01): SW lacks URL.createObjectURL per
DEC-006; offscreen has it. Reusing the existing keepalivePort (D-17)
avoids two connect-overhead penalties per save flow.
- PortMessageType union grows 4 → 7 entries adding the D-P2-01 Blob URL
migration triplet (CREATE_DOWNLOAD_URL, DOWNLOAD_URL, REVOKE_DOWNLOAD_URL).
- PortMessage interface gains optional dataBase64, mimeType, url fields
following the same optional-tagged-union pattern as the existing
segments? field. Wire format reuses the D-12 base64 precedent from
src/shared/binary.ts (chrome.runtime.Port JSON-serializes payloads).
- Docstring above the union explains the SW↔offscreen mint/revoke
lifecycle and points to .planning/phases/02-stabilize-export-pipeline/
02-CONTEXT.md D-P2-01 for the full architectural rationale.
- No SessionMetadata changes — meta.urls migration is Plan 02-03 territory.
Plan 02-01 Wave 0 RED gate closed. Three failing test files (16 it()
blocks total: 11 RED + 5 GREEN regression guards) pin the locked
decisions for Phase 2 ahead of Plans 02-02 + 02-03 implementation:
- blob-url-download.test.ts (3 RED) — D-P2-01 offscreen Blob URL
pipeline (closes audit P0-6: base64 data: URL → blob: URL).
- meta-json-urls-schema.test.ts (5 RED) — D-P2-02 meta.url → meta.urls
migration + F2 empty-tracker → urls:[] resolution.
- strict-meta-json-validation.test.ts (3 RED + 5 GREEN) — D-P2-03
strict 8-field schema validation with EXPECTED_KEYS pin including
planner-suggested `schemaVersion` 8th field.
Test count delta: 155 GREEN → 159 GREEN + 11 RED (+4 GREEN regression
guards, +11 RED test contracts). Vitest reporter:
Test Files 4 failed | 27 passed (31)
Tests 12 failed | 159 passed (171)
(12 failed = 3 + 5 + 3 RED from this plan + 1 pre-existing flaky
ffprobe test in webm-remux.test.ts — out of scope; documented in
SUMMARY.md Deferred Issues.)
Tier-1 grep gate: 13/13 GREEN preserved (this plan touches no
production code).
Planner-resolved tensions carried forward in SUMMARY.md:
- D-P2-03 'non-empty urls[]' vs CONTEXT.md permissive empty-array →
F2 resolved in favor of permissive (Test 3 of Task 3 relaxed).
- 8th field name `schemaVersion` → tentative planner pick;
Plan 02-03 implementer commits to schemaVersion: '2' const.
- tab-url-tracker module seam → planner-suggested name
`src/background/tab-url-tracker.ts` with getTabUrlsSeen() export.
- Plan claim 'ALL 8 fail' reconciled honestly: 3 RED + 5 GREEN
regression guards (timestamp/semver/totalEvents/buffer-seconds/
duration-minutes already match current 7-field shape).
Plan suggestions reconciled with reality:
- vitest env: 'node' not 'jsdom' (Node 24 has URL/Blob/performance
globals; jsdom not in devDeps). FileReader polyfill inline.
- Task 2 Test 1 source-text scan instead of tsc-compile-failure
(vitest.config.ts typecheck:{enabled:false}).
Per worktree-mode constraint: STATE.md, ROADMAP.md, REQUIREMENTS.md
NOT modified. The orchestrator owns those writes after all worktree
agents in Wave 0 complete.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan 02-01 Task 3 RED gate. Eight strict-validation tests pin D-P2-03
(strict 8-field meta.json schema) plus F2 plan-checker-iter-1
resolution (empty urls[] permitted for whole-desktop-no-tab sessions).
Tests (3 RED-today + 5 GREEN-today regression guards; ALL 8 GREEN
after Plan 02-03):
1. RED — Object.keys(meta).length === 8.
2. GREEN — timestamp matches ISO-8601 Z-suffix regex.
3. RED — urls is Array of valid URLs (empty permitted per F2).
4. GREEN — extensionVersion matches semver.
5. GREEN — totalEvents is non-negative integer.
6. GREEN — videoBufferSeconds === 30 (CON-video-window).
7. GREEN — logDurationMinutes === 10 (CON-event-log-window).
8. RED — no extra fields beyond EXPECTED_KEYS.
RED evidence (vitest 4.1.6 against current HEAD):
× Test 1: meta.json has 7 fields; D-P2-03 requires exactly 8.
Current keys: [timestamp, url, userAgent, extensionVersion,
videoBufferSeconds, logDurationMinutes, totalEvents].
× Test 3: meta.urls is not an Array. Got: undefined.
× Test 8: meta.json contains extra (unexpected) fields: ["url"].
PLANNER-RESOLVED TENSIONS (documented in file header):
- D-P2-03 'non-empty urls[]' vs CONTEXT.md permissive empty-array:
resolved in favor of the permissive clause (F2 — empty is the
canonical representation of whole-desktop-no-tab sessions).
- 8th field name 'schemaVersion': tentative planner pick to mark
the D-P2-02 url→urls breaking-change cutover.
- Plan's 'ALL 8 fail' claim vs reality: 5 of 8 already pass under
the current 7-field shape (timestamp, semver, totalEvents,
videoBufferSeconds, logDurationMinutes). These stay GREEN as
regression guards after Plan 02-03 lands.
EXPECTED_KEYS constant:
['timestamp', 'urls', 'userAgent', 'extensionVersion',
'videoBufferSeconds', 'logDurationMinutes', 'totalEvents',
'schemaVersion']
Plan 02-03 implementer MUST add `schemaVersion` (recommended value:
'2') to satisfy Tests 1 + 8 simultaneously.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan 02-01 Task 2 RED gate. Five failing tests pin D-P2-02 (meta.json
url→urls migration) and the F2 plan-checker-iter-1 resolution (empty-
tracker → urls:[], no sentinel fallback) ahead of Plan 02-03.
Tests:
1. SessionMetadata interface in src/shared/types.ts has 'urls: string[]'
and no 'url:' field. Source-text scan (typecheck disabled in
vitest.config.ts so tsc-failure pin would be a no-op).
2. createArchive emits meta.json with Array urls and no url field.
3. meta.urls deduplicates repeated URLs (first-seen-first order).
4. meta.urls filters chrome:// + about:; includes chrome-extension://.
5. Empty tracker → meta.urls === [] (NOT undefined/null/[origin]).
RED evidence (vitest 4.1.6 against current HEAD):
× Test 1: SessionMetadata interface body does not contain a
'urls: string[]' field (and still contains 'url:').
× Test 2: meta.urls is not an Array. Got: undefined.
× Tests 3+4+5: src/background/tab-url-tracker.ts does not exist —
Plan 02-03 GREEN gate. Each expect.fail emits the precise
contract for the GREEN flip (export name getTabUrlsSeen(),
dedup Set semantics, first-seen-first order, URL filter spec,
empty-array empty-tracker resolution).
Module seam (Plan 02-03 implements):
src/background/tab-url-tracker.ts
export function getTabUrlsSeen(): string[]
Fed by chrome.tabs.onUpdated + chrome.tabs.onActivated (per DEC-011
Amendment 1 'tabs' permission grant).
Baseline: 155 GREEN preserved (no regressions); this plan now has 8
NEW RED tests total (Task 1: 3 + Task 2: 5).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan 02-01 Task 1 RED gate. Three failing tests pin D-P2-01
(offscreen-minted Blob URL pipeline) ahead of Plan 02-02 implementation:
1. chrome.downloads.download is called with a blob: URL and NOT a
data:application/zip;base64, URL (closes audit P0-6).
2. A 6 MB archive completes through downloadArchive in under 5 s AND
emits a blob: URL (REQ-archive-export-latency; vi-mocked
remuxSegments short-circuits the muxer for the 6 MB stress path).
3. URL.revokeObjectURL is scheduled with the minted URL after
chrome.downloads.onChanged fires 'complete' (lifecycle hygiene).
RED evidence (vitest 4.1.6 against current HEAD):
× Test 1: chrome.downloads.download was called with
url='data:application/zip;base64,UEsDBAoAAAAAAL1qtFw...'
— D-P2-01 forbids data:application/zip;base64, prefix.
× Test 2: chrome.downloads.download was called with
url='data:application/zip;base64,...' at the 6 MB scale —
D-P2-01 requires blob: prefix.
× Test 3: URL.revokeObjectURL was never called after
chrome.downloads.onChanged 'complete' fired
(chrome.downloads.onChanged._callbacks.length === 0 at probe time).
Implementation notes:
- vitest default env is 'node' (vitest.config.ts); Node 24 ships
URL.createObjectURL + URL.revokeObjectURL + performance as globals,
so no jsdom override is required.
- FileReader is NOT in Node 24; added a minimal FileReader polyfill
(delegates to Blob.arrayBuffer()) so JSZip's Blob ingestion works.
- Test 2 mocks remuxSegments via vi.doMock to bypass muxer monotonic-
timestamp constraints for the synthetic 6 MB payload.
- Tests 1 + 3 drive the SW with the canonical 3-slice raw-3ebml-concat
fixture (same byte offsets as tests/background/webm-remux.test.ts).
- T-02-01-01 mitigation: grep -c '\.skip' returns 0.
Baseline: 155 GREEN preserved (no regressions); this plan adds 3 NEW
RED tests. Plan 02-02 flips them GREEN.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Delete HANDOFF.json (one-shot artifact; resume succeeded)
- Update STATE.md Current Position to reflect re-phased structure (Phase 1 of 4 closed; Phase 2 planning complete)
- Update Session Continuity with current resume context
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan-checker iteration 2 surfaced an informational stale-reference: the
Constraints section's CON-manifest-permissions bullet still listed the
original SPEC §7 5-permission set (`tabCapture`, `activeTab`, `downloads`,
`scripting`, `storage`). This was superseded by:
- Phase 01 DEC-003 Amendment (retired `tabCapture`; added `desktopCapture`
+ `offscreen` + `notifications`)
- Phase 02 DEC-011 Amendment 1 (added `tabs` for D-P2-02 meta.urls feature
at commit 9dcfcf0)
Updated CON-manifest-permissions to reflect the current 8-entry locked set
with strikethrough on the original 5 + cross-ref to DEC-011 Amendment 1.
Plan-checker iteration 2 verdict: GREEN; Phase 2 plans cleared for
/gsd-execute-phase 2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The legacy chrome.tabs.query({ active: true, currentWindow: true }) +
"No active tab found" validation inside startVideoCapture were load-
bearing in the pre-D-01 chrome.tabCapture era but became functionally
dead after Plan 01-09's D-01 conversion to getDisplayMedia-in-offscreen.
The only post-D-01 consumer was a log line at index.ts:521.
The dead validation caused an activeTab-permission-scope asymmetry
between callers: chrome.action.onClicked grants activeTab on the click
gesture (so tab.url was readable → toolbar path worked silently) but
chrome.notifications.onClicked does NOT grant activeTab and the extension
has no `tabs` permission, so notifications.onClicked → startVideoCapture
threw "No active tab found" before reaching ensureOffscreen. Operator
2026-05-20 UAT against the new notifStartupCta CTA copy ("Mokosh ready.
Click to start a recording.", commit 4bba679) surfaced the silent
notification failure.
Surgical fix: remove the dead tab query + validation + tab-dependent log
(src/background/index.ts:514-521); replace with a tab-independent log
that documents WHY (cites D-01 + this debug session). captureScreenshot
+ saveArchive retain their genuine tab dependencies (tab.windowId for
chrome.tabs.captureVisibleTab; tab.id for content-script sendMessage).
Tests: tests/background/start-video-capture-no-tab.test.ts (NEW) pins
the contract with 3 cases (tabs.query → []; → [{id}] url-less; →
[{id,url,windowId}] regression guard for toolbar path).
Gates: vitest 153/153 GREEN (was 150/150 baseline; +3); test:uat 24/24
GREEN; tsc clean; build clean. Pre-checkpoint bundle gates per
feedback-pre-checkpoint-bundle-gates.md: SW chunk hook-string Tier-1
grep 0 matches; eval/Node-global/DOM-global matches unchanged from
baseline (all vendor-library feature-detect, guarded; no new imports).
Debug record: .planning/debug/resolved/01-09-notification-start-no-active-tab.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The build-completes Tier-1 gate at tests/background/no-test-hooks-in-prod-bundle.test.ts:247
was racing vitest's default 5000ms it() ceiling. Plan 01-10 closure shipped the welcome
page (commits d48a715 welcome mark + 49f087f welcome HTML/CSS/JS + 8 WOFF2 fonts) which
slowed standalone `npm run build` from ~2.88s to ~5.28s. The exec-level
BUILD_TIMEOUT_MS = 60_000 child-process bound was correctly declared at line 240, but
the surrounding it() block had no timeout option, so the 5s default fired first and the
60s exec bound was never reachable.
Surgical fix: add `, 30_000` 3rd arg to the it() call. 30s is ~6× the observed build
duration and well below the 60s exec ceiling, so both bounds remain meaningfully
active. SKIP_BUILD=1 env-var escape hatch untouched.
Acceptance gates:
- `npm test` (FULL, no SKIP_BUILD=1): 150/150 GREEN, exit 0
- `npx tsc --noEmit`: exit 0
- `npm run build`: exit 0
- Tier-1 grep gate: PASS (all 12 FORBIDDEN_HOOK_STRINGS asserted against dist/)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan 01-10 must_have #9 path-A swap-in (landed 2026-05-20 per debug
session 01-10-welcome-page-missing-mark). Closes the planning-coverage
gap where Plan 01-12 path-B (canonical tokens import) ran ahead of
01-10, leaving the welcome hero with a text placeholder 'Mokosh'
inside the rec-bg circle instead of the canonical 2×2 woven-square
mark from src/shared/brand/mokosh-mark.svg.
Why Option B (Vite ?url import) over manual WAR (A) or inline SVG (C):
- @crxjs/vite-plugin ^2.0.0-beta.25 auto-WARs transitively-reachable
resources from extension pages — no manifest.json edit needed.
- Vite default-inlines small SVGs (~600 bytes < 4096 byte default
assetsInlineLimit) as data:image/svg+xml URLs in the welcome chunk
— no extra HTTP request, no extra WAR entry.
- Hashed asset fallback works automatically if the SVG grows past
the inline limit in future revisions.
- Existing font-bundling precedent (dist/assets/Lora-*.woff2 +
IBMPlex*.woff2) proves the Vite + crxjs pipeline.
Files modified:
- src/welcome/welcome.ts — added markUrl import + populateMark() that
walks [data-mokosh-slot='mark'] and injects an <img>.
- src/welcome/welcome.html — added explanatory comment block; preserved
the data-mokosh-slot wrapper for forward-compat (the placeholder
span remains as the JS-fail-gracefully fallback).
- src/welcome/welcome.css — added .welcome-hero__mark-img rule
(60% sizing inside the existing styled circle wrapper).
- src/welcome/copy.ts — added 'welcome.hero.mark.alt' COPY key
(Russian per D-03 Sober voice).
- globals.d.ts — added *.svg?url ambient module declaration
(Vite recommended pattern; keeps tsconfig.json types: ['chrome']
clean by not requiring vite/client triple-slash directives).
- tests/uat/extension-page-harness.ts — extended A17 with A17.8
sub-check verifying the canonical mark SVG is bundled into the
welcome chunk (data URL OR file URL form) AND that the canonical
viewBox='0 0 32 32' is preserved through bundling.
Acceptance gates passed:
- npx tsc --noEmit exit 0
- npm run build exit 0
- SKIP_BUILD=1 npm test → 150/150 GREEN
- npm run test:uat → 24/24 GREEN including A17.8
- Tier-1 hook-string grep gate PASS (no FORBIDDEN_HOOK_STRINGS
in production bundle).
- Manifest valid JSON; web_accessible_resources auto-bundled.
- Pre-checkpoint bundle gates 1/2/3: vendor pre-existing hits
(JSZip + ts-ebml) confirmed identical pre-change via git stash
baseline; not caused by this fix.
Forward-looking deferred (out of scope):
- Issue 2 dark-surface contrast (e.g. chrome.notifications icon128
may need a light-stroke variant). The welcome hero's rec-orange
BG already provides high contrast with the dark ink stroke — this
is correct design. Per the orchestrator's explicit constraint,
light-variant mark for dark notification panels is deferred to
Phase 5.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>