Cloud agent bc-fd13dd23-…2ccd ran ~22 hours (2026-09-07 → 2026-09-08),
opened PRs #73–#149, and spawned recursive “hunt remaining bugs”
children. That loop is forbidden. Details:
.cursor/rules/no-factory-loop.mdc.
- Finish the user's list in this run. Do not stop after the first item to ask for a merge, rebase, review, or permission to continue.
- Do not ask the user to merge. Keep working on the same branch and the same PR. They merge when they want.
- One request → one branch → one PR unless they explicitly asked for separate PRs. Update that PR each turn; do not open a new PR per file or per YAML key.
- Batch isomorphic holes (typed config keys,
or defaultcoerces, the same fail-closed helper on the next field) into that one PR — or stop and list leftovers. Never one-key-per-PR. - When the list is done, STOP. Do not hunt the next milestone, spawn
“hunt remaining / fail-closed holes” agents, or invent
milestones/<one-config-key>.mdas the next task. - If the next patch is the same shape as the last (new milestone file, same helper, different key): hard stop. Report. Do not open another PR.
Stable goals for this repository:
- Embeddable generator —
pip install docgen(git URL or editable install from this repo), a consumer bundle (docgen.yaml+ hints/narration), and shell/CI are enough to build and maintain narrated demos. Do not vendor this library into a product repo’ssrc/; pin viarequirements-docgen.txt/pipx/uv tool. No IDE assistant is required; optionaldocgen wizardis a local web app only. - Hybrid config and prose —
docgen.yamlshould stay maintainable: deterministic merges (yaml-generate, gap checks) plus optional LLM (OpenAI or Grok) where it adds value (narration hints, declarative scene YAML). Prefer Git-reviewed changes over opaque single-shot generation. - Video stack — Long-form demos pair Markdown narration, TTS (OpenAI or xAI), Whisper-style timestamps, Manim visuals,
compose(ffmpeg),concat, andvalidate(sync and narration lint). The CLI also supportspagesfor static preview sites. - Stable contracts — CLI, exit codes, and reusable workflows should stay predictable for downstream repos and automation.
- Library, not app — There is no in-repo dogfood bundle. Consumer projects (e.g.
course-builder) are the integration test of record. The library must not import or special-case any consumer. - Tool-only generation — Narration, merged
docgen.yaml, compiledscenes.py, TTS audio, composed media, and other generated artifacts must come from docgen (CLI/library) and committed wrapper scripts that call it — not from hand-edited outputs passed off as sources. In a consumer bundle, preferhints/*.md+yaml-generateover ad-hoc YAML surgery. Cursor rules:.cursor/rules/docgen-tools-only.mdc,.cursor/rules/no-asset-edits.mdc.
docgen (OpenAI or Grok) is the only path that produces category C outputs — see .cursor/rules/no-asset-edits.mdc. Summary (paths relative to a consumer bundle, typically docs/demos/):
- Outputs (do not hand-edit):
<bundle>/docgen.yaml(as emitted byyaml-generate);<bundle>/narration/*.md(exceptREADME.md);<bundle>/animations/scenes.py,timing.json,animations/specs/*.scene.yaml(scene pipeline);<bundle>/audio/*.mp3;<bundle>/images/*.png(scene image assets fromimage-generate);<bundle>/recordings/**where applicable. - Inputs (maintainer-owned):
<bundle>/hints/**with YAML front matter (docgen.segment,docgen.wiring); maintainer scripts under the bundle;tests/**fixtures inside this library;<bundle>/narration/README.md.
docgen avoids hardcoding consumer segment ids in library code; tests may use concrete fixtures.
Downstream repos that pin this library should:
- Run
docgen yaml-generate(and review the diff). - Regenerate narration, scenes, audio, and video with the documented CLI sequence for their bundle.
- Run
validate/validate --pre-pushbefore pushing.
Pin bumps (pip install …@<sha>) are routine when adopting a new docgen commit.
docgen is a documentation and narrated demo video toolkit: CLI + library focused on Manim (diagram-heavy segments), TTS, timestamps, composition, validation, pages, and wizard-assisted authoring. It is not the product application and not the CI orchestrator for downstream apps.
The Playwright/VHS/demo-function/per-function/discover-tests/catalog surface area was removed; see the README for what is supported today.
Commands registered on the docgen CLI include:
ai-status— print the resolved AI provider and which env var supplied the key (never the secret).init— scaffold bundle layout anddocgen.yaml.wizard— local web UI for narration/bootstrap workflows (focus files, in-place narration revise, per-segment asset freshness + rebuild-from-here, Vue Benchmark view, Tool tab to pip-upgrade docgen and pinrequirements-docgen.txt).gui— desktop window over the same Vue/Flask UI (pip install 'docgen[gui]'for pywebview).--smokeis a headless HTTP check. PyInstaller spec:packaging/docgen-gui.spec. Frozen apps resolve templates/static/benchmark JSON viadocgen.resources.freeze—docgen freezebuilds thedocgen-guionedir (pip install 'docgen[packaging]'). Optional--smokeruns the binary headless. Do not run a full freeze in routine pytest; setDOCGEN_FREEZE_SMOKE=1for the optional test.tts— text-to-speech for segment files (OpenAI or xAI/v1/tts).timestamps— word/segment timing (timing.json). Default enginelocalaligns the known narration text against the mp3 offline (ffmpeg silencedetect, no API);--engine whisperuses OpenAI whisper-1 or xAI/v1/sttwhenai.provideris grok. Both emit the same Whisper-shaped blocks. Failed ffmpeg silencedetect raisesAlignmentError(empty stderr is not treated as full-span speech). OpenAI whisper-1 word/segmentstart/endmust be finite JSON numbers (bool/NaN raiseAIError). Grok/v1/sttrejects empty word tokens and invertedend < startintervals (AIError). Emptysegments.allraisesTimestampError(same as TTS) and does not leave a staletiming.jsonas success.image-generate— render scene-spec image elements (image:+prompt:boxes) via OpenAI Images or xAI Imagine into the bundle (also runs for missing assets insidegenerate-all).manim— render Manim scenes declared in config.compose— mux narration audio with visual sources via ffmpeg. With no segment ids, usessegments.all(same asgenerate-all), notsegments.default. Atype: mixedrow raisesComposeErrorif any listed source is missing (no silent subset mux).validate/validate --pre-push— drift, narration lint, Manim hints,timing_sync,story_end(last paced reveal vs audio end; hard fail),scene_assets(pre-render: stuck-board cadence, frame-budget overlaps,MANIM_FONTconsistency, stale helpers / stale compiled class including hand-edited generated-region labels andrun_time— hard fail; also agenerate-allgate before Manim),av_sync(hard fail on--pre-push/generate-all; prefers scene-spec labels as OCR anchors),subject_beat_coverage(declarative specs vs narration topic beats; hard fail when enabled), and related visual-sync checks (ocr_scan,layout,freeze_ratio— hard fail on--pre-push/generate-all). Missing tesseract failsocr_scan/av_sync/layout(not skip-PASS). Missing audio or an LFS pointer failstiming_syncand recording media gates (stream_presence,av_drift,ocr_scan,av_sync) (not skip-PASS). Missing*.scene.yamlfailsstory_end/subject_beat_coveragefortype: manim(not skip-PASS).lint— narration lint helper.narration-generate— LLM-assisted narration from hints and repo context; optional--revise --revision-notesfor in-place edits (same contract as the wizard Revise button).scene-spec-generate— LLM emits declarative*.scene.yaml; enforces frame budget + subject-beat coverage (dwell OK; cover topic shifts; reject invented labels).scene-compile— compile specs intoscenes.py(generated regions only).yaml-generate— merge defaults and hint wiring intodocgen.yaml.clean-bundle— remove regenerable outputs per policy.concat— stitch segment videos.pages— emit static HTML for demo assets.generate-all— orchestrated pipeline: TTS → timestamps → scene specs (autoscene-spec-generatewhenanimations/specs/is empty; otherwise offline retime) → images → Manim → compose → validate → concat → pages.--regen-scene-specsforces LLM rewrite;--skip-scene-retimekeeps legacy handscenes.pyonly.rebuild-after-audio— same as generate-all with TTS skipped (still retimes scenes after timestamps).benchmark— score the packaged scene-timing corpus (no bundle / Manim / OpenAI). Executes compiledconstruct()on the real_TimedSceneclock and diffssrc/docgen/benchmark_data/baseline.json. Exit 1 on regression.--update-baselineonly after an intentional improvement.
- Manim /
scenes.py(marker blocks): Fix generators undersrc/docgen/**(manim_scene_support.py,scene_spec.py,scene_spec_generate.py,validate,yaml_generate, tests). Do not patch generated classes inside a consumer'sanimations/scenes.pybetweenBEGIN/END GENERATED SCENEmarkers; re-runscene-spec-generate/scene-compile --retimeandmaniminstead. Preferred consumer order: narration → TTS → timestamps → scene-spec/compile → Manim → compose. - Beat sync (fail-closed): when
timing.jsonhas words, every story box label must match a spoken phrase (wait_word); unmatched labels and leftover LLM indices are rejected. Opt out withpace: none. Legacy row-levelwait_segmentis upgraded towait_wordand written back onscene-compile(directcompile_scene_classstill rejects leftoverwait_segment). Fuzzy containment matching is not used.scene-compileclamps FadeIn / page-faderun_timeagainst the next word start so_TimedScene._clockcannot race past waits (issue #66 — do not emit cascading first-board dumps). After a reveal, a dwell slot may playIndicate/Circumscribewhen the gap to the nextwait_wordis long enough (also clamped). Long holds emit additional mid-hold pulses (timed_wait+ emphasis) so the board does not freeze after the first Indicate. Optional box fields:shape(rounded/pill/diamond),reveal(fade/grow/slide),emphasis(none/pulse/ring). Page transitions FadeOut revealed boxes, not the parentVGroup.scene-compilerefreshes stale_box/_arrow/_TimedScenehelpers inscenes.py. - Subject-beat coverage: implemented in
scene_spec.layout_density_violations/cluster_subject_beats; enforced byscene-spec-generateandvalidate(validation.subject_beat_coverage.enabled, default true). Not a blind label count. docgen benchmarkis required after clock / compile /_TimedScene/ dwell changes. Pytest string assertions are not a substitute. Do not remove the CIbenchmarkjob. See Required gate below.- Prefer stable CLI / library contracts and documented exit codes so CI can depend on them.
narration_from_source: hints in config +docgen narration-generate— owner-supplied context paths, not opaque bulk edits to outputs.- Avoid duplicating long orchestration docs here; link to downstream repos when describing their publish pipelines.
Clock, compile, scene-spec motion, _TimedScene helpers, and dwell/hold changes must stay green on the packaged corpus:
- Run
docgen benchmark(or./scripts/benchmark-scenes.sh). Exit 0 vssrc/docgen/benchmark_data/baseline.json. - Keep
tests/test_scene_benchmark.pyin the defaultpytest tests/run. - Keep the
benchmarkjob in.github/workflows/ci.yml(it must invokedocgen benchmark). Do not delete or skip it. --update-baselineonly when the scorecard change is the point of the PR; review the JSON diff. Do not bump the baseline to hide a regression.- When production fails in a new way, add a case to
standard_cases()and update the baseline in the same PR.
String assertions on compiled scenes.py and simulate_reveal_timeline are not a substitute — the harness executes construct() on the real clock.
Tests should cover CLI-visible behavior and contracts that adopters rely on: yaml-generate, scene-spec-generate, scene-compile, validate, compose, generate-all, pages, init, benchmark, gui / freeze (--smoke only in default pytest), freeze-safe docgen.resources paths, config loading (repo_root, env_file), and package exports. Use small in-tree fixtures; this library does not ship a dogfood bundle. Clock / compile changes must keep docgen benchmark at or above benchmark_data/baseline.json — string assertions on scenes.py are not enough. Do not run a full PyInstaller freeze in routine pytest.
- Session contract first: see the top of this file and
.cursor/rules/no-factory-loop.mdc. Finish the assigned list on one PR. Do not ask to merge. Do not start a YAML-key factory or recursive hunt.milestones/README.mdActive is human-assigned only; an empty Active line means stop, not “pick the next config key”. - Virtualenv: the project is installed editable into
/workspace/.venv(created by the startup update script). Shells do not auto-activate it — run. /workspace/.venv/bin/activate(or prefix the venv path) beforedocgen,pytest, orruff. Thedocgenconsole script lives at/workspace/.venv/bin/docgen. - System deps are pre-baked in the VM snapshot (not the update script):
ffmpeg+tesseract-ocr(validation/compose/OCR), plusbuild-essential,python3-dev,libcairo2-dev,libpango1.0-dev,pkg-config(needed to build themanimextra'smanimpango/pycairowheels). If a fresh VM ever lacks these, reinstall via apt beforepip install. - Standard commands are in
README.md/pyproject.toml/.github/workflows/ci.yml: lintruff check src/ tests/; testspytest tests/ -v --tb=short; requireddocgen benchmark(CI jobbenchmark); requireddocgen validateon a scratchinitbundle (CI jobvalidate,scripts/ci-validate-scratch-bundle.sh). The CI unit job also exportsPYTHONPATH=src(not needed locally because of the editable install, but harmless). - OpenAI / Grok / Anthropic vs offline commands:
tts,timestamps --engine whisper,image-generate,narration-generate,scene-spec-generate, andyaml-generate --llmcall a provider. Keys:CURSOR_API_KEY(Cursor Cloud), thenOPENAI_API_KEY(local Cursor, Claude Code, CI). OnlyANTHROPIC_API_KEY→ Claude chat (no TTS/images). Grok:DOCGEN_AI_PROVIDER=grok+XAI_API_KEY.docgen ai-statusprints the resolved key env. Image model isimage_generation.model/--model. Integration tests auto-skip without credentials. Fully offline:init,scene-compile,manim,compose,validate,lint,pages,concat,yaml-generate(no--llm),timestamps(defaultlocalengine), andbenchmark. - Generate against another repo (do not vendor): this environment already has
docgenon PATH. Point at a consumer checkout or clone URL — nothing is copied into that project'ssrc/:docgen --repo /path/to/consumer generate-alldocgen --repo github.com/org/consumer init --defaults--repofindsdocs/demos/docgen.yaml. Add the consumer as a Cloud repository dependency if you clone by GitHub URL (usesGITHUB_TOKEN). Networked stages:CURSOR_API_KEY(Cloud),OPENAI_API_KEY(local / Claude Code),ANTHROPIC_API_KEY(chat), orXAI_API_KEY(Grok). scene-compilegotcha: paced specs (wait_word) need atiming.jsonentry for that stem (docgen timestampsafter TTS). Preferscene-compile --retimeafter fresh timestamps; for a fully offline smoke render, author rows without wait indices only if you accept unpaced reveals.- No in-repo dogfood bundle: exercise the pipeline against a scratch bundle (
docgen init /tmp/<name> --defaultsin a throwaway git dir). Do not hand-edit consumer generated assets (see.cursor/rules/no-asset-edits.mdc). - Wizard / desktop GUI:
docgen wizard --port 8501is the Flask UI (bundle optional for the Benchmark view).docgen gui/docgen benchmark --guiopen the Vue benchmark view in a desktop window whenpywebviewis installed.docgen gui --smokeis the headless HTTP check (default pytest).docgen freezebuilds the GUI onedir; do not run a full PyInstaller freeze in routine CI/pytest. Keeppackaging/docgen-gui.specanddocgen.gui.packagingin sync.