Skip to content

Latest commit

 

History

History
873 lines (760 loc) · 57.2 KB

File metadata and controls

873 lines (760 loc) · 57.2 KB

Lesson Studio: Build Plan

Working plan file for the gstack review pipeline. Requirements and architecture are in HANDOVER.md (authoritative spec, revised 2026-07-12); this file turns that spec into an ordered build plan. Reviews append their reports below.

Goal

Ship a local-first Mac tool that turns slides + a script into a narrated lesson video: import a deck, edit slides/script, record voice against a teleprompter, produce a 1080p MP4 with SRT captions, and re-produce quickly after replacing any single scene. Quality bar: finished, tested, high quality, with a best-in-class intuitive UI.

Milestones

Status 2026-07-13 (overnight build): M0 through M6 implemented and tested (32 tests). Remaining open: NAS archive path (Dave decides; setting exists in the app), logo pick (three concepts in docs/brand/), restore-from-archive and conditioning profile UI (TODOS.md), and human verification of the teleprompter recording flow.

M0: Scaffold + dependency check

  • Node.js backend (Express or Fastify) + React frontend (Vite), single npm start style launch command that opens http://localhost:PORT.
  • Startup dependency check: ffmpeg, LibreOffice (soffice), pdftoppm present and on PATH; report anything missing with install hints instead of failing mid-pipeline.
  • Project data model on disk: ~/LessonStudio/projects/<project>/ holding project.json (scenes, script blocks, take metadata), slides/, audio/, output/.
  • project.json writes are serialised through a single in-process mutation queue; every write is temp file + fsync + rename with a rolling .bak of last-known-good. It is the sole source of truth with multiple async writers (editor, take-close, produce job); corruption here destroys the project.
  • Jobs are records (id, status, per-scene progress) with GET /jobs/:id for rehydration; SSE is only the push channel over that record. On server start, any running job is marked failed (interrupted). A tab reload during a produce must not blind the UI.
  • Instance lock at startup (pid check): a second launch, or a stale instance on the port, refuses with a clear message instead of opening a browser onto the wrong instance's state.
  • Encoder behind a config flag: h264_videotoolbox default, libx264 fallback (one line now; keeps tests runnable off-Mac later).

M1: End-to-end spike (one scene)

  • Upload one image + one script block, record narration in Chrome (MediaRecorder, WebM/Opus), mux with ffmpeg (h264_videotoolbox, AAC, 1920x1080 letterboxed), produce a one-scene MP4.
  • Convert one real DHE deck through LibreOffice headless and eyeball slide fidelity, before M0's dependency choices are treated as settled. Outcome (2026-07-13): PASS. DHE lessons turned out to be HTML slides, so the check ran on a real PowerPoint-authored business deck (True Managed ICT Review, a harder test). soffice converted the deck in ~6s (warm profile), pdftoppm ~1s at 150 dpi; layout, brand colours, tables, callouts and stat panels all faithful; fonts metric-substituted (Calibri to Carlito) without overflow; one minor artefact on an italic footnote. LibreOffice pipeline settled.
  • Proves the whole pipeline (secure origin, recording, transcode, mux) before any editing UI is built on top.

M2: Slide + script ingestion

  • PPTX upload: LibreOffice headless to PDF, pdftoppm to per-slide PNGs.
  • Speaker-notes import: offer to pre-fill script blocks from the deck's notes, parsed directly from the pptx XML (ppt/notesSlides/ inside the zip); no office suite involved in the notes path.
  • Direct image upload, normalised once at import (pre-scaled 1920x1080 cache); dimensions probed before normalisation and rejected above 10k x 10k (a small-on-disk pixel bomb must not OOM the normaliser).
  • Zip reading caps: per-entry decompressed-size limit and entry-count limit on the pptx (the 500 MB upload cap bounds the zip, not what it inflates to).
  • Notes extraction filters to the body placeholder only (slide-number and footer placeholders otherwise import "12" into every script block) and converts <a:br/> to line breaks.
  • Conversion timeout scales with deck size (60s + 3s/MB) and the soffice profile is warmed once during the M0 doctor check so first-conversion latency is paid at install time.
  • Script paste/upload, split into per-scene blocks at blank-line delimiters with a preview before commit; a block count mismatch against scenes is shown for review, never silently mapped.
  • Deck re-import in v1 is "replace slide images for unreordered projects" only; after reorders, new deck slides are presented as an unmatched pool for manual assignment (positional mapping over a reordered project is guaranteed wrong).

