# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Quick orientation (read this first)

- **Entrypoint:** `src/cli/vclaw.ts` — a thin ~680-line dispatch shell; all command logic lives in `src/cli/handlers/*.ts` (40 modules).
- **Domain core:** `src/video/*` — small single-purpose modules (projects, execution, assemble, audio-platform, studio, preview-portal, motion-overlay, provider-platform, …).
- **Contracts & state:** `schemas/video/*` JSON Schemas are the source of truth for artifact *shapes*; the on-disk `projects/<slug>/` tree is the source of truth for project *state*.
- **Tests:** `node:test` files in `src/tests/*.test.ts`, run from compiled `dist/tests/` (`npm test`).
- **Also load:** the sibling `.claude/CLAUDE.md` for the `graphify`, `improvement-run`, and `concierge`/`videoclaw` skill triggers.

> **Repository status (2026-07-02):** This is `videoclaw-v3`, the current repo —
> unified from `videoclaw-v2`, itself the merged successor of the older `videoclaw`
> package and the clean-room `vclaw-video-core` rebuild. (The
> npm package is `videoclaw`; "videoclaw-v3" is just the repo name.) Foundation
> copied from `vclaw-video-core`; presenter skills synced from
> `video-creation-projects/video-replicator-veo-cli/.claude/skills/`; Runway
> transport ported from `videoclaw/src/video/providers/runway-useapi.ts`.
> `MERGE_PLAN.md` holds the merge's architecture rationale and phase history
> (all phases but source retirement are done). Current status lives in
> `docs/RELEASE_READINESS.md` and `CHANGELOG.md`; the shipped architecture in
> `docs/ARCHITECTURE.md`.

## Repository purpose

`videoclaw` is a TypeScript/Node.js 20 multi-provider video CLI (`vclaw`). It targets Veo (Google Flow + UseAPI, including Omni Flash), Seedance, and Runway. Every pipeline stage is explicit, every artifact is machine-readable JSON, and provider routes never silently fall back across materially different paths. The on-disk per-project layout (`projects/<slug>/{project.json, artifacts/, checkpoints/, characters/, events/, ...}`) is the source of truth; the CLI is a thin operator over it. A browser-based Review UI (`vclaw video review-ui`) handles human-in-the-loop storyboard approval.

## Concierge front door (user-facing requests)

`skills/concierge/SKILL.md` is the user-facing front door, and it speaks as
**VideoClaw** — the product itself (`videoclaw` is a persona alias of the skill).
The on-screen mascot is VideoClaw too: Go Bananas character 291, renamed from
its earlier name on 2026-09-09; the artwork is unchanged.
When the user asks to make a video, gives an ambiguous creative request, types
`/concierge` or `/videoclaw`, asks for VideoClaw, or seems new to the system, read
that skill and follow it before doing anything else: greet them as VideoClaw,
present the menu, route their choice through its lane table, and keep the
plan → preview → spend order (never spend without explicit go-ahead).
Engineering and maintenance requests on this codebase itself are NOT concierge
territory — handle those normally.

## Working discipline — ground in what already exists before you build (READ THIS FIRST)

This project keeps getting bitten by the same failure: **improvising a fresh
(worse) approach instead of reusing the proven artifact that already exists**, and
silently dropping load-bearing pieces between turns.

**The honest root cause is a default to fast *visible* output** — a clip, an HTML,
a keyframe, a render — **over a provably-correct setup.** A fast wrong render is
slower than a slow right one: it burns spend, trust, and rework. The rules below
are forcing functions against that default; the friction IS the point. The
highest-leverage rules are **0, 1, and 4** — do them even when it feels slower.

Before authoring or changing **any** skill, procedure, function, prompt, render
setup, or review surface — and before creating a new command — work through this:

0. **Read the spec in full, once, up front — and write the contract down.** When
   the user points to an authoritative method file (a doc, `WORKFLOW.md`, a
   compliance sheet, a working harness script, a `--validate`d prompt), read the
   WHOLE thing first and extract its requirements into a written checklist/contract
   (a file or notepad) BEFORE building. Do NOT skim it on demand and discover the
   pieces one correction at a time — that is exactly what makes the operator repeat
   himself. For any multi-step build/production, keep that contract on disk and
   **re-read it each turn**; requirements held only in working memory erode over a
   long session. The on-disk contract, not your memory, is what you build against.
1. **Inventory before you build (the #1 rule).** Search first. Grep/read the
   relevant `src/video/*`, `src/cli/handlers/*`, `skills/*`, `docs/*`, the
   project's on-disk `artifacts/*` (e.g. `show-bible.json`, `voice-clones.json`,
   `story-bible`, `flow-characters.json`, `seedance-assets.json`), and any
   `references/` assets, proven harness scripts, or already-`--validate`d prompts.
   If a working artifact exists, **use it** — never regenerate an ad-hoc
   substitute. Most "why did this break again" pain is re-deriving something that
   was already solved and written down.
2. **Artifacts/registries are the source of truth, not memory.** Drive work from
   the on-disk artifacts (the `show-bible` registry, voice clones, reference
   sheets, validated prompt files) — not from paths or prompts re-typed from
   memory. If you catch yourself re-typing a file path or re-authoring a prompt
   that already exists, stop and load it from the artifact.
3. **Prompting is per-model — never reuse one prompt style across routes.**
   Seedance/Runway/Dreamina lock identity from **references + a rich per-subject
   visual descriptor** (never a generic "the man") + the multi-shot framework, and
   take character-sheet + location + per-speaker voice refs. Flow (`veo-useapi`)
   locks identity on a **registered Flow Character** and tolerates generic
   prompts. Image models (GB, `openai-gpt-image-2`) differ again. Check the
   route's ruleset (and any model-specific prompting skill/doc) before authoring a
   prompt or choosing references.
4. **Encode the method so the TOOL enforces it — don't rely on remembering.**
   When a procedure has required inputs/steps, add a fail-fast preflight/readiness
   gate that ERRORS (non-zero, clear blocker) when a required piece is missing, and
   prefer **registry-driven auto-attach** over manual setup. The target shape: an
   operator just prompts; the tool attaches the right inputs and refuses to render
   wrong (see `show-preflight` + show-bible auto-attach).
5. **Review-first, one-at-a-time for any spend.** Before any paid or slow
   generation, emit the exact **contract** — the prompt + the references + the JSON
   that will actually be submitted — for review (the review portal IS the
   contract; show the EXACT submit payload, not a paraphrase). Then render ONE
   unit, verify it (extract a frame + whisper the audio / QC), get approval, and
   only then the next. Never batch-spend on an unreviewed setup. **Immediately
   before submitting, dry-run `buildExecutionPayload` and confirm the resolved
   refs/prompt/route match the approved contract** — the actual submission has
   silently diverged before (an `@tag` hijacked the references and dropped the
   approved sheets/location). If it doesn't match the contract, do not submit.
6. **New command/skill = conventions below + steps 1 & 4.** Still do the full
   registration (handler + `vclaw.ts` dispatch + `cli-schema.ts` `COMMANDS` entry +
   bump the `cli-schema.test.ts` count + a `cli-*.test.ts` + README/CLI_REFERENCE +
   a schema for any new artifact), but **inventory first** (step 1) and add a
   **preflight gate** (step 4) if it has required inputs.
7. **Verify OUTCOMES, not that a command ran — and never report unverified
   progress.** "Launched a driver" / "submitted" is NOT "it's working." After any
   submit or render, confirm the real provider-side state before claiming it: a
   live job exists (`execute-status` → `live-submitted` + a candidate + a
   `.vclaw-jobs/*.json` file), and then the actual output downloaded + QC'd (frame
   + whisper). If you tell the user something is happening, prove it (a job id /
   status / a frame) — do not assert progress you have not checked.

   **QC the whole clip, not one frame.** A single mid-frame grab is not
   verification: it passed 8 clips whose defect (extra figures walking in from the
   frame edges) only began around the four-second mark, and the operator found it
   on screen after delivery. Use `vclaw video clip-qc`, or a filmstrip —
   `ffmpeg -i clip.mp4 -vf "fps=1,scale=200:-1,tile=8x1" strip.jpg` renders every
   second in one pass. `consistency-audit` (one mid-frame, identity) and
   `motion-qc` (nine frames, morph/vanish) are advisory and never block, so read
   their findings rather than treating silence as clean.

   **Re-render corollary — deleting the clip does NOT force a re-render.** The
   resumable drivers resume from the **scene-selection artifact**, never from
   disk, so a changed prompt plus a deleted `outputs/scene-N.mp4` yields
   `skipped, attempts=0` and no provider call at all. Clear the selection with
   `vclaw video reroll-scene --project <slug> --scene <i> --void` (it voids all
   four stores atomically). After any re-render read the **per-scene statuses**,
   not the file count — a silent no-op looks exactly like success from the
   outside.

When in doubt, the on-disk project and its artifacts win over anything you
remember.

## Build, test, and smoke commands

