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.
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.
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.
- Node.js backend (Express or Fastify) + React frontend (Vite), single
npm startstyle launch command that openshttp://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>/holdingproject.json(scenes, script blocks, take metadata),slides/,audio/,output/. project.jsonwrites are serialised through a single in-process mutation queue; every write is temp file + fsync + rename with a rolling.bakof 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/:idfor rehydration; SSE is only the push channel over that record. On server start, anyrunningjob 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_videotoolboxdefault,libx264fallback (one line now; keeps tests runnable off-Mac later).
- 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.
- 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).
- 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).
- 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.
- 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.)
- 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) vscaptionsDirty(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.
- "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.
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.
- 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.
- NAS archive share/path (blocks M6 only).
- Logo direction (blocks nothing; M6).
- 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.
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).
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.
| 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.
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.
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.
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).
- HOUR 1: port fixed (default 5173 dev / 3555 app; final call at M0), project dir layout
and
project.jsonschema 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:UserInstallationprofile 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.
SELECTIVE EXPANSION (fixed by /autoplan). Approach A confirmed under this mode.
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.
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):
- Bind 127.0.0.1 explicitly, never 0.0.0.0; the server must not be reachable from the LAN.
project.jsoncarries aschemaVersionfield 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.- 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.
- 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.
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.
| 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.
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.
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.
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).
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.
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.
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.
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.
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.
- 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).
- Compass
platform/apiandplatform/spa: Node/React conventions, upload handling. - DHE build pipeline: LibreOffice slide rasterisation precedent.
- Descript workflow: the current production path; stays usable throughout the build.
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.
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.
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.
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.
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)
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
- 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.
- Interaction State Coverage 7 > 9. Editor row added, teleprompter has its own state map, recovered takes have a resolution path.
- 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.
- 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+).
- Design System Alignment 2 > 5. No DESIGN.md exists; M2.5 creates it. Recommendation stands: run /design-consultation as part of M2.5.
- 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.
- 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.
- 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)
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
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.
- 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)
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)
| # | 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 |
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.
| 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