M2.5: Design direction (added by design review; before any editor UI is built)

  • Produce DESIGN.md: type scale (a real typeface, not a system stack), colour system as CSS variables (calm surfaces, one accent), spacing scale, App UI rules (dense but readable, minimal chrome, cards only where the card is the interaction).
  • Scene card anatomy (the editor's core unit) and the teleprompter screen design, previewed with the visualize tool for Dave's pick before implementation.
  • Logo/identity stays at M6; direction cannot wait until M6 (polish cannot rescue structure decided screen by screen during M3-M5).

M3: Pre-production editing

  • The editor's unit is the scene card: one card holds the slide thumbnail and its script block, in a single vertical list. Reordering moves both; there is no slides-vs-script sync problem because there is nothing to sync (this deletes the worst risk in the original plan).
  • Per-scene status chip on every card: no take / has take / stale / clean. The chip is the fast re-produce promise made visible.
  • Reorder, replace, delete, add scene cards; add/split/merge script blocks within the list.
  • Project browser one click away; launch resumes the last-open project directly (single user, one active project for weeks; no ceremony).
  • User-facing scene references always show slide thumbnail + current position + first words of the script block, never a bare ordinal (reorders make "Scene 7" a moving target).
  • Editor warns when a scene's script exceeds ~90 seconds at 145 wpm ("split this scene for caption accuracy"); scene granularity is the lever that keeps script-timed captions honest.
  • Tab lock is pid + timestamp with server-side takeover when the heartbeat goes stale; a second tab gets a "take over editing" button, never a dead-end read-only screen.

M4: Recording + teleprompter

  • Full-screen teleprompter, script-first: script occupies roughly 70 percent of the screen in large high-contrast type with a fixed focus band (the reading line stays at constant height; text moves through it); the current slide is a picture-in-picture confidence monitor (roughly 25 percent). Exact proportions tuned at M2.5.
  • Scroll is a reading aid only, decoupled from recording: recording runs from space-start to space-stop regardless of scroll state (pausing the scroll must never corrupt caption timing).
  • Auto-scroll at ~145 wpm default, adjustable, pause/resume, speed nudge mid-take.
  • Teleprompter state map: armed (mic ready) > countdown (3-2-1 on space) > recording > rest (take saved: duration + next slide preview) > next scene on explicit arrow/space. Never auto-advance; the reader needs a breath.
  • Retake key (R): closes the current take (kept, per take policy), rewinds the scroll to the top of the scene, re-arms. The mid-scene flub is the most common event in any session and must not need the mouse.
  • ESC while recording: stop and keep (never discard), exit to the editor scrolled to the scene just recorded.
  • Keyboard-driven flow: space start/stop, arrows between scenes, R retake.
  • Crash-safe chunked audio upload, specified as a protocol (eng review): the client sends chunks strictly sequentially (await + bounded retry, idempotent by takeId and sequence number); the server runs a take lifecycle (open > appending > closed), acks the highest contiguous sequence, and rejects chunks for closed takes loudly; the UI surfaces "recording upload stalled" within ~5 seconds of a stuck chunk; the client keeps the full local blob and offers it as a download on any terminal upload failure. This is the hardest code in the app; the M1 spike uses the real protocol, not a naive blob upload.
  • Every take is remuxed at close (ffmpeg -c copy), not just crash-recovered ones: streamed WebM has no duration header, and take duration is load-bearing for the takes list, rest state and caption math. Duration comes from ffprobe of the remuxed file.
  • Take management: prior takes kept per scene, active-take selector, new take never destroys the old one before it is safely written.
  • Take audition (moved into scope by design review): every take is playable in the takes list with a native audio player; a take selector over takes you cannot hear is not a selector. Recovered partial takes appear marked "recovered (partial)", playable, never auto-active. (The waveform renderer stays out; audition and waveform were wrongly bundled as one deferral.)

M5: Production + captions

  • Per-scene audio conditioning, in this filter order: auto-trim leading/trailing silence (threshold tuned once to Dave's mic/room), then loudness normalisation (ffmpeg loudnorm, two-pass: analyse then apply), then configurable in/out silence padding (default ~0.3s). Order matters: trimming after padding would remove the padding, and normalising before trimming would measure the silence.
  • Per-scene mux, concat to final MP4, fast re-production after single-scene changes (only re-mux changed scenes; concat is cheap).
  • Script-timed SRT captions, chunked at sentence/clause boundaries (~2 lines of ~42 chars per cue, grapheme-aware length counting). The SRT is always regenerated whole: cue timing derives from ffprobe of the conditioned audio (never the raw take), and cumulative offsets from actual segment durations, because any scene duration change shifts every subsequent cue.
  • Dirty tracking is split: videoDirty (slide/take/conditioning change, re-mux the scene) vs captionsDirty (script edit, regenerate SRT only); cache keys are hash(take bytes + conditioning params) and hash(slide bytes + encode params) so a global settings change correctly invalidates everything.
  • Loudnorm handling for short takes: below ~3 seconds, skip two-pass loudnorm and apply the last-measured gain (integrated loudness measurement is unreliable on very short audio); at least one test fixture is 5 seconds or longer.
  • Pre-produce disk check: estimate (sum of take durations x bitrate x 2.5) against free space before spawning anything.

M6: Archive + polish

  • "Archive to NAS" per project + not-yet-archived indicator; restore from archive. (NAS share/path decision needed from Dave before this milestone.)
  • Logo/visual identity (preview concepts with the visualize tool first), UI polish pass to the best-in-class bar.

Architecture (settled, see HANDOVER.md)

Local-first on the Mac. Node.js + React. Chrome target. LibreOffice + pdftoppm for PPTX. ffmpeg with h264_videotoolbox for production. Local working directory for all recording/editing/production; NAS is an explicit archive target. Gitea repo at 192.168.50.200:3000.

Risks

  • MediaRecorder chunk handling: audio must be durable to local disk during long recording sessions (flush chunks server-side as they arrive, not one blob at the end).
  • LibreOffice conversion fidelity: some PPTX features render differently than PowerPoint; acceptable for lesson slides, verify early with a real DHE deck.
  • Scene/script sync integrity through reorder/delete operations (M3) is the most bug-prone area; needs property-style tests around the data model.

Open decisions for Dave

  • NAS archive share/path (blocks M6 only).
  • Logo direction (blocks nothing; M6).

Pre-build evidence tasks (final gate, approved 2026-07-13)

  • Before M0: ~30 minutes of market verification: try PowerPoint Recording Studio's teleprompter view and a Narakeet trial against a real DHE lesson; the build must beat the best real alternative. Record the outcome here. Outcome (researched 2026-07-13): PowerPoint for Mac's Recording Studio does have a teleprompter view, but the Mac version lacks auto-scroll and scroll speed controls (Windows-only features), which are the core of the Lesson Studio teleprompter; it also has no take management and no script-timed SRT. Narakeet converts PPTX (with speaker notes) to narrated MP4 with auto captions, 900 TTS voices or uploaded pre-recorded audio, at roughly $0.05-0.20 per output minute, cloud-only; it has no live teleprompter recording workflow. Verdict: the build premise survives for the record-own-voice teleprompter workflow. Narakeet is the buy-side benchmark for the TTS path specifically, so the pre-M4 TTS spike doubles as the Narakeet comparison: if TTS wins the A/B, evaluate Narakeet before building M4 at all.
  • Before M0: demand numbers (Dave, 2026-07-13): 50-150 videos remaining across DHE and planned courses; 5-15 videos/month expected over the next 12 months; 40-90 minutes lost per video in the current Descript workflow beyond narration itself (setup, assembly, captions, re-records, export, upload). Worst case that is 33 hours of overhead on the remaining pipeline, best case 225 hours; the build clears its cost by a wide margin.
  • Before M4: one-hour TTS voice-clone spike: clone the voice, render one real DHE script block, A/B against a recorded take. If it wins, M4's recording apparatus shrinks dramatically; if it loses, the recording path has documented evidence.
  • Soft time-box: if M0-M5 overruns roughly 3 working sessions, pause and reassess the buy side (Descript/Narakeet trajectories) before continuing.
  • Gate outcomes: script-timed captions kept for v1 (revisit after the first real lesson); M6 logo/polish kept per Dave; browser capture kept (hardened protocol); waveform UI deferred to TODOS.

CEO Review (Phase 1, /autoplan, 2026-07-12)

Mode: SELECTIVE EXPANSION (autoplan override). Voices: Claude subagent only, Codex CLI absent, tagged [subagent-only]. Landscape check: WebSearch skipped, in-distribution knowledge used (Descript, PowerPoint Record Slide Show, Keynote, Camtasia, Loom, OBS).

0A. Premise Challenge

Premises examined: (1) recurring real pain from ongoing course production, (2) existing tools miss the teleprompter/takes/SRT/re-produce combination, (3) script-first beats ad-lib for lesson quality, (4) build beats buy at Claude Code build costs. Strongest counter: Dave already has a Descript workflow, and PowerPoint Record Slide Show covers roughly 70 percent free. The differentiators (teleprompter, take management, script-timed SRT, one-click re-produce, no subscription) are the actual workflow bottlenecks. GATE PASSED: Dave confirmed all four premises.

0B. Existing Code Leverage

Sub-problem Existing leverage
Node backend + React SPA conventions Compass platform/api + platform/spa (patterns, not literal reuse)
File upload handling Compass api upload endpoints
PPTX to slide images DHE build pipeline + docx/pptx skill approach (soffice headless, pdftoppm)
Project-on-disk data model New, but shaped like Compass project handling
ffmpeg mux/concat New to this repo; well-trodden pattern

Nothing in the plan rebuilds something that already exists in Dave's estate; the video pipeline is genuinely new capability.

0C. Dream State

CURRENT STATE                      THIS PLAN                        12-MONTH IDEAL
Descript + manual assembly   -->   Script-first local studio:  -->  Whole-course production line:
per lesson; no teleprompter;       teleprompter, takes,             batch projects per course,
inconsistent output                scene re-record, 1080p           templates, publish hook to
                                   MP4 + SRT in one click           Moodle/NAS, course library

The plan moves directly toward the ideal; the scene data model and production pipeline are the platform pieces the 12-month version builds on.

0C-bis. Implementation Alternatives (auto-decided, P1 highest completeness)

APPROACH A: As-planned (local Node server + browser UI)
  Summary: Express/Fastify + React (Vite), filesystem project store, ffmpeg shell-outs.
  Effort:  M   Risk: Low
  Pros:    matches settled architecture; secure origin free via localhost; Compass patterns
  Cons:    browser-tab UX rather than native app feel
  Reuses:  Compass api/spa conventions, DHE slide pipeline

APPROACH B: Electron-packaged app
  Summary: Same internals wrapped in Electron for a native app feel.
  Effort:  L   Risk: Med
  Pros:    dock icon, native menus, no port management
  Cons:    packaging/signing overhead; contradicts settled local-server decision; zero
           functional gain for a single-user tool
  Reuses:  same internals

APPROACH C: CLI pipeline only + PowerPoint recording
  Summary: Skip the UI; record in PowerPoint, CLI does mux/captions.
  Effort:  S   Risk: Low
  Pros:    smallest build
  Cons:    no teleprompter or take management, which are the confirmed differentiators
  Reuses:  ffmpeg pipeline only

DECISION (auto, P1+P5): Approach A. Highest completeness against the confirmed premises without Electron overhead; C fails the premise, B adds effort for no capability. Not close enough to mark as a taste decision: B contradicts a settled decision in HANDOVER.md, C removes confirmed differentiators.

0D. Selective Expansion Analysis

Complexity check: greenfield, three moving parts (React UI, Node API, ffmpeg/LibreOffice pipeline). Inherent to the goal; no fewer-moving-parts shape exists that keeps the teleprompter + production requirements. Minimum set that ships value: M0 to M5; M6 archive is deferrable without blocking core recording.

Expansion scan decisions (auto-decided per principles; T = surfaced at final gate):

# Proposal Effort Decision Reasoning
1 Loudness normalisation (ffmpeg loudnorm) per scene S ACCEPTED (P2) In pipeline blast radius, under 1 day, fixes cross-session volume drift between takes recorded on different days
2 Auto-trim leading/trailing silence per take S ACCEPTED (P2) Same blast radius; removes click-to-speech gap on every scene
3 Waveform preview + take audition in UI M TASTE DECISION (T, pending final gate) Genuinely useful for picking the active take, but adds a waveform renderer; reasonable to defer to v1.1
4 Keyboard-driven recording flow (space start/stop, arrows between scenes) S ACCEPTED (P2) Core teleprompter usability; hands stay off the mouse mid-session
5 Configurable in/out silence padding per scene (default ~0.3s) S ACCEPTED (P2) One ffmpeg flag; softens abrupt scene cuts
6 Publish hook (copy MP4+SRT to Moodle-upload folder) M DEFERRED to TODOS (P3) Outside core blast radius; integration work after the tool proves itself
7 TTS preview voice for scripts L DEFERRED to TODOS (P3) New infrastructure (TTS engine); not needed to record real lessons
8 Project templates (intro/outro slides, defaults) M DEFERRED to TODOS (P3) Valuable at course scale, not for first lessons
9 Crash-safe session recovery (chunked audio flushed to disk during recording) S FOLDED INTO M4 (P1) Not an expansion; core durability requirement

Platform potential: the scene data model + production pipeline is the foundation for batch whole-course production (12-month ideal). No extra work now; noted so M0 schema decisions do not preclude it (scenes keyed by UUID, project.json versioned).

0E. Temporal Interrogation (resolved now, not during implementation)

  • HOUR 1: port fixed (default 5173 dev / 3555 app; final call at M0), project dir layout and project.json schema written before any code; scenes and takes keyed by UUID, never by array index.
  • HOUR 2-3: MediaRecorder audio is uploaded in chunks and appended to disk server-side as it arrives (crash-safe), not sent as one blob at stop. Reorders only ever permute a UUID list.
  • HOUR 4-5: LibreOffice conversions run with a temp -env:UserInstallation profile so a user-open LibreOffice never locks the conversion; pdftoppm rasterises at -r 150 (about 2000px wide) then normalises to 1920x1080; pdftoppm zero-pads page numbers to the final page's digit count, so slide ordering is parsed numerically, never by glob sort (pinned by a >10-page fixture).
  • HOUR 6+: concat uses re-encoded uniform segments (same codec params, timebase) so the concat demuxer never sees mixed streams; SRT cue timing derives from ffprobe of the conditioned segments (see M5), never the raw take; teleprompter scroll speed is a starting pace, with manual nudge as the primary control during recording.

Human-team scale: roughly 2-3 weeks. CC+gstack scale: a few working sessions.

0F. Mode Selection

SELECTIVE EXPANSION (fixed by /autoplan). Approach A confirmed under this mode.

Step 0.5: Dual Voices [subagent-only]

CODEX SAYS (CEO, strategy challenge): N/A, Codex CLI not installed.

CLAUDE SUBAGENT (CEO, strategic independence): 10 findings, summarised with dispositions:

# Finding Severity Disposition
F1 TTS voice-clone as the production voice would delete M4 (teleprompter, takes, crash-safe recording); never analysed as more than a preview toy Critical GATE (challenges Dave's settled audio-recording decision; single-voice, so not auto-decided)
F2 Demand unquantified: videos remaining, cadence, minutes lost per video in Descript Critical GATE (only Dave has the numbers)
F3 Premise 2 checked from memory: PowerPoint Recording Studio now has a teleprompter view; Narakeet sells this exact spec High GATE (cheap pre-build verification, Dave decides)
F4 Descript deficiencies never itemised; milestones should trace to concrete frictions High GATE note (folds into F2)
F5 Server-side native capture (ffmpeg avfoundation) would delete the crash-safety apparatus, Opus transcode and Chrome pinning High TASTE DECISION, re-examined in eng phase
F6 Script-timed SRT drifts within scenes; whisper.cpp is local, fast, word-timestamped Medium GATE (challenges settled v1 caption decision)
F7 Cut M6 logo/best-in-class polish for a single-user tool; estimate vs quality bar contradiction High GATE (challenges Dave's explicit requirement)
F8 Speaker notes: parse pptx XML directly (LibreOffice PDF path never carried notes); verify LibreOffice slide fidelity with a real DHE deck before M0 locks dependencies Medium ADOPTED (mechanical, P4/P5): notes via XML parse; fidelity spike moved into M1
F9 Alternatives compared packaging, not workflows High Partially remediated by F1/F5/F8 dispositions; noted
F10 Time-box the build; the make-vs-buy line moves monthly Medium GATE note

CEO DUAL VOICES, CONSENSUS TABLE:

  Dimension                              Claude       Codex  Consensus
  1. Premises valid?                     CHALLENGED   N/A    flagged (F2, F3, F4)
  2. Right problem to solve?             CHALLENGED   N/A    flagged (F1)
  3. Scope calibration correct?          CHALLENGED   N/A    flagged (F7)
  4. Alternatives sufficiently explored? INSUFFICIENT N/A    flagged (F9, part-fixed)
  5. Competitive/market risks covered?   NOTED        N/A    flagged (F10)
  6. 6-month trajectory sound?           AT RISK      N/A    flagged (F7)

Single-voice mode: critical findings flagged regardless of consensus rules.

Auto-adopted into the plan (mechanical):

  • M2 speaker-notes import now specifies direct pptx XML parsing (the file is a zip; notes live in ppt/notesSlides/), no office suite needed for notes.
  • M1 spike now includes converting one real DHE deck through LibreOffice to check slide fidelity before M0's dependency check treats LibreOffice as settled.

11-Section Deep Review

Section 1: Architecture

  SYSTEM ARCHITECTURE
  +---------------------------+        +----------------------------------+
  |  Chrome (localhost only)  |  HTTP  |  Node API (127.0.0.1:PORT)       |
  |  React SPA (Vite)         |<------>|  REST + SSE progress events      |
  |  - project browser        |        |  - zod-validated endpoints       |
  |  - editor (scenes/script) |        |  - job queue (in-process)        |
  |  - teleprompter/recorder  |        +---------+------------------------+
  |    (MediaRecorder)        |                  | execFile (never shell)
  +---------------------------+                  v
                                  +------------------------------------+
                                  |  Pipeline workers                  |
                                  |  soffice --headless (temp profile) |
                                  |  pdftoppm | ffmpeg (videotoolbox)  |
                                  +---------+--------------------------+
                                            v
                        ~/LessonStudio/projects/<uuid>/
                        project.json (versioned) | slides/ | audio/<scene>/<take>.webm
                        conditioned/ (cached trim+loudnorm) | output/ | logs/
                                            |  explicit archive step
                                            v
                                    NAS SMB share (path TBD)

Findings, all auto-adopted (mechanical, P5 explicit over clever):

  1. Bind 127.0.0.1 explicitly, never 0.0.0.0; the server must not be reachable from the LAN.
  2. project.json carries a schemaVersion field from day one with a migration function per bump; the on-disk format is the closest thing this tool has to a one-way door.
  3. Production runs as a job with SSE progress events, not a blocking HTTP request; a 34-scene produce takes tens of seconds and the UI must show per-scene progress.
  4. All child processes via execFile with argument arrays (no shell string interpolation), which also closes the command-injection vector in Section 3. State machine (per scene):
  NO_TAKE --record--> RECORDING --stop/flush--> HAS_TAKE(active)
     ^                   | crash                    | re-record -> new take, old kept
     |                   v                          v
     +---delete take--- PARTIAL(recoverable) --remux--> HAS_TAKE
  Produce marks scene CLEAN; editing slide/script/take marks scene DIRTY (re-mux list)

Invalid transitions prevented by: single-writer tab lock (Section 4), takes are append-only until explicit delete. Scaling: single user; worst realistic load is a 100-slide deck. LibreOffice conversion is the slow path (async job). Concat of 100 segments is trivial. SPOF: the two external binaries, guarded by the M0 dependency check.

Section 2: Error & Rescue Map (registry below in Required Outputs)

Every pipeline call gets a named failure, a rescue, and a user-visible message; the full registry is in the Error & Rescue Registry output section. Notable rules adopted: soffice/pdftoppm/ffmpeg failures always surface the command and the stderr tail in the UI error panel and are written to logs/; chunk-append failures stop the recording UI immediately (never silently drop audio); disk-full is checked before recording starts (refuse to record with under 1 GB free) rather than discovered mid-take.

Section 3: Security & Threat Model

Threat Likelihood Impact Mitigation
DNS rebinding / drive-by POST from a malicious web page to localhost Med High Validate Host and Origin headers on every request; reject non-localhost origins; no CORS headers
Path traversal via project/file names Med High Server-generated UUID directory names only; user titles live inside project.json, never in paths
Command injection via crafted filenames into ffmpeg args Low High execFile arg arrays (Section 1 finding 4); filenames are server-generated
Malformed/hostile PPTX (zip bomb, XML entity expansion) Low Med Size cap on upload (default 500 MB), entity-expansion-safe XML parser, conversion in temp dir with timeout
LAN exposure of recordings Low Med 127.0.0.1 binding (Section 1 finding 1)

No auth for a single-user localhost tool is acceptable once origin validation and loopback binding are in place. No PII beyond Dave's own voice recordings. No secrets.

Section 4: Data Flow & Interaction Edge Cases

Recording upload flow with shadow paths:

  MediaRecorder chunk --> POST /scenes/:id/takes/:take/chunks --> append to .webm.part
      |                        |                                      |
   [mic denied?]          [project deleted mid-take?]            [disk full?]
   error state, help      410 Gone, UI stops recording           refuse pre-take (<1GB)
   text for macOS mic     and preserves local blob               abort + keep .part
   permissions            [chunks out of order?]                 [crash mid-take?]
                          sequence numbers, buffer + reorder      .part remuxed to
                          [empty chunk?] ignore, log              recoverable take on restart

Interaction edge cases adopted into scope: double-press of record guarded by state machine; navigate-away during recording triggers a beforeunload warning; a second tab on the same project gets read-only mode (single-writer lock file with heartbeat); deleting a scene with takes asks once, then moves audio to project trash (emptied on archive) rather than unlinking; re-importing a deck over an edited project maps slides by position with an explicit review step, never a silent overwrite; teleprompter with an empty script block shows "no script for this scene" rather than an empty scroll; production requested while a recording is live is queued until the take closes.

Section 5: Code Quality

Greenfield conventions locked now (all mechanical, P5): TypeScript on both ends; shared types/ package holding the project.json schema and API types; zod validation at every API boundary; API and pipeline as separate modules so the pipeline is unit-testable without HTTP; no ORM or database, the filesystem plus project.json is the store; keep pipeline functions pure (paths in, paths out) with all process spawning in one exec.ts wrapper.

Section 6: Test Review

  NEW UX FLOWS: create project, import deck/images, import/edit script, reorder scenes,
    record (sequence + single), pick take, produce, re-produce, archive
  NEW DATA FLOWS: pptx->pdf->png, notes XML->script blocks, chunked webm->take,
    take->conditioned audio->scene mp4->final mp4 + srt
  NEW CODEPATHS: schema migrations, reorder/delete sync, SRT cue chunking, dirty-scene
    tracking, crash recovery remux
  NEW ASYNC WORK: conversion job, production job (SSE progress)
  NEW INTEGRATIONS: soffice, pdftoppm, ffmpeg, SMB copy
  NEW ERROR PATHS: per the Error & Rescue Registry

Coverage plan: unit tests for SRT chunking (sentence boundaries, 42-char lines, cue timing math), scene/script sync through reorder/insert/delete (property-style: any operation sequence keeps blocks and slides paired), schema migration, dirty-scene calculation. Integration tests with a small fixture deck and 2-second fixture audio: pptx conversion, notes extraction, full produce, single-scene re-produce reuses cached segments (assert mtimes), crash-recovery remux of a truncated .part file. E2E: one Playwright happy path using system Chrome (channel: 'chrome'), explicitly NOT the bundled Chromium download, which reproducibly hangs on this machine (prior learning); fake media stream flags for mic. 2am Friday test: 3-scene project produced end to end, output probed with ffprobe (duration, streams, resolution). Chaos: kill ffmpeg mid-produce and assert the job fails loudly with stderr surfaced and no corrupt final MP4 (write to temp, atomic rename on success).

Section 7: Performance

Slow paths, worst realistic case (100-slide deck): LibreOffice conversion, tens of seconds, async job with progress (already adopted); loudnorm two-pass per take, seconds, mitigated by caching conditioned audio per take content-hash (re-produce never re-conditions unchanged takes); image normalisation done once at import (pre-scaled 1920x1080 PNGs cached), not at every produce. Re-produce cost is then proportional to changed scenes only, which is the tool's headline promise. No database, no N+1, no connection pools. Memory: never hold slide images or audio in process memory; stream everything through files.

Section 8: Observability & Debuggability

Adopted: structured log lines (pino or similar) at job start/end/failure with project and scene ids; every external command logged with full argv and duration, stderr tail captured to logs/<job-id>.log inside the project dir so a failed produce three weeks later is reconstructable from the project folder alone; per-project "last production report" (which scenes re-encoded, which reused, total time) shown in the UI after every produce; the M0 dependency check doubles as the runbook (doctor output names the missing binary and the brew command to fix it). Metric that says it works: produce job success and duration, visible in the production report. No dashboards or alerting; a single-user tool's alert channel is the UI error panel.

Section 9: Deployment & Rollout

No deployment; it runs from a git checkout on the Mac. The analogue risks are covered: schema migrations versioned and forward-only with a backup copy of project.json written before migrating; rollback is git checkout of a prior tag plus the project.json backup; smoke test is npm run doctor plus a fixture produce (npm run smoke); the risk window equivalent (data format change while projects exist) is handled by schemaVersion. Feature flags not needed.

Section 10: Long-Term Trajectory

Debt introduced: none structural; the deliberate cut is script-timed captions (upgrade path to whisper.cpp noted at the gate). Reversibility 4/5: all artefacts are plain files (PNG, WebM, MP4, JSON, SRT); worst one-way door is project.json schema, mitigated by versioning. Knowledge concentration: README quickstart plus HANDOVER.md plus this plan is sufficient for a cold restart. Phase 2 trajectory (batch course production, publish hook) sits cleanly on the scene data model; nothing in this plan blocks it. Cherry-pick retrospective: accepted items are all inside the recording/production blast radius; no rejected expansion is load-bearing for an accepted one.

Section 11: Design & UX

  USER FLOW
  [Project browser] --create/open--> [Editor: scene grid + script panel]
        |                                |            |
        | first-run: welcome +           | per-scene  | Record all -->
        |  "create your first lesson"    v            v
        |                          [Scene detail] [Teleprompter (full-screen, dark)]
        |                          takes list      slide + auto-scroll script
        |                                          space=start/stop, arrows=scene
        v                                                 |
  [Produce panel] <--- progress (SSE), per-scene ---------+
        | success: player + SRT download + production report
        v
  [Archive to NAS] (M6)

Interaction state coverage map (all five states specified per feature):

Feature Loading Empty Error Success Partial
Project browser skeleton first-run welcome disk unreadable panel grid n/a
Deck import job progress zero slides warning stderr panel scene grid filled some slides failed: list + retry
Recording mic-arming state no script notice mic denied help take saved toast + level meter partial take recovered banner
Produce per-scene progress nothing to produce (all clean) failed scene named + log link player + report some scenes stale warning
Archive copy progress nothing new to archive NAS unreachable archived badge partial copy resumable

Design intentionality: the teleprompter screen is the product's soul; it gets a dedicated dark, chrome-free design (large high-contrast type, subtle scroll, live level meter) rather than a generic component-library page. Slop risk is highest in the editor screens; mitigate at Phase 2 (design review) with a real design direction rather than default shadcn-style panels. Desktop-only is intentional (Mac tool). Accessibility: keyboard-first recording is already scope; focus states and contrast targets to be set by the design phase. Emotional arc: the anxious moment is "did it record?"; the take-saved confirmation plus takes list with durations answers it within a second of stopping.

Required Outputs (CEO phase)

NOT in scope

  • Publish hook to Moodle/NAS course folders (TODOS; after the tool proves itself).
  • TTS voices, preview or production (TODOS; also a final-gate challenge item, F1).
  • Project templates (TODOS; course-scale feature).
  • Waveform preview + take audition UI (pending final gate; defaults to deferred).
  • Webcam/PIP recording (settled decision: audio only).
  • Speech-accurate captions via whisper.cpp (settled v1 decision; final-gate item F6).
  • Multi-user, LAN access, HTTPS (single-user localhost tool by design).

What already exists

  • Compass platform/api and platform/spa: Node/React conventions, upload handling.
  • DHE build pipeline: LibreOffice slide rasterisation precedent.
  • Descript workflow: the current production path; stays usable throughout the build.

Dream state delta

This plan ships the load-bearing half of the 12-month ideal (scene model, pipeline, take management). Remaining gap after M6: batch orchestration across a course, publish hook, templates. Nothing in the plan works against the ideal.

Error & Rescue Registry

  CODEPATH                    | WHAT CAN GO WRONG              | EXCEPTION/SIGNAL      | RESCUED? | ACTION                          | USER SEES
  ----------------------------|--------------------------------|-----------------------|----------|---------------------------------|--------------------------
  POST /projects/:id/deck     | soffice missing                | DoctorCheckFailed     | Y        | refuse, name brew fix           | "LibreOffice not found" + fix
                              | soffice exits non-zero         | ConversionFailed      | Y        | stderr tail to panel + logs/    | "Deck conversion failed" + log link
                              | soffice hangs                  | ConversionTimeout     | Y        | kill at 120s, cleanup temp      | timeout message + retry
                              | PDF has zero pages             | EmptyDeckError        | Y        | reject upload                   | "No slides found in deck"
                              | pdftoppm fails                 | RasteriseFailed       | Y        | stderr tail + logs/             | error panel + retry
  notes XML parse             | no notesSlides/                | (not an error)        | Y        | skip offer                      | "No speaker notes in deck"
                              | malformed XML                  | NotesParseError       | Y        | import slides without notes     | warning toast
                              | notes/slide count mismatch     | (not an error)        | Y        | map by slide index, gaps empty  | review step shows gaps
  POST chunk (recording)      | disk under 1 GB free           | LowDiskError          | Y        | refuse to start take            | "Free up disk space" pre-take
                              | append fails mid-take          | ChunkWriteError       | Y        | stop recording, keep .part      | recording stopped + recover banner
                              | project deleted mid-take       | GoneError (410)       | Y        | stop, preserve local blob       | "Project was deleted" + download blob
                              | out-of-order chunk             | (buffered)            | Y        | sequence numbers, reorder       | nothing
  crash recovery              | truncated .part                | RemuxNeeded           | Y        | ffmpeg remux on next open       | "Recovered take" banner
  produce job                 | take audio missing             | MissingTakeError      | Y        | job fails naming the scene      | "Scene 7 has no take" + jump link
                              | ffmpeg exits non-zero          | EncodeFailed          | Y        | fail job, stderr tail, temp file kept | failed scene named + log link
                              | disk full mid-produce          | ENOSPC                | Y        | fail loudly, clean temp         | disk-full message
                              | loudnorm analysis fails        | ConditionFailed       | Y        | fall back to unconditioned audio, warn | warning in production report
  concat/final                | mixed stream params            | (prevented)           | Y        | uniform re-encode per scene     | n/a
                              | interrupted final write        | (prevented)           | Y        | write temp, atomic rename       | n/a
  archive (M6)                | NAS unmounted                  | ArchiveUnreachable    | Y        | fail with mount hint            | "NAS not mounted" + hint
                              | partial copy                   | ArchiveIncomplete     | Y        | per-file checksums, resumable   | "Archive incomplete, resume"

No CRITICAL GAPS: every failure row has a rescue and a user-visible outcome.

Failure Modes Registry

  CODEPATH        | FAILURE MODE            | RESCUED? | TEST?            | USER SEES?          | LOGGED?
  ----------------|-------------------------|----------|------------------|---------------------|--------
  deck import     | conversion fail/timeout | Y        | integration      | error panel         | Y
  notes import    | parse fail/mismatch     | Y        | unit             | warning/review step | Y
  recording       | chunk write fail        | Y        | integration      | stop + banner       | Y
  recording       | browser crash mid-take  | Y        | integration(remux)| recover banner     | Y
  produce         | encode fail             | Y        | integration+chaos| failed scene named  | Y
  produce         | partial final file      | Y (atomic)| chaos           | n/a (prevented)     | Y
  archive         | unreachable/partial     | Y        | integration      | actionable message  | Y

No rows with RESCUED=N or silent user outcomes.

Scope Expansion Decisions

Recorded in the CEO plan (~/.gstack/projects/dkempson-lesson-studio/ceo-plans/2026-07-12-lesson-studio.md). Accepted: loudnorm, silence auto-trim, keyboard flow, scene padding (+ crash-safe chunked upload folded into core). Deferred: publish hook, TTS, templates. Pending gate: waveform/take audition.

Implementation Tasks (CEO phase)

Synthesised from findings above; each traces to a section.

  • T1 (P1, human: ~2h / CC: ~15min) server: Bind 127.0.0.1 and validate Host/Origin on every request (Sections 1, 3)
  • T2 (P1, human: ~4h / CC: ~30min) data model: project.json with schemaVersion, UUID keys, migration scaffold + backup-before-migrate (Sections 1, 9)
  • T3 (P1, human: ~1d / CC: ~1h) pipeline: execFile-only process wrapper with argv logging, stderr capture to logs/, timeouts (Sections 1, 2, 8)
  • T4 (P1, human: ~1d / CC: ~1h) recording: chunked upload with sequence numbers, pre-take disk check, .part recovery remux (Sections 2, 4)
  • T5 (P2, human: ~4h / CC: ~30min) produce: SSE job progress + production report + atomic final write (Sections 1, 7, 8)
  • T6 (P2, human: ~4h / CC: ~30min) produce: conditioned-audio and pre-scaled-image caches keyed by content hash (Section 7)
  • T7 (P2, human: ~2h / CC: ~15min) editor: single-writer tab lock, beforeunload guard, scene-delete-to-trash (Section 4)
  • T8 (P2, human: ~1d / CC: ~1h) tests: fixture deck + fixture audio integration suite, property-style reorder sync tests, chaos kill-ffmpeg test, Playwright with system Chrome channel (Section 6)
  • T9 (P3, human: ~2h / CC: ~15min) upload: size caps + entity-safe XML parsing (Section 3)

Design Review (Phase 2, /autoplan, 2026-07-12)

Voices: Claude subagent only [subagent-only]; Codex absent. Mockups skipped: design binary present but no OPENAI_API_KEY; text/wireframe fallback used. UI classifier: APP UI (task-focused workspace), App UI rules applied.

CODEX SAYS (design, UX challenge): N/A.

CLAUDE SUBAGENT (design, independent review): 14 findings (3 critical, 5 high, 6 medium). All structural findings auto-adopted (P5) as plan edits: unified scene cards (1.1), teleprompter layout + focus band + countdown + state map (1.2, 2.3), resume-last-project launch (1.3), editor state row + status chips (2.1), recovered take resolution path (2.2), take audition split from waveform and moved into M4 (3.1), retake key (3.2), rest state / never auto-advance (3.3), design direction milestone M2.5 inserted before editor build (5.1), scroll/record decoupling (5.2), non-ordinal scene references (5.3), ESC stop-and-keep semantics (5.4). Finding 4.1 (generic vocabulary) is resolved by M2.5 producing DESIGN.md.

Design litmus scorecard (App UI):

  Check                                        Claude  Codex  Consensus
  Calm surface hierarchy, few colours?         PLANNED  N/A   M2.5 output
  Strong typography (no system stack)?         PLANNED  N/A   M2.5 output
  Cards only where card = interaction?         YES      N/A   scene card IS the interaction
  Dense but readable, minimal chrome?          YES      N/A   teleprompter chrome-free
  Copy = orientation/status/action?            YES      N/A   state table language
  Premium without decorative shadows?          PLANNED  N/A   M2.5 gate

7 Passes (rating before > after adopted fixes)

  1. Information Architecture 5 > 9. Was: two parallel lists and an unspecified teleprompter. Now: scene-card list, script-first teleprompter with focus band, resume-last-project. Remaining point: M2.5 must fix real proportions.
  2. Interaction State Coverage 7 > 9. Editor row added, teleprompter has its own state map, recovered takes have a resolution path.
  3. User Journey 4 > 9. The peak-anxiety moment now has audition, retake key, and a breathing rhythm; the arc no longer breaks at the exact moment the product exists to serve.
  4. AI Slop Risk 4 > 7. App UI rules named, slop blacklist noted; risk retires fully only when M2.5 lands DESIGN.md (no purple gradients, no icon-circle grids, no system-ui, no placeholder-as-label; body text 16px+ at 4.5:1+).
  5. Design System Alignment 2 > 5. No DESIGN.md exists; M2.5 creates it. Recommendation stands: run /design-consultation as part of M2.5.
  6. Responsive & Accessibility 6 > 8. Desktop-only is intentional and stated; keyboard-first flow is scope; contrast and focus-band requirements set; touch targets n/a. Screen-reader depth deliberately minimal for a single-user tool, ARIA landmarks are cheap and included.
  7. Unresolved Design Decisions: typeface, accent colour, level-meter design, dark vs light editor (teleprompter is dark regardless). All deferred to M2.5 where Dave picks from visualize previews; none block M0-M2.

Implementation Tasks (design phase)

  • D1 (P1, human: ~1d / CC: ~1h): M2.5: DESIGN.md via /design-consultation, scene card + teleprompter designs previewed with visualize (Pass 5, finding 5.1)
  • D2 (P1, human: ~4h / CC: ~30min): editor: scene-card component with status chip (findings 1.1, 2.1)
  • D3 (P1, human: ~1d / CC: ~1h): teleprompter: focus-band scroll, countdown, state machine, retake key, ESC semantics, decoupled scroll (findings 1.2, 2.3, 3.2, 5.2, 5.4)
  • D4 (P2, human: ~2h / CC: ~15min): takes list: native audio audition + recovered-take marking (findings 3.1, 2.2)
  • D5 (P3, human: ~2h / CC: ~15min): non-ordinal scene references in all messages (finding 5.3)

Eng Review (Phase 3, /autoplan, 2026-07-12)

Voices: Claude subagent only [subagent-only]; Codex absent. Search check: WebSearch skipped, in-distribution. Step 0 scope challenge: complexity inherent (held, P2); distribution not needed (git checkout on one Mac, flagged in NOT in scope); built-ins preferred (SSE over WebSocket for one-way progress [Layer 1], in-process job records over queue infrastructure for a single user [Layer 3], crypto.randomUUID, jszip + fast-xml-parser for notes).

CODEX SAYS (eng, architecture challenge): N/A.

CLAUDE SUBAGENT (eng, independent review): 24 findings (2 critical, 4 high, 12 medium, 6 low). ALL adopted as plan amendments (mechanical, P1/P5); none challenged Dave's direction. The two criticals:

  • E1.1 project.json durability: sole source of truth with multiple async writers and no atomic-write story. Adopted: serialised mutation queue, temp+fsync+rename, rolling .bak (now in M0 and T2).
  • E2.1 chunk protocol: sequence numbers alone handle reorder, not loss; a lost chunk meant unbounded buffering and a silently truncated take. Adopted: full ack/retry/idempotency protocol with server-side take lifecycle, stall banner, local blob fallback (now in M4; T4 repriced as the hardest code in the app; M1 spike uses the real protocol). Highs adopted: split videoDirty/captionsDirty with param-inclusive cache keys (E1.3); remux every take at close for duration correctness (E2.2); SRT timed from conditioned segments with cumulative actual offsets (E2.3); durability protocol tests including server-kill mid-take and dropped-chunk recovery (E3.1). Mediums/lows adopted across M0/M2/M3/M5 (job records + GET /jobs/:id, timeout scaling + profile warm, pre-produce disk estimate, short-take loudnorm, re-import restriction, script split definition, zip entry caps, image dimension probe, UUID route-param validation before path join, scene-length caption warning, tab-lock takeover, instance lock, encoder flag, pdftoppm numeric ordering, hostile-text SRT fixtures, notes placeholder filtering, long-take fixture, SRT-vs-video alignment assertion).

ENG DUAL VOICES, CONSENSUS TABLE:

  Dimension                     Claude              Codex  Consensus
  1. Architecture sound?        YES after E1.x      N/A    adopted fixes
  2. Test coverage sufficient?  NO > YES after E3.x N/A    adopted fixes
  3. Performance risks?         COVERED (caches)    N/A    confirmed
  4. Security threats covered?  YES (4.1) + 3 gaps  N/A    gaps adopted
  5. Error paths handled?       YES after E2.x      N/A    adopted fixes
  6. Deployment risk?           N/A (local tool)    N/A    instance lock adopted

Sections 1-4 (Architecture / Code Quality / Tests / Performance)

Executed against the amended plan: architecture findings are E1.1-E1.5 above (the scene-card unit, pure pipeline functions and filesystem store were explicitly endorsed); code quality adds UUID validation of route params before any path join (now in T4's scope) on top of the Phase 1 conventions; test review produced the test plan artifact below plus five new suite requirements (durability protocol, SRT alignment, long-take fixture, hostile-text chunking, notes placeholder fixture); performance confirmed the cache design once keys include conditioning/encode params, with no other hot paths for a single user.

Implementation Tasks (eng phase)

  • E1 (P1, human: ~1d / CC: ~1h): store: serialised project.json writer, atomic temp+fsync+rename, rolling .bak (E1.1)
  • E2 (P1, human: ~2d / CC: ~2-3h): recording: full chunk protocol with acks, bounded retry, idempotency, server-side take lifecycle, stall UI, blob fallback; repriced from the original T4 (E2.1, E1.4)
  • E3 (P1, human: ~4h / CC: ~30min): takes: remux at close + ffprobe duration (E2.2)
  • E4 (P1, human: ~4h / CC: ~30min): captions: SRT from conditioned segment durations, cumulative offsets, whole-file regeneration, videoDirty/captionsDirty split (E2.3, E1.3)
  • E5 (P2, human: ~1d / CC: ~1h): tests: durability protocol suite (drop chunk N, kill server mid-take), SRT alignment assertions, 5-minute fixture, hostile-text chunking fixtures, notes placeholder fixture, >10-page deck fixture (E3.1-E3.6)
  • E6 (P2, human: ~4h / CC: ~30min): jobs: records + GET /jobs/:id + interrupted-on-restart; instance lock; encoder config flag (E1.2, E5.4, E3.7)
  • E7 (P3, human: ~2h / CC: ~15min): ingestion hardening: zip entry caps, image dimension probe, timeout scaling, profile warm (E4.2, E4.3, E2.4)

Completion Summary

  Mode: SELECTIVE EXPANSION | Audit: greenfield, docs only, no stashes/TODOs
  Step 0: premises confirmed by Dave; Approach A; 9 expansion proposals decided
  S1 Arch: 4 findings (adopted) | S2 Errors: 20 paths mapped, 0 gaps
  S3 Security: 5 threats, 5 mitigated | S4 Data/UX: 14 edge cases, 0 unhandled
  S5 Quality: conventions locked | S6 Tests: diagram + suite plan, chromium pitfall avoided
  S7 Perf: 3 findings (adopted) | S8 Observability: 5 adoptions | S9 Deploy: n/a analogues covered
  S10 Future: reversibility 4/5, debt 1 deliberate (captions) | S11 Design: flow + state map, design phase next
  NOT in scope: 7 items | Registries: complete, 0 CRITICAL GAPS | CEO plan: written (9/10 after 2 review rounds)
  Outside voice: subagent-only (Codex absent), 10 findings, 2 adopted, 7 to final gate, 1 noted
  Unresolved decisions: 7 gate items (F1, F2, F3, F5, F6, F7, F10 + waveform cherry-pick)

Decision Audit Trail

# Phase Decision Classification Principle Rationale Rejected
1 CEO Approach A (local server + browser) Mechanical P1+P5 Highest completeness, no Electron overhead Electron, CLI-only
2 CEO Accept expansions 1,2,4,5; fold 9 into M4 Mechanical P2 In blast radius, under a day each none
3 CEO Defer expansions 6,7,8 to TODOS Mechanical P3 Outside core blast radius building now
4 CEO Waveform UI to final gate Taste P3/P1 Genuinely two-way auto-include
5 CEO Notes via pptx XML parse Mechanical P4/P5 LibreOffice PDF path never carried notes office-suite notes path
6 CEO LibreOffice fidelity spike into M1 Mechanical P3 Riskiest dependency verified first verify at M2
7 CEO F1/F2/F3/F6/F7/F10 to final gate User-adjacent gate rule Challenge Dave's settled decisions or need his data auto-deciding them
8 Design Unified scene cards Mechanical P5 Deletes the sync problem structurally two synced lists
9 Design M2.5 design milestone before editor build Mechanical P1 Polish cannot rescue structure M6-only polish
10 Design Audition split from waveform, into M4 Mechanical P1 Take selector requires playback keeping them bundled
11 Design Teleprompter spec (focus band, countdown, R, ESC, no auto-advance, decoupled scroll) Mechanical P5 Ambiguity here breaks takes and captions leaving to implementer
12 Design Resume-last-project launch Mechanical P2 Single user, zero-ceremony browser-first landing
13 Eng Serialised atomic project.json writes Mechanical P1 Corruption destroys the project best-effort writes
14 Eng Full chunk ack/retry protocol + server take lifecycle Mechanical P1 Silent take truncation is the worst outcome seq-numbers-only
15 Eng Remux every take at close Mechanical P5 Duration is load-bearing remux only recovered
16 Eng SRT from conditioned segments, videoDirty/captionsDirty split Mechanical P1 Caption correctness + re-produce speed raw-take timing
17 Eng Keep browser capture (hardened) over avfoundation Taste (resolved) P3 Eng durability story built around it; native capture adds permission and monitoring complexity server-side native capture
18 Eng Job records + rehydration endpoint; SSE as push only Mechanical P5 Streams are not state SSE-only
19 Eng 12 medium/low hardening items adopted Mechanical P1 Cheap now, expensive later deferring

Cross-Phase Themes

Theme: script-timed caption accuracy. Flagged independently by the CEO voice (F6: whisper.cpp is cheap and local now) and the eng voice (5.2: even-spread degrades with scene length and the decoupled scroll makes pace changes invisible to caption math). High-confidence signal that captions are the weakest deliberate cut in the plan. The adopted mitigation (scene-length warning + SRT from conditioned segments) keeps v1 honest; the upgrade decision is at the gate (item 4).

No other concern appeared in two or more phases independently.

GSTACK REVIEW REPORT

Review Trigger Why Runs Status Findings
CEO Review /plan-ceo-review Scope & strategy 1 issues_open (PLAN via /autoplan) 9 proposals, 5 accepted, 3 deferred
Codex Review /codex review Independent 2nd opinion 0 not installed subagent voices used instead
Eng Review /plan-eng-review Architecture & tests (required) 1 CLEAR (PLAN via /autoplan) 24 issues, 0 critical gaps remaining
Design Review /plan-design-review UI/UX gaps 1 issues_open (FULL via /autoplan) score: 6/10 to 8/10, 13 decisions
DX Review /plan-devex-review Developer experience gaps 0 skipped no developer-facing scope

VERDICT: CEO + DESIGN + ENG CLEARED. Plan APPROVED at the final gate on 2026-07-13; all seven gate items resolved per "Pre-build evidence tasks" above (items 1-3 and 6 became pre-build tasks; captions, M6 and browser capture kept; waveform deferred). Ready to implement, starting at M0 after the pre-M0 evidence tasks.

NO UNRESOLVED DECISIONS