```bash
npm install                              # Node 20+
npm run build                            # tsc into a staging dir, chmod the CLI bins, then SWAP it into dist/ (scripts/build-atomic.mjs): the previous dist/ stays complete for the whole compile, a failed compile leaves it in place, at most one build SWAPS per checkout (.videoclaw-build.lock — taking over a dead builder's lock is racy, so a builder that loses the lock discards its output; a lock older than 30 min or with a dead pid is stale, and a refusal names the file to delete)
npm run dev                              # tsc --watch
npm test                                 # rebuild, then node --test dist/tests/*.test.js
npm run test:node                        # rerun compiled tests without rebuilding (serial by default; VCLAW_TEST_CONCURRENCY=4 saturates a 4-core box — CI additionally splits it with `--shard i/4` across four runners, see .github/workflows/ci.yml)
```

Run a subset after `npm run build` (or `npm run dev`) — the arg is a filename
substring:

```bash
npm run test:node -- cli-full-flow
```

Prefer this over a bare `node --test dist/tests/...`. The runner points `python3`
at the interpreter `check:test-python` pins; invoking `node --test` directly runs
the suite's ~114 Python call sites on whatever `python3` PATH happens to give,
which is how a drifted Pillow once passed the gate and silently changed
title-card rendering.

End-to-end smokes (each runs `npm run build` first) and local guardrails:

```bash
npm run smoke:runtime                    # init → brief → storyboard → assets → plan → produce --dry-run → status → report → Obsidian
npm run smoke:native-veo                 # native Veo (Flow/Bun) transport path
npm run smoke:character-hydration        # create-time cast hydration + approval-gate cost
npm run smoke:execution-cancel           # adapter + project-level cancel
npm run smoke:portfolio                  # index → report → export-csv visibility
npm run smoke:reference-sheets           # character reference-sheet generation path
npm run smoke:scene-candidates           # scene-candidate generation path
npm run smoke:story-bible-image          # story-bible image-only path
npm run smoke:assemble                   # FFmpeg assemble/stitch layer (dry)
npm run smoke:assemble-render            # real FFmpeg render validation
npm run smoke:multi-shot                 # multi-shot prompt plan→validate round-trip
npm run smoke:rap-dryrun                 # rap lane: a solo and a duet film built from nothing reach qc (no renders, no spend; needs ffmpeg-full, macOS say)
npm run e2e:image-storyboard             # image-storyboard workflow (runs --verify-server)
npm run e2e:image-storyboard:examples    # image-storyboard e2e against the bundled examples
npm run check:movie-director-wrappers    # bundled Director helper scripts
npm run check:cleanroom-docs             # clean-room docs + skills
npm run check:skill-frontdoor            # repo-local skill front door
npm run check:artifact-schema-coverage   # writers vs schemas drift (advisory)
npm run check:artifact-schema-coverage:strict  # same, but fails the build on drift (--strict)
npm run check:release-readiness-lite     # one-shot: build + tests + main smokes + guardrails
npm run acceptance:live -- --route <id> [--veo-model free] [--operation t2v|i2v|r2v] [--approve]  # ONE quoted provider job per route → an audit row (dry without --approve; the repeatable form of docs/audits/*-live-acceptance.md); i2v takes --input <image>, r2v takes --character <name> --character-project <slug> (Flow only); --kind narrate|soundtrack|image-upscale [--backend <id>] certifies an audio / Magnific backend the same way; --kind cinema --sheet-dir <dir> walks the evidence-gated cinema ladder and submits ONE paid Higgsfield CLI shot
npm run lint                             # eslint src --max-warnings=0 (eqeqeq, no-var, prefer-const, no-eval family); dead imports/locals are a BUILD error instead (tsconfig noUnusedLocals, since 2026-09-15 — four slipped every gate in Phase 3)
npm run check:module-size                # fails on any src/**/*.ts over 1,000 lines; legacy giants are frozen at per-file ceilings in scripts/check-module-size.mjs — lower them, never raise them
npm run check:seedance-engine            # in-tree free Seedance engine present + wired + offline stub self-test (no browser, no spend)
npm run check:build-fresh                # dist/ is not stale relative to src/
npm run frontdoors:check                 # .claude/CLAUDE.md skill front-door blocks match scripts/skill-frontdoors.json (regenerate: npm run frontdoors:install)
npm run test:coverage                    # build, then compiled tests under coverage
npm run demo                             # zero-key creator-demo quickstart in a temp workspace (providerCalls: 0)
npm run docs:sync                        # regenerate docs-site/reference/ from docs/ — run after ANY docs/*.md edit (CI: check:docs-site)
npm run graph:full                       # rebuild the graphify knowledge graph (graphify-out/)
npm run shared-queue:status              # `vclaw video lane status` against the shared coordinator; shared-queue:setup / :verify / :dashboard / :self-test manage services/shared-lane-coordinator
```

`npm run check:release-readiness-lite` is the preferred local pre-flight before non-trivial changes land.

**CI is checks-only by default (since 2026-09-15, #525).** Every PR and every push to
main runs the `checks` job (build, lint, coverage, smokes, guardrails and the docs guard
tests — about 4 billable minutes); the four test shards and the Bun sidecar do NOT run
unless the PR carries the `ci:full` label, the workflow is dispatched by hand, the
change touches `.github/`, or it is the nightly run on main (03:00 UTC), which opens a
dated issue when red. A third conditional leg (#575): a PR that touches
`services/shared-lane-coordinator/` also runs that package's own deploy gate — typecheck,
vitest in real workerd, a credential-free Wrangler dry run, on Node 22 — because the root
build never compiles it and vitest does not typecheck (#570 merged green over three type
errors); every other PR skips it. The account's Actions budget is the reason. So: run `npm test`
(or the lite gate) locally before merging a code change, and add `ci:full` to a PR you
want the shards to prove on GitHub. The required check keeps its name; a green
`build · test · smokes · guardrails` on an unlabelled PR means checks passed and the
shards were deliberately skipped — read the run's job list before treating it as a
full-suite pass.

## Big-picture architecture

### Layers (read top-down)

1. `src/cli/vclaw.ts` — the single user-facing entrypoint, now a thin dispatch shell (~680 lines: imports, `printHelp`, the `NOUN_VERB_ALIASES` map, `resolveSubcommand`, the `VIDEO_DISPATCH` table, and `main`). The decomposition is COMPLETE (PRs #89–#120): pure arg/slug/spend helpers live in `src/cli/args.ts`, and ALL command handlers live in `src/cli/handlers/` (40 modules — the original 25: `analysis`, `audio`, `batch`, `candidates`, `character`, `clone`, `create`, `execution`, `library`, `media-ops`, `media-production`, `migrate-home`, `monitor`, `motion-overlay`, `multi-shot`, `project-ops`, `prompt-craft`, `provider-registration`, `reference-sheets`, `reporting`, `review-portal`, `show`, `stages`, `studio`, `templates`; plus the later families `cinema`, `cinema-archive`, `cinema-candidate`, `cinema-delivery`, `cinema-execution`, `cinema-history`, `cinema-image-jobs`, `cinema-migration`, `creator-demo`, `creator-ui`, `lane`, `mograph`, `publish-platform`, `stock`, `vocal-guides`), each a verbatim extraction with module-private helpers and only dispatch-facing handlers exported. ⚠️ When moving handler code, watch for position-sensitive path math (`import.meta.url` etc.) — the compiled location changes (see the documented adaptations in `handlers/studio.ts` and `handlers/motion-overlay.ts`). `src/cli/provider-adapter.ts` is the built-in adapter binary for all seven built-in adapter routes (`seedance-direct`, `veo-useapi`, `runway-useapi`, `dreamina-useapi`, `magnific-rest`, `seedance-modelark`, `reapi-seedance`; see Provider routes below). Besides `video`/`studio`, `main` also dispatches two smaller families: `vclaw mcp serve` (`src/mcp/` — a read-only stdio MCP server exposing `list_projects`, `get_project_status`, `get_artifacts`, `get_event_log`, `list_provider_routes`; writes stay CLI-only by design) and `vclaw veo <verb>` (`src/video/veo-subprocess.ts` — spawn-forwarding to the Bun-based `vclaw-cli/flow.ts`, required for Puppeteer/Google Flow verbs like `status`, `resume`, `cancel`, `useapi:*`).
2. `src/video/` — the core domain. Each file is small and single-purpose, e.g. `artifacts.ts`, `artifact-store.ts`, `checkpoints.ts`, `workspace.ts`, `projects.ts`, `status.ts`, `doctor.ts`, `doctor-portfolio.ts`, `readiness.ts`, `execution-plan.ts`, `execute.ts`, `execution-runtime.ts`, `execution-status.ts`, `execution-cancel.ts`, `director-preflight.ts`, `report.ts`, `csv-export.ts`, `obsidian-export.ts`, `project-index.ts`, `metrics.ts`, `next-actions.ts`, `template-store.ts`, `provider-status.ts`, `native-seedance.ts`, `native-veo.ts`, `multi-shot-prompt.ts`, `storyboard-grid.ts`, `cinematography.ts` (detail-leveled quantified camera/lighting/grade/audio emitters — `--detail terse|standard|rich`; since Phase 3a the lighting/grade, hook, realism and dynamic tables live in `lighting-grade-register.ts` / `hook-register.ts` / `realism-register.ts` / `dynamic-register.ts` and are re-exported from it, so import from `cinematography.js` as before), `prompt-rules.ts` (standing prompt rules: visual-descriptor-not-names, brand-neutral, no-face-morph, diegetic audio), `seedance-asset-library.ts` (Asset Library character/product consistency), `story-bible.ts` (deterministic continuity bible — cast/settings/props/scene-timeline from brief + storyboard + characters), `brand-definition.ts` (locked brand system — palette/voice/typography/theme map; `filmmaking-prompts` appends a prose BRAND line when present), `assemble/media-qc.ts` (post-stitch ffprobe QC of clips + master), `assemble/narration-fit.ts` (TTS-vs-video timing planner: atempo / loop-video fit). `src/video/studio/` is the planning front door and `src/video/preview-portal/` is the review/delivery portal (both described below).
3. `src/video/provider-platform/` — route descriptors (Veo / Seedance / Runway direct and useapi flavors).
4. `src/video/pipeline-manifests/` — built-in stage definitions for the two production modes.
5. `schemas/video/` — canonical JSON Schema contracts for artifacts and pipeline manifests. Treat these as the source of truth for artifact shapes.
6. `src/tests/` — `node:test` files named `*.test.ts`. `dist/tests/**.test.js` runs via `node --test`.
7. `src/index.ts` — the public library surface; re-exports the subset of `src/video/*` that should be callable from outside the CLI.

### Project lifecycle (per project, on disk)

After `vclaw video init <slug>`, a project lives at `projects/<slug>/` under the workspace root and is the unit of everything else. **The workspace root is the ONE canonical home `~/videoclaw`** (`CANONICAL_WORKSPACE_ROOT` in `src/video/workspace-root.ts`), resolved as `--root` flag → `VCLAW_WORKSPACE` → `VIDEOCLAW_WORKSPACE` → `~/videoclaw` — this replaced the old per-invocation `process.cwd()` default that scattered projects wherever `vclaw` happened to run. `vclaw video migrate-home` consolidates scattered projects into the home (dry-run plan by default; `--confirm` moves each project and leaves a symlink at the old path so old absolute paths still resolve).

```
projects/<slug>/
  project.json                 # manifest: slug, mode, state, metadata, execution profile
  artifacts/                   # canonical JSON: brief, storyboard, story-bible, asset-manifest, review-report, publish-report, analyze-output, clone-plan, execution-plan, execution-report, readiness, character-consistency, ...
    history/                   # artifact snapshots (append-only)
  checkpoints/                 # one file per stage: brief, storyboard, assets, review, publish; tracks approval states
  events/events.jsonl          # append-only timeline
  state/                       # derived state cache
  characters/characters.json   # optional character profiles with GB identity anchors
  storyboard.md                # director-mode approval review file (human-readable)
```

Canonical stage order: **init → brief → storyboard → assets → review → publish**. `readiness`, `plan`/`execution-plan`, `produce`/`execute`, `execute-status`, `execute-cancel`, `execute-bind`, `execute-abandon` are the runtime-execution layer that sits between assets and review.

`produce`/`execute --auto-chain` and `vclaw video pool` are **durable-queue compilers, not renderers** (since the cinema queue landed, PRs #360/#364). `--auto-chain` compiles the storyboard into `artifacts/cinema/production-queue.json` as a dependency chain (each scene's task seeds from its predecessor's *selected* candidate; `cinema-chain-fallback.ts` carries the image-only fallback policy per link), `pool` enqueues the pending scenes as independent tasks, and both return `{ enqueued, pendingScenes, providerCalls: 0, spendAuthorized: false }` without touching a provider. `--execute`, `--dry-run` and `--confirm-spend` are refused on both (`--execute` is retired). The queue drains one task at a time through `vclaw video cinema-work --task <id>` (`src/cli/handlers/cinema-execution.ts`): a task compiled for any paid route — which includes `veo-useapi` even when the model is the 0-credit `veo-3.1-lite-low-priority` — is `authorizationRequirement: 'exact-quote'` and needs a bound authorization + repeated quote hash + `--confirm-spend` + a fresh quote-adapter observation; only `runway-useapi` explore mode is a provider-free submission (`--confirm-provider-call`). The chain-from-prev engine underneath is unchanged: `resolveSceneReferencePaths` (`execution-runtime.ts`) keeps BOTH a seedance scene's character `Asset://` refs AND the keyframe video, and `execute-autochain.ts` keeps `waitForSceneVideo` / `resolveInFlightScene` for the submit-only async routes (a chain seed must be a candidate that already HAS a video, or `chain-from-prev-source-missing` fires). Continuation advisories (`continuation-handoff.ts`, `analyzeChainContinuity`) still flag chain-depth drift.

`vclaw video render-scenes` (`execute-render-scenes.ts`, `runRenderScenes`) is the one scene driver that still renders **directly** (a PURE scheduler + an injectable per-scene runner, offline-testable): sequential, walking a per-scene **fallback route ladder** — try route A, escalate to B then C on provider rejection, first success wins — and spend-gated by `--confirm-spend`. Plain `produce` (no `--auto-chain`) also still submits directly and re-submits a scene whose clip was deleted; the "resumes from the scene-selection artifact, so deleting the clip is a silent no-op" trap in the working-discipline rules applies to the queue-driven paths, where `reroll-scene --void` is what clears a selection.

### Two production modes

Every command accepts `--mode storyboard|director`. Pipeline manifests under `src/video/pipeline-manifests/` define the stage contract per mode. `director` mode adds a storyboard-approval gate: `produce`/`execute` export `storyboard.md` and block before provider submission unless `VIDEOCLAW_APPROVE_STORYBOARD=1` is set or the run is `produce --approve` (the command the blocked report prints; `approve` is a deprecated spelling of it). Plain `produce` has no `--confirm-spend` gate and refuses that flag (and `--execute`) rather than ignoring it; every direct front door reaches the provider through `executeProject` (ADR 0008 amendment). `storyboard-review` (no-execution) can perform preflight + transition the project into `awaiting-approval` without starting a run.

### Studio front door (planning layer)

`vclaw studio` (`src/video/studio/`, handler `handleStudio` in `src/cli/handlers/studio.ts`) is a human-friendly planning front door that sits *above* the low-level CLI — it does not replace it. **Plan-only by default:** it builds a `StudioPlan` from a goal and prints the exact `vclaw video ...` commands and artifacts that would run, without calling providers/FFmpeg. **`--execute` (Phase 2) RUNS the emitted plan** via `src/video/studio/execute.ts` (`runStudioPlan`) by shelling out to the same `vclaw video` commands — it is NOT a second orchestrator (no readiness/route/approval logic is re-derived; the plan stays the source of truth). Three run-time modes: **default `--execute` is provably dry** (`classifyStep` refuses any `SPEND_SUBCOMMANDS` step lacking `--dry-run`; dry spend steps keep `--dry-run`), running free+dry steps and pausing at the real director gate (a `blocked` child) with fail-fast; **`--confirm-spend`** promotes dry spend steps to real renders (`stripDryRunArgv` removes `--dry-run`) but `studioChildEnv` still strips `VIDEOCLAW_APPROVE_STORYBOARD` so the storyboard gate keeps blocking unless approved out-of-band; **`--confirm-spend --auto-approve-storyboard`** sets the approval var for an unattended render (auto-approve without confirm-spend throws). `--from-step <id>` resumes. The runner is pure with an injectable `StudioStepRunner` (offline-tested in `studio-execute.test.ts`/`studio-classify.test.ts`). The planner stays pure/deterministic apart from `session.ts`:
- `recipes.ts` — `STUDIO_RECIPES`, one `StudioRecipe` per goal (command templates, required/optional inputs, `riskLevel`, `executionPolicy`).
- `planner.ts` — `buildStudioPlan()` resolves the goal, fills `<placeholder>` command templates, computes `missingInputs`/`warnings`, and emits a `StudioPlan` (`schemaVersion: 1`).
- `project-context.ts` — `loadStudioProjectContext()` reads readiness + next-actions to enrich the plan; `session.ts` `writeStudioSession()` persists `projects/<slug>/artifacts/studio-session.json` when `--write-session` is passed.
- `types.ts` — shared `StudioGoal` (10 goals), plan, and recipe types.

Goals (each has a short alias, e.g. `presenter`→`presenter-video`): `create-video`, `copy-reference`, `presenter-video`, `music-video`, `ugc-campaign`, `existing-project`, `review-regenerate`, `publish-deliver`, `brand-campaign`, `character-video`. Studio output is JSON on stdout. When extending it, add the recipe to `recipes.ts`, the goal+alias to `handleStudio`, a `studio-*.test.ts`, and update `docs/STUDIO.md`; the command is also registered in `src/video/cli-schema.ts` `COMMANDS` (whose length is asserted by `cli-schema.test.ts`).

### Review-state ladder

The ops layer tracks a normalized `storyboardReviewState` of `missing | current | stale`. This flows through status, index, report, CSV export, Obsidian export, dashboards, next-actions, snapshot diffs, and the doctor layer. A stale director review blocks `execute`/`execute-status` at runtime even if approval is set. When touching review/approval logic, keep this ladder consistent across all surfaces.

### Review & delivery portal (`src/video/preview-portal/`)

The portal generates the standardized HTML surfaces that used to be hand-written per project: `edit.html`/`review.html` (editor/operator human-in-the-loop, with approve/regenerate controls and `VIDEOCLAW_REVIEW_DECISIONS` copy output), `client-review.html` (lightweight client approve/decline/comment, `VIDEOCLAW_CLIENT_FEEDBACK` copy output), and `preview.html` (polished final showcase: every production image is lightbox-enabled for click-to-fullscreen, a soundtrack `<audio controls preload="none">` player renders when a soundtrack is discovered, plus downloads). The module is split into `discovery.ts` (find project assets), `generate.ts` + `templates.ts` + `shared-assets.ts` (render the surfaces), `render.ts`/`publish.ts` (emit/ship — since Phase 3d `render.ts` keeps the surface router and imports `render-primitives.ts` (escaping, aspect, dates), `render-scene-contract.ts` (the exact-submit contract), `render-run.ts` (the run dashboard) and `render-brand.ts` (brand theming)), and `audit.ts` (drift checks); `src/video/review-ui.ts` (`vclaw video review-ui`) serves the editor surface interactively. The decisions/feedback flow back through env-var copy blocks rather than a server round-trip, keeping the on-disk project the source of truth. See `docs/preview-portal-audit.md`.

### Mission Control (`vclaw video monitor`)

`src/video/monitor/` serves a read-only localhost cockpit (default port 8765): `monitor/discovery.ts` discovers every project across workspace roots (`--root` adds extra roots to scan) and the server renders a live overview from the on-disk artifacts. Never calls a provider, never spends.

### Storyboard grid & multi-shot prompt handoff

`src/video/multi-shot-prompt.ts` builds project-ready, provider-tuned multi-shot prompt packets (presets via `vclaw video multi-shot --presets`; see `references/video/multi-shot-framework.md`, especially its Anti-patterns section). `multi-shot --plan` can render through alternate composers via `--format default|seedance-paragraph|per-shot` (default = the original `{ preset, shots[] }` JSON, unchanged) and wrap the rendered text bilingually via `--lang en|zh|en+zh` (offline identity translator — the flag surfaces the two-block `en+zh` structure, not live translation); `--category <id>` drives the composed prose. On a non-`default` `--format`, `--hook <patternId>` (named `HOOK_PATTERNS` opening directive from `hook-register.ts`, re-exported by `cinematography.ts`) prepends an `Opening hook — ...` line and `--dialogue "<speaker>: <line> [|| <speaker>: <line>]"` appends spoken dialogue to the opening via `withDialogue` — both are post-render text transforms (composers stay pure), default off / unchanged output. `--dialogue` parses a trailing `[emotion]` per speaker, and `--emotion-cues` rewrites those named emotions into physical-cue descriptors (`rewriteEmotionAsPhysical`/`EMOTION_PHYSICAL_CUE_MAP` in `emotion-cues.ts`) — advisory/additive, extremes left named, default off / byte-identical. `filmmaking-prompts --phase storyboard|video` gates the heavy video `seedancePackets` to `[]` in the `storyboard` phase (lock-the-grid step) while keeping the storyboard/camera-language portion. Joey cinematic-adaptation opt-in flags (all additive — omit = byte-identical legacy output) route through these same commands: `filmmaking-prompts` takes `--sheet 8-shot|6-panel` (`characterSheetSixPanelPrompt`), `--realism`/`--wet`/`--haze thin|light|heavy` (the `captureRealismBlock` keystone + `volumetricHaze`, at `--detail rich`), `--background mid-gray|white|black` (`backgroundPlate`), and `--lighting <id>`/`--grade <id>` (rich cinematography-suffix registers, e.g. `night-fire`/`bleach-bypass`); `multi-shot` takes `--genre <id>` (`resolveStyleLine`, Nolan fallback) and `--vfx <id>` (physical-VFX effects register, `src/video/vfx-register.ts`). Operator trigger-word map: mid-gray → `backgroundPlate`; haze → `volumetricHaze`; anti-plastic → `captureRealismBlock`; wet → moisture clause; bleach-bypass/lifted-blacks → lift/gamma grade; no-on-screen-text → `noOnScreenTextBlock`, the FIRST directive block (it moved out of Last Frame: overlay text is decided early in the frame, so the instruction has to sit early in the prompt). See `docs/CLI_REFERENCE.md` ("Joey cinematic opt-in flags") and the framework Anti-patterns.

`src/video/storyboard-grid.ts` (`vclaw video storyboard-grid`, `renderStoryboardGrid`) renders a **deterministic shot-spec sheet** — a 3×3 SVG→PNG of CAM/MOVE/MOOD annotation panels — **not** a cinematic storyboard with character imagery. It is the *layout/intent contract*, not the finished reference image. The intended two-step is: (1) `storyboard-grid` to lock panel order + camera language, then (2) generate the real cinematic 3×3 grid via an image model (always `openai-gpt-image-2` for multi-panel composites) and re-attach it with `vclaw video filmmaking-prompts --storyboard-grid <path>`, which feeds it into the Seedance/Veo/Runway prompt packets.

Two production-learned gotchas baked into the generated packets (see the framework Anti-patterns): (a) grids passed as provider `reference_images` get **reproduced as a moving 9-panel split-screen** unless the prompt explicitly forces single-full-frame output — the packets now embed that guard; (b) real-person content filters (xskill/ARK Seedance) reject photoreal faces as `reference_images`, so use `filmmaking-prompts --no-faces` to render the grid prompt in a silhouette / no-face register.

`vclaw video prompt-lint` (`src/video/prompt-lint.ts`, pure/deterministic — the handler does the I/O) lints a filmmaking-prompts artifact BEFORE spend: Seedance 13-block order, the 280–1000-words-per-packet window, brand/proper-name scrub leaks, the single-full-frame grid guard whenever a storyboard-grid reference is attached, NO ON-SCREEN TEXT / CAPTURE CADENCE / SUBJECT LOCK / CAPTURE REALISM / CAMERA CAPTURE presence on video packets, grid-panel annotation style (advisory), the character-identity word budget (30–60 words target, >100 hard failure), and two newer advisories: the reference-transfer contract and allocation-model over-allocation. `--checklist` prints the route checklist. `vclaw video cinema-profile` persists per-project cinematography defaults onto the project manifest (`updateProjectManifestCinemaProfile` in `workspace.ts`) so the prompt/packet layer picks them up without re-typing flags. `vclaw video diagnose` (`src/video/output-diagnosis.ts`) is the companion for AFTER a render: an output-quality troubleshoot tree — `--symptom <text>` matches an observed defect to causes + fixes, `--retry-pattern` prints the conservative retry pattern.

### Provider routes and adapters

Live execution calls route-specific adapters. A custom adapter is set via one of:

```
VCLAW_VEO_USEAPI_ADAPTER
VCLAW_SEEDANCE_DIRECT_ADAPTER
VCLAW_RUNWAY_USEAPI_ADAPTER
VCLAW_DREAMINA_USEAPI_ADAPTER
```

Adapters receive JSON on stdin and must return JSON on stdout (`externalJobId` for submit, `pending|completed|failed` for poll).

**One declaration per route.** What a route NEEDS — required env vars, runtime dependencies, maturity, its `..._ADAPTER` and `..._SUBMIT_CMD` env var names, and declared membership of the specialist lanes (`batch-queue.ts`'s batch routes, `execution-runtime.ts`'s `VOICE_REF_ROUTES`) — lives once in `src/video/provider-platform/route-prerequisites.ts`; `provider-status.ts` reads it instead of keeping five parallel tables, and the lane consumers derive their subsets from the declared flags. `src/video/capability-contract.ts` composes it with `ROUTE_CAPABILITIES`, the Cinema route registry, the audio backend registries and `FINISH_BACKEND_IDS` into one pure `CapabilityManifest`. Adding a route means editing the id tuple plus `route-prerequisites.ts` — `src/tests/capability-contract.test.ts` FAILS when any consumer, the MCP `list_provider_routes` description, or a fenced table in `docs/CAPABILITIES.md` / `docs/PROVIDER_PLATFORM.md` drifts from it. Do not merge the core and Cinema route id spaces (ADR 0001).

For every route, `vclaw` ships a built-in adapter binary (`dist/cli/provider-adapter.js`) used automatically unless the full `..._ADAPTER` override is set. The built-in adapters read route-specific `..._SUBMIT_CMD` / `..._POLL_CMD` / `..._CANCEL_CMD` command shims. Every route also has a native in-process transport, and with no override set a route falls through to it (`resolveActiveTransport` order: `..._ADAPTER` → the free in-tree engine on `seedance-direct` → `..._SUBMIT_CMD` → blocked → native): `native-seedance.ts` (`SUTUI_API_KEY`), `native-veo.ts` (drives the local `vclaw-cli` Bun package), `native-runway.ts` and `native-dreamina.ts` (pure Node fetch + fs, UseAPI bearer auth), `native-magnific.ts` (`MAGNIFIC_API_KEY`), `native-modelark.ts` (`ARK_API_KEY`) and `native-reapi.ts` (treg or direct). Two further `native-*.ts` modules serve commands rather than routes: `native-flow-r2v.ts` and `native-runway-upscale.ts`.

**`reapi-seedance` = Seedance 2.5 "Less Restriction" via reAPI (2026-09-21).** `content_filter:false` accepts a photograph of a real person as the subject reference AND a voice clip as the speech reference — the one API-keyed route with both (the free Higgsfield engine behind seedance-direct also takes a face plus a voice, through a browser session, at $0). Two credential paths chosen by `VCLAW_REAPI_SEEDANCE_VIA` and never inferred (two credentials are two bills): `treg` relays `reapi.video-gen.seedance-2-5.unrestricted` on `TREG_TOKEN` and hosts references on treg's `POST /media`; `direct` calls `https://reapi.ai/api/v1` on `REAPI_API_KEY` and hosts references on Go Bananas R2. An unset selector is the `blocked` transport (`execution_blocked_by_readiness`). `RoutePrerequisites.credentialAlternatives` + `requiredEnvVarsFor` is the declaration shape for such a route. `src/video/providers/reapi-seedance.ts` (pure body/endpoints/envelope), `providers/treg-client.ts` (headers, media host, 402/503 decoding) and `native-reapi.ts` (the transport: hosts local references IMMEDIATELY before the HTTP call — after the cinema quote and the run-contract approval hash the bytes — with a content-hash cache under `<outputDir>/.vclaw-hosted/`; keyframe → `image_with_roles` first/last frame + `size:adaptive`, character/plain refs → `image_urls`, audio → `audio_urls`, video → `video_urls` (BILLED on top of the output, so a bound OR `@`-tagged voice clone contributes `sourceAudio`, not its mp4 — `voiceTagLookupForAudioRoute` swaps the tag lookup's references, and a tagged voice stays out of the identity winner set so it cannot demote a chain keyframe); a chain link reuses the `scene-N-last-frame.png` the poll already saved beside the clip when it is not older than it (`savedLastFrameFor`), else ffmpeg extracts one; whole seconds 4–30 gated in `assertRouteRequestValid`, `-1` never sent; poll downloads the clip + last frame and states `actualCost` from `usage.credits`; no cancel → `execute-abandon`). Paid per second (treg-observed 2026-09-14: 480p ≈ $0.12/s, 720p ≈ $0.27/s, 1080p ≈ $0.46/s, 4 s minimum); `VCLAW_REAPI_SEEDANCE_RESOLUTION=480p` is the probe tier and, with the selector, part of the approval fingerprint. Appended LAST to `providerOrder`; never a default, never a fallback rung. CERTIFIED on both credential paths (treg 2026-09-21, direct 2026-09-22: each a 480p 4 s job billed 475 credits = $0.475, charge == wallet delta — the face-accepting row costs the same on both, so the paths differ only in whose account pays; `scripts/live-acceptance.mjs` reads the treg team balance or reAPI's own `GET /balance`). A create's INTENT is on file before the POST, and a lost answer is TERMINAL here — reAPI publishes no task-LIST endpoint, so the scene stays `submit-unknown`, the job is returned (never thrown) and stays pending with `actualCost` withheld, every poll repeats that the task may exist and may be billing (never "nothing billed"), nothing is re-submitted, and `execute-abandon` is the only exit (after which no `actualCost` is stated at all). A refusal throws only when NOTHING is in flight yet — on this route and on `seedance-modelark` alike a 4xx after an earlier scene is billing returns the job with the refusal named, because a throw would strand the paid task.

**`seedance-modelark` = the official Seedance 2.5 API on BytePlus ModelArk (2026-09-21, #603–#636).** Paid per second of OUTPUT to your BytePlus account on `ARK_API_KEY` — a different host, key, body shape and biller from `seedance-direct`, hence its own route id (ADR 0007). `VCLAW_MODELARK_MODEL` picks 2.5 (default, 30 image / 10 video / 10 audio references, 4–30 s) or the cheaper 2.0 fast/mini (9/3/3, 4–15 s, no audio-only input). The transport (`native-modelark.ts`, client `providers/modelark.ts`) plans every scene BEFORE the first paid create: a lone keyframe is a strict `first_frame`, anything else is an omni request whose references the prompt names `@Image 1` / `@Video 1` / `@Audio 1`; duration must be a whole number in range (the vendor's `-1` is never sent). Reference videos are held to a shape gate before upload (`providers/modelark-references.ts`): 300–6000 px a side, aspect 0.4–2.5, **23.9–60 fps** — the vendor's pages say 24–60, but 23.976 was measured accepted 2026-09-23, and a 1920×1080 omni reference was too (`docs/audits/2026-09-23-live-acceptance.md`); an EXTENSION at 1080p is a different task type and stays refused. A create is one POST, never retried, and its INTENT is on file before it: a lost answer leaves the scene `submit-unknown`, the job is RETURNED (never thrown) so the run keeps its job id, and `execute-status` looks the task up in ModelArk's own list by model and creation window. When that cannot decide — several tasks in the window, a window it could not search, no intent time — **`vclaw video execute-bind --project <slug> --task <id> --confirm-bind`** is the exit: read the id off the ModelArk console, and it is bound only if it exists, names this job's model, is unowned, and the scene's `scene-N.mp4` is not already another run's clip. It makes no submission. `execute-cancel` DELETEs a QUEUED task only — a running one cannot be stopped and is still billing, so `execute-abandon` is what ends the wait. The job-state file is checked on every read AND before every write (`modelArkJobStateProblems`), narrowly: a refused file strands a paid task, so the model and resolution need only be text. In the batch lane; NOT a Cinema route (no honest quote adapter is possible — no BytePlus credential exposes a balance).

**`dreamina-useapi` = Seedance 2.0 / Dreamina via useapi.net.** Dreamina (CapCut/ByteDance Seed) exposes the Seedance family (`seedance-2.0` default, `-2.0-fast`, `-2.0-mini`, `-1.5-pro`, `-1.0-pro`, `-1.0-mini`, `-1.0-fast`) and `sora2` through useapi.net; CA-region accounts unlock 1080p Seedance 2.0 (and 4k on `seedance-2.0` as of 2026-06-26 — opted into route-locally via `VCLAW_DREAMINA_RESOLUTION=4k`, since the shared execution profile only expresses 720p/1080p; the transport clamps 4k→1080p on non-seedance-2.0 models and →720p on the 720p-only ones). `-2.0-fast`/`-2.0-mini`/`sora2` are 720p-only. `native-dreamina.ts` reuses the **same `USEAPI_API_TOKEN`** as runway-useapi (no new token) and reads the account from `VCLAW_DREAMINA_ACCOUNT` (e.g. `CA:ai@example.com`), region from `VCLAW_DREAMINA_REGION` (default `CA`), and model from `VCLAW_DREAMINA_MODEL` (default `seedance-2.0`); the route adapter override is `VCLAW_DREAMINA_USEAPI_ADAPTER`. The account is registered server-side out-of-band (`POST /accounts` with `{email,password,region,maxJobs}`), so submit only needs the account id + token. For image-to-video, the first image reference is uploaded via `POST /dreamina/assets/<account>` to obtain an `assetRef`, which is passed as `firstFrameRef` on `POST /dreamina/videos` (first_frame mode auto-detects aspect ratio from the image); a scene's `endKeyframePath` (the same field the runway route uses) uploads a second frame passed as `endFrameRef` alongside `firstFrameRef` → **end_frame mode**, where the clip animates first→end (two stills → one continuous shot); poll uses `GET /dreamina/videos/<jobid>` (`status: created → completed|failed`) and downloads `response.videoUrl`. Like runway-useapi, real human faces are rejected by Seedance moderation — describe stylized characters by visual descriptor.

**Seedance character consistency = the Asset Library, not raw URLs.** `ark/seedance-2.0` (the official Volcengine Ark Seedance 2.0, Standard = 1080p) locks character identity via managed **Asset Library avatars** (`Asset://` URIs), NOT raw photoreal image URLs (those trip the "real person" content filter and don't lock identity). `src/video/seedance-asset-library.ts` (`vclaw video seedance-register-assets`) registers character images as Assets, waits for international-profile sync, and writes `artifacts/seedance-assets.json` (canonical schema `schemas/video/artifacts/seedance-assets.schema.json`; shape `{ schemaVersion, projectSlug, groupName, generatedAt, assets:[{name, assetId, assetUri, intlAssetUri}] }`); `readSeedanceAssets(workspaceRoot, slug)` reads it back into a name→`Asset://`-URI map (graceful when absent). On the `seedance-direct` route only, `buildExecutionPayload` (`execution-runtime.ts`) auto-resolves each scene's `referencePaths` from that artifact by matching `scene.characters` names → their `Asset://` URIs (a project without the artifact behaves as before); `native-seedance.ts`'s `seedanceReferenceParams` then routes `Asset://` references into `reference_images`. References are capped at ≤9 image / ≤3 video / ≤3 audio per submission via `assertReferenceBudget`, preflighted across the whole payload in `submitSeedanceDirectNative` before any submit (fail-fast, no partial submission). Describe characters by visual descriptor (not proper names) in prompts — names don't survive across generations. (Validated 2026-05-29 against the same Ark endpoint the user's production project uses.) `@Name` tags in scene prompts (`resolveAssetTags` in `prompt-rules.ts`, lookup via `asset-tag-lookup.ts` `buildAssetTagLookup`) resolve at `buildExecutionPayload` time to the character's visual descriptor (text) + its saved reference (`Asset://`/image), merged through `resolveSceneReferencePaths`; unresolved tags strip the `@` and warn (never fatal), `@imageN` positional bindings are reserved, and it runs before `stripProperNames`. `@location` tags resolve via locked environment plates: `vclaw video environment-auto-create` (`src/video/environment-auto-create.ts`, mirrors `character-auto-create`) generates seamless no-people location plates and writes `artifacts/environment-assets.json` (own schema, like seedance-assets); `readEnvironmentAssets` (`environment-assets.ts`) feeds `buildAssetTagLookup`'s `environmentsByName` so `@tokyo-alley` resolves to the plate descriptor + ref (absent → graceful no-op).

**Flow identity is registered, not referenced.** The Flow-side counterpart of `seedance-register-assets`: `vclaw video flow-register-characters` / `flow-register-voices` (`src/video/flow-character-library.ts`, handlers in `provider-registration.ts`) register reusable Google Flow Characters/Voices via useapi and write `artifacts/flow-characters.json` / `flow-voices.json` (needs `USEAPI_API_TOKEN` + `USEAPI_ACCOUNT_EMAIL`). `vclaw video flow-r2v` (`src/video/native-flow-r2v.ts`) then submits reference-to-video against those registrations, running `applyR2vPromptHygiene` over the prompt first. This is what working-discipline rule 3 means by "locks identity on a registered Flow Character".

### Cartoon-show layer (show-bible → show-preflight → voice clones)

The repeatable cartoon-SHOW production system (from the Jack-Vs-AI workflow). `vclaw video show-bible` (`src/cli/handlers/show.ts`) is the show's asset-library index: it ties the project's characters + locations (environment plates) + voice clones into one reusable world and tracks the episode list — plan/derive by default, deterministic, no provider calls. `vclaw video show-preflight` (`src/video/show-preflight.ts`, `buildShowPreflight`) is the fail-fast, **route-aware** readiness gate that working-discipline rule 4 points at: given the show-bible + storyboard, it verifies every cast/speaking subject in every scene has what the CHOSEN route actually needs — Seedance-family routes (`seedance-direct`/`runway-useapi`/`dreamina-useapi`) require a resolvable character sheet per cast member (on `seedance-direct` it must additionally be a registered Asset Library avatar — a raw portrait trips the real-person filter), a location plate per scene, and a bound, resolvable voice clip per speaking character, with subjects described by full visual descriptor (never a bare generic noun); Flow (`veo-useapi`) requires a registered Flow Character per cast member (registration is out-of-band via `flow-characters.json`, so the gate enforces — it never fabricates). `vclaw video voice-clone` (`src/video/voice-clone.ts`) implements the production-learned voice lock: a raw MP3/WAV voice reference DRIFTS to a generic accent, but the same audio as the track of a **black-frame video** locks it — the command builds that black-frame+audio MP4 (shared `runFfmpeg`) and persists reusable voice-clone assets in `artifacts/voice-clones.json` (mirroring the seedance-assets/environment-assets pattern), bindable to characters.

### Overnight batch queue

`src/video/batch-queue.ts` + the `vclaw video batch-submit` / `batch-monitor` / `batch-status` commands queue many independent video jobs to run unattended overnight. The default route is the **free** `runway-useapi` explore mode (low-res, slow — backfill drafts); target a paid route (`seedance-direct`, `seedance-modelark`, `reapi-seedance` or `dreamina-useapi`) for hi-res. An operator-authored manifest (`schemas/video/artifacts/batch-queue-manifest.schema.json` — an input-only artifact, allowlisted in `check-artifact-schema-coverage.mjs`) compiles via the pure `buildBatchPayload()` into a single `VideoExecutionPayload` with N tasks, so it reuses the existing native route transports (`native-runway`/`native-dreamina`/`native-seedance`/`native-reapi`/`native-modelark`, the five `BATCH_ROUTE_IDS`) and their job-state — no duplicate submit/poll logic. Audio follows what the route renders under `produce` unless the manifest sets `defaults.generateAudio` (#617/#632: the lane used to hardcode silence, so one prompt came back silent here and with speech through `produce`); a per-job or top-level `generateAudio` is refused, as is `defaults.generateAudio` on `dreamina-useapi`, which has no audio switch. **`batch-submit` no longer submits or persists `batch-queue.json`**: it compiles the manifest into the durable Cinema queue and returns a compatibility receipt (`handlers/batch.ts`; `--execute` throws). `batch-queue.json` is written only by the historical `batch-monitor`, itself deprecated in favour of `cinema-sync`; it polls once (the transport downloads to `<dir>/scene-<i>.mp4`), copies each completed scene to `<dir>/clips/<jobId>.mp4`, and writes `<dir>/batch-status.json`. It is **resumable/idempotent**: re-running only advances pending→done/failed and never re-downloads completed clips, so `batch-monitor --out <dir> --once` is safe to schedule via launchd (one pass per tick). Opt-in wedge handling: `--stall-minutes <n>` (0=off) flags scenes the provider has left `submitted` past the stall window as **wedged** (`detectWedgedScenes`/`applyWedgeHandling` in `batch-queue.ts`), and `--fail-wedged` marks them `failed` so the queue reaches terminal and the monitor exits instead of polling a stuck job to the deadline. The monitor also backs off automatically when the explore queue is throttled (`isExploreThrottled`/`nextBackoffMs`). **`--auto-resubmit` / `--max-resubmits` are retired and now throw**: they created replacement jobs outside the canonical durable queue. See `docs/CLI_REFERENCE.md` ("Overnight batch video queue").

### Cinema production queue + render lanes (the durable execution layer)

ADR 0008 converges every generated-work front door (`produce --auto-chain`, `pool`, `batch-submit`, `mograph-render --enqueue` (its `--emit-batch` still writes the legacy batch manifest), fallback rendering) onto ONE durable, dependency-aware production queue owned by the `src/video/cinema-*` store family (~60 small modules: `cinema-production-queue.ts`, `cinema-production-contract-*`, `cinema-provider-*`, `cinema-evidence*`, `cinema-review-*`, `cinema-preflight*`, `cinema-retry-policy.ts`, `cinema-file-lock.ts`, …). The queued path is `compile → exact quote → authorise → cinema-work → reconcile → review → delivery`, driven by the `vclaw video cinema-*` verbs (handlers `cinema*.ts`: `cinema-create`/`-compile`/`-preflight`/`-quote`/`-authorize`/`-work`/`-work-quote`/`-ingest`/`-review`/`-promote`/`-deliver`/`-status`/`-console`/`-console-live`/`-archive`/`-restore`/`-migrate`/`-history-import`). Workers act on SAVED state (the quote, the immutable request, and the provider job id must survive interruption); an ambiguous submission is resolved before any replacement work is authorised, never re-submitted blindly. ADR 0009 makes the Cinema console (`cinema-console` / `cinema-console-live`, `cinema-project-console*.ts`, `cinema-live-console.ts`) the one authoritative live review surface that review-ui, the preview portal and monitor detail pages converge on. Cinema route ids are a separate id space from the core `ProviderRouteId`s (ADR 0001 + 0007: unlimited Higgsfield, paid xskill API and the official Higgsfield CLI — `cinema-higgsfield-cli-*.ts` — are distinct routes and never silently swap).

**Render lanes** (`src/video/lane-queue.ts`, `lane-coordinator.ts`; `vclaw video lane <acquire|await|heartbeat|release|status>` in `handlers/lane.ts`) are the cross-process slot leases every driver — in any language — must take before submitting. The lane key is `route:account` (provider limits are per ACCOUNT), one lane per transport so different engines run in parallel. The local queue is the external `sqlite3` CLI (not `node:sqlite`); the shared multi-computer authority is a Cloudflare Durable Object per lane deployed from `services/shared-lane-coordinator/` (`docs/SHARED_QUEUE.md`). It is a lease, not a task queue: the agent still runs its own render, it only waits its turn. Two drivers on one lane look exactly like moderation failures and have the opposite fix (serialise vs redraw) — check `lane status` before diagnosing rejections.

### Free Seedance engine, vendored in-tree (`engines/seedance-direct/`)

ADR 0006 (supersedes 0005): the free, unlimited Higgsfield Seedance engine is a ~600-line Python adapter (`adapter.py`, `run.sh`, `bootstrap.sh`, `test_adapter.py`) living OUTSIDE `src/` — it does not touch `tsc`, `node:test` or the npm tarball. It speaks the same stdin/stdout SUBMIT/POLL/CANCEL JSON contract as every `..._ADAPTER`. `resolveAdapterCommand` prefers it for `seedance-direct` only when the engine is fully bootstrapped; the readiness verdict is `describeFreeInTreeSeedanceEngine` in `src/video/execution-adapter.ts`, which requires the `.vclaw-engine-ready.json` marker (`FREE_SEEDANCE_ENGINE_MARKER`) that ONLY a successful signed-in cookie import writes inside the browser-session directory — an existing venv + an empty profile directory is NOT ready and the route refuses to run unless `VCLAW_SEEDANCE_DIRECT_NATIVE=1` opts into the paid transport, per ADR 0007 + ADR 0001 (the refusal is `execution_blocked_by_readiness` carrying `details.activeTransport === 'blocked'`, and `resolveAdapterCommand` derives it from `resolveActiveTransport` so, for the same environment, the refusal and the report cannot drift; `providers` merges the workspace `.env.local` and a render reads only the shell environment, a gap the report now names rather than papering over). Poll, cancel and lookup are deliberately NOT gated — they cannot spend, and refusing them would strand a job someone already paid for. The venv and the `.cloak-profile` browser session are local, secret and git-ignored; `engines/seedance-direct/bootstrap.sh` creates them. Before any `--confirm-spend` on `seedance-direct`, read `activeTransport` in `vclaw video providers` output — `in-tree-engine` is free, `blocked` renders nothing, anything else is paid. `VCLAW_SEEDANCE_DIRECT_NATIVE=1` selects the paid path (the truthy spellings `1`/`true`/`yes`/`on`; `0`/`false`/`no`/`off` read as unset).

### Smaller newer families (each self-contained under `src/video/`)

- `creator-ui/` (`vclaw video creator-ui`, `creator-demo`; `docs/CREATOR_DEMO.md`) — the zero-key product-shell: `capabilities.ts` probes which stock/TTS/music/generative routes this machine can actually use, `read-model.ts` + `server.ts` serve a local creator surface, and `creator-demo` (also `vclaw studio --goal creator-demo`, `npm run demo`) builds a full project + preview with `providerCalls: 0`, `networkCalls: 0`, never marking it publish-ready.
- `stock-platform/` (`vclaw video stock-search` / `stock-import`) — stock footage providers (`providers/pexels.ts`) behind a registry; `rights.ts` records licence/provenance (sha256 + source) per imported asset and `store.ts` keeps imports inside the project.
- `publish-platform/` (`vclaw video publish-metadata` / `publish-package`) — versioned, source-cited platform profiles (`profiles.ts`: YouTube Shorts etc. with max duration, aspect, codec, title/description limits, synthetic-media disclosure) and `validate.ts` checks a final cut + metadata against the chosen profile before packaging. Distinct from `publish-preview` / `publish-portal-index` (the preview portal) and from `docs/PUBLISHING.md` (releasing the npm package itself).
- `reframe/` — the shot-plan engine behind `make-vertical`: `--write-plan-template` detects cuts and writes UNVERIFIED subject-anchor suggestions; `--strict` refuses to render until every anchor is verified (or `--quick-center-crop` is explicit); `captions.ts` renders portrait-native captions and `qc.ts` audits the crop.
- `run-state/` — `readProjectRunState` normalises candidates + storyboard + execution report into one derived run view (stale `pending` candidates are treated as abandoned); the read model that monitor/console surfaces share.
- `magnific/` + `providers/` — the Magnific REST route (`magnific-rest`, one of the seven built-in adapter routes) and the thin useapi/xskill/Flow transport clients the native routes call.

### Director Blueprint (director layer)

`src/video/project-blueprint.ts` + `blueprint-prompt.ts` + `director-defaults.ts` (`vclaw video director-blueprint --project <slug> (--from-json <path> [--write] | --show)`) persist a project-level **visual bible** (`artifacts/project-blueprint.json`): color system, lighting grammar, per-character camera language, forbidden moves, "the one rule". It is distinct from the story bible (continuity); the blueprint locks **visual direction** above the execution layer. Authoring is creative and lives in the `ai-director` skill (`skills/ai-director/SKILL.md`); the CLI half is deterministic (validate/normalize/persist). `filmmaking-prompts` auto-appends a prose DIRECTOR block and flags forbidden-move violations when a blueprint exists; no blueprint → byte-identical legacy output. See `docs/DIRECTOR_BLUEPRINT.md`.

### Motion overlay (`src/video/motion-overlay/`)

`vclaw video motion-overlay --input <video>` turns an existing talking-head video into a reel with speech-synced motion-graphics overlays. Pipeline: ingest → transcribe (Gemini STT) → slice into ≤10s takes → compose overlay prompts (retention principles + concept→animation metaphor map live as deterministic code, not markdown) → render via one of four layouts (`split|overlay|motion-only|avatar-host`). Render transports: Omni Flash V2V (moderation-prone on person footage), **local** per-frame render (`render-local.ts`/`animate*.ts` — word reveal, count-up, gauge fill), and `avatar-host` (locked go-bananas character via `--gb-character <Name:ID>`, pin+chain identity). Plan/dry by default; spend requires `--execute --confirm-spend`; retry/resume self-heals probabilistic Flow moderation. See `docs/MOTION_OVERLAY.md`.

### Mograph — style-locked motion graphics (`src/video/mograph/`)

`vclaw video mograph-sheet` / `mograph-pack` / `mograph-render` / `mograph-logos` generate fleets of motion-graphics clips that share ONE look: a **motion sheet** (`artifacts/motion-sheet.json` — master style-board image + ≤120-word style lock carrying the do-NOT-copy-the-sheet-layout guard + a negative ending in the five audio bans) locks the style once, and a **motion pack** (`artifacts/motion-pack.json` — coverage map + action-only `B###` blocks, P1/P2/P3, `t2v|ref2v|v2v-*` modes) carries pure choreography. `mograph-pack --check` is the anti-drift gate (style words/hex codes banned from actions, quoted on-screen text ≤4 words, SFX-never-music, coverage integrity); `mograph-render` is PLAN-ONLY — it assembles `style lock + SHOT + AVOID` per block (clips render silent; a block's SFX cues ride in its sidecar for the post mix) and `--emit-batch` compiles a batch-queue manifest for the existing `batch-submit`/`batch-monitor` machinery (refs ride in `characterRefs`, the REFERENCE slot; v2v blocks are set aside for omni-flash or prompt-only, never dropped silently). Creative authoring lives in `skills/mograph/SKILL.md`; contract in `docs/MOGRAPH.md`; design spec in `docs/design/specs/2026-07-13-mograph-native-port.md`. Native port of a licensed external skill's concepts — original wording only, the purchased pack is never committed.

### Audio platform (`src/video/audio-platform/`)

`narrate`, `dialogue`, `sfx`, and `soundtrack` are backend-pluggable audio commands over a backend registry (`registry.ts`): TTS via `gemini-tts`/`elevenlabs-tts`, SFX via ElevenLabs, music via `lyria` (Vertex), `lyria3` (Gemini API, key-based), `flowmusic` (Lyria 3 Pro vocals via useapi.net), and Suno. All spend-gated: without `--confirm-spend` they exit 3 with `spend_confirmation_required` (`--dry-run` to plan). `assemble` consumes their outputs via the mix-plan (dialogue + SFX mixed at stitch; `assemble/narration-fit.ts` handles TTS-vs-video timing).

### Media production & local post-production

Two handler families cover the finishing lane. `handlers/media-production.ts`: `assemble` (FFmpeg stitch, incl. `--from-clips`), `gen-image` (diegetic prop/screen/overlay stills; Flow image models via `gen-image-flow.ts`), `overlay` (graphic/alert/lower-third motion graphics with a drawtext preflight), `music-video` (vocal-synced beat-exact assembler, plan/dry by default), `title-card` (Pillow+RAQM title overlays), `stitch-ad`, `animation-styles` (shared `animation-styles.json` registry), `finish` (`src/video/finish.ts` — upscale a rendered cut to an HD/QHD master via hosted Topaz (spend-gated) or local Real-ESRGAN/Topaz CLI; the anti-plastic recipe is baked in: photoreal model, denoise/sharpen DISABLED, film grain kept, and Topaz `grain` clamps to 0.1 because the published schema overstates the range), and `lipsync` (OmniHuman, spend-gated). `assemble`'s stitch layer also supports opt-in **reading-holds** (`assemble/stitch.ts`, `readingHold`) — since Phase 3c the stitch constants/filter register, the concat/prep argv builders and the multi-layer audio mix live in `assemble/stitch-filters.ts` / `stitch-concat.ts` / `stitch-audio-mix.ts` and are re-exported from `stitch.ts`, so import from `stitch.js` as before — readable pre-roll holds on dense motion-comic segments. `handlers/media-ops.ts` is local-FFmpeg-only post-production on a finished cut — `make-vertical`/`make-square`/`make-loop`, `thumbnail`, `burn-subtitles`, `verify-final`, and `qc` (recursively scans `projects/<slug>/final/` + `outputs/` for clips and runs the assemble media-QC probe: missing-audio, nonstandard codec, duration drift; graceful `status: 'skipped'` when no clips) — real file I/O but NO provider calls and NO spend. Two adjacent quality/finishing tools: `vclaw video image-ops` (`src/video/image-ops.ts` + `native-magnific.ts`) is still-image post-processing, currently the Magnific precision-v2 image upscaler (pure planning core + injectable runner; `--dry-run` plans without spending; the IMAGE endpoint is live-verified, a different service from the video upscaler), and `vclaw video consistency-audit` (`src/video/consistency-audit.ts`) is an automated character-consistency VISION audit across rendered scenes/keyframes — the engine-side forcing function that catches wardrobe/face drift on recurring characters BEFORE a render is presented as done.

### Gemini key pool

`src/video/gemini-key-pool.ts` provides round-robin selection with per-key cooldown across `GEMINI_API_KEYS`, `GOOGLE_API_KEYS`, `GOOGLE_API_KEY`. `analyze-template --auto` and `analyze --auto` use it via `src/video/gemini-analyze.ts`. `VCLAW_GEMINI_API_ENDPOINT` overrides the endpoint.

### Compatibility aliases (preserve on changes)

- `execution-plan` ↔ `plan`
- `execute` ↔ `produce`

These two are supported spellings: the product itself emits `vclaw video execute` (`create.ts`, `execute.ts`, `storyboard-markdown.ts`, the studio recipes), so they never print a notice.

### Declared aliases on their way out (notice-only, dispatch preserved until removal)

`template-create` → `template-save`, `clone-ad` → `clone-execute`, `analyze-template` → `analyze`, `preflight` → `director-preflight`, `library find` → `find-library` — all marked `deprecatedAliases` since 3.0.0-alpha.13, as are the one-shot and historical-only commands listed in `docs/DEPRECATION.md` "Marked as of 3.0.0-alpha.13". Each alias is an `aliases` entry on the canonical `CommandSpec` in `src/video/cli-schema.ts` (one schema entry, one handler); marking one deprecated is `deprecatedAliases: [{ name, since }]` on that entry, which makes `main` print one stderr line and run the command unchanged. Lifecycle: `docs/DEPRECATION.md` "Command lifecycle". `src/tests/cli-dispatch-table.test.ts` fails on a dispatch key that shares a handler but is declared nowhere. The command `approve` is marked `deprecated` (not an alias: its own dispatch key) as a spelling of `produce --approve`; its handler forwards to the produce handler.

(The old `omx` wrapper binary has been removed entirely — don't reintroduce it.)

## Conventions that are not obvious

- TypeScript is `strict` with **NodeNext** ESM. Relative imports in `src/` must include the emitted `.js` extension (e.g. `'../video/projects.js'`) — required by NodeNext ESM resolution. Don't "fix" these to drop the extension.
- `dist/` is generated — never edit it, never commit it. Edit `src/` and rebuild.
- Filenames: `kebab-case.ts`. Identifiers: `camelCase` for functions/variables, `PascalCase` for types.
- 2-space indent; modules stay small and single-purpose.
- CLI output is machine-readable JSON by default; do not add silent fallbacks across provider routes.
- Tests use `node:test` with `assert/strict`. Prefer `mkdtemp`/`tmpdir` for temp-directory isolation. Put CLI end-to-end tests under `src/tests/cli-*.test.ts` and module-contract tests under `src/tests/*.test.ts`.
- When adding a new CLI subcommand: add the handler in the appropriate `src/cli/handlers/*.ts` module (or a new one following the verbatim-move pattern) and register the dispatch-table entry (plus any alias) in `src/cli/vclaw.ts`; update the relevant `src/video/*` module(s), a schema under `schemas/video/` if it introduces or changes an artifact, register the command in the `COMMANDS` array of `src/video/cli-schema.ts` (bump the hardcoded command-count assertion in `cli-schema.test.ts` to match), add a `cli-*.test.ts`, and update `README.md` + `docs/CLI_REFERENCE.md`. The `check:cleanroom-docs` guardrail watches docs drift.
- Project slugs are validated by `isProjectSlug` (`src/video/projects.ts`); both `parseProjectSlug` and `handleVideoInit` (`validateInitSlug`) enforce it so flag-looking values (e.g. `--project`) cannot be silently accepted as slugs. Preserve this guard when adding new slug-accepting commands.
- Architecture diagrams under `docs/assets/*.jpg` are generated from the Mermaid sources in `docs/DIAGRAMS_SOURCE.md`. Edit the Mermaid block there, then `npm run diagrams:mirror && npm run diagrams:render` (mermaid-cli with the brand config — the deterministic render the existing JPGs came from; `npm run diagrams:lint` fails when a mirror drifts). Never hand-edit the JPGs.
- `check:skill-frontdoor` deliberately ignores `skills/seedance-prompts/SKILL.md` and the three presenter skills (`bunty`, `davendra-presenter`, `nex-presenter`) because their docs legitimately reference the legacy Python pipeline scripts. Don't "fix" the ignore list — it's load-bearing.
- Do not commit secrets, `.env.local`, provider cookies, `engines/*/.cloak-profile/`, or `.omx/` state (already gitignored).
- `.cursor/rules/codegraph.mdc` (alwaysApply) is the CodeGraph MCP usage guide: prefer `codegraph_context` / `codegraph_trace` / `codegraph_impact` for structural questions (callers, callees, blast radius) and grep only for literal text. Skills that touch many modules (the cinema store, provider routes) are where this pays off most.

## Start in your own worktree

Sessions share this repo's `.git`. Two in the MAIN checkout also share its
working tree, index, HEAD and `dist/` — which on 2026-08-01 absorbed one
session's uncommitted work into another's commit, wiped `dist/` mid-test-run
(322 phantom failures), and switched the branch under an in-flight commit. A
worktree gives a session its own copy of all four.

Run `scripts/new-session.sh <name> [branch]` before doing anything else. It
builds `.claude/worktrees/<name>` off a freshly fetched `origin/main`, links
`node_modules`, proves the link resolves, and prints the `cd`. Stage explicit
paths when committing, so a commit holds only what you touched.

Working in the main checkout is fine when you are certain you are the only
session. When you are not certain, you are not.

Since 2026-09-18 the build no longer deletes `dist/` while it compiles: it stages
and swaps (`scripts/build-atomic.mjs`), so a second build cannot produce the
"322 phantom failures" above, and a render driver's `vclaw video lane` calls keep
working during a rebuild. The swap makes that failure QUIET instead — later test
files would load the other build — so `scripts/run-tests.mjs` compares
`dist/.build-id` before and after and FAILS the run when it moved ("dist/ was
rebuilt under this run"). A reviewer agent that builds counts as a second
builder: give it its own worktree, or run the suite first. A worktree is still
the right answer; this only turns a confusing failure into a named one.

`scripts/hooks/install.sh` activates a `pre-commit` guard: it blocks a commit
made directly on `main`, and warns when you commit from the main checkout while
another worktree holds uncommitted work. Both are advisory-on-error and skippable
with `VCLAW_SKIP_GUARD=1` — a guard that wrongly blocks just gets deleted.

It also activates `post-merge` / `post-rewrite`: a `git pull` (merge or rebase) into
the MAIN checkout that touches `src/`, `schemas/`, the build scripts or `package*.json`
rebuilds `dist/` (running `npm install` first when dependencies changed), because the
global `vclaw` resolves to this checkout's `dist/`. Worktrees are skipped; a failed build
never fails the pull and names `.git-autobuild.log`. Skip once with `VCLAW_SKIP_AUTOBUILD=1`.

## Autonomy directive (from AGENTS.md)

Proceed by default on obvious next steps. Keep work scoped to this repository and its generated `projects/<slug>/` folders. If a blocker is local and solvable, solve it; if it's external, note it and continue with the next meaningful lane rather than pausing for confirmation.

## Recommended reading order

`docs/AGENT_QUICKSTART.md` (install → first free render; `vclaw schema --json` + `skills/catalog.json` are the live command/skill indexes) → `docs/ARCHITECTURE.md` → `docs/PROJECT_LAYOUT.md` → `docs/CLI_REFERENCE.md` → `docs/STUDIO.md` → `docs/SHARED_QUEUE.md` → `docs/CREATOR_DEMO.md` → `docs/PUBLISHING.md` → `docs/ASSEMBLE.md` → `docs/PRODUCTION_WORKFLOW.md` → `docs/REVIEW_UI_STORYBOARD_WORKFLOW.md` → `docs/preview-portal-audit.md` → `docs/STORY_BIBLE.md` → `docs/DIRECTOR_BLUEPRINT.md` → `docs/MOTION_OVERLAY.md` → `docs/MOGRAPH.md` → `docs/REFERENCE_SHEETS.md` → `docs/SCENE_CANDIDATES.md` → `docs/OPERATIONS.md` → `docs/GENERATION_TELEMETRY.md` → `docs/OBSIDIAN.md` → `docs/TEMPLATES.md` → `docs/MIGRATION.md` → `docs/DEPRECATION.md` → `docs/RELEASE_READINESS.md` → `docs/MASTER_PLAN_ALIGNMENT.md` → `docs/DIAGRAMS_SOURCE.md`.

Architecture decision records live in `docs/adr/` — 0001 no-silent-fallback across routes, 0002 on-disk project as source of truth, 0003 Seedance identity via Asset Library, 0004 director-mode approval gate, 0005→0006 free Higgsfield Seedance engine vendored in-tree, 0007 unlimited / paid-API / official-CLI Higgsfield are separate routes, 0008 one durable production queue, 0009 one HTML5 production review console. Consult them before relitigating those decisions.

## Agent skills

Per-repo configuration for the Matt Pocock engineering skills (`to-issues`, `to-prd`, `triage`, `diagnose`, `tdd`, `improve-codebase-architecture`, `zoom-out`, `qa`). Re-run `/setup-matt-pocock-skills` to change any of these.

### Issue tracker

Issues and PRDs live as GitHub issues on `davendra/videoclaw-v3` via the `gh` CLI. See `docs/agents/issue-tracker.md`.

### Triage labels

Canonical five-role vocabulary (`needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`); labels created on first `triage` use. See `docs/agents/triage-labels.md`.

### Domain docs

Single-context: one `CONTEXT.md` glossary + `docs/adr/` at the repo root. See `docs/agents/domain.md`.

## graphify

This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships.

Rules:
- For codebase questions, first run `graphify query "<question>"` when graphify-out/graph.json exists. Use `graphify path "<A>" "<B>"` for relationships and `graphify explain "<concept>"` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output.
- If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing.
- Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context.
- After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost).
