Read on 2026-09-29. Path shorthands used below:
VFE/=~/.claude/skills/vwc-faceless-explainer/FE/=~/.claude/skills/faceless-explainer/MU/=~/.claude/skills/media-use/HC/=~/.claude/skills/hyperframes-creative/HF/=~/.claude/skills/hyperframes/APP/= the vets-who-code-app repository (HEAD b7c19088)RUN/=APP/videos/labor-day-sprint-proof-of-work/[issue-N.md](issue-N.md) dK= decision K in the context doc for hashflag-skills #N (docs/context/issue-N.md). This doc is docs/context/issue-3.md.
Issue text. The goal is to generalize vwc-faceless-explainer so any org can make a faceless explainer in its own brand. The issue records one lesson: the auto-generated frame spec "could not express VWC's brand", so brand/frame.md is hand-written. There are five acceptance criteria (ACs), mapped below. The issue is open, labelled enhancement, and has no comments (gh issue view 3 -R Vets-Who-Code/hashflag-skills).
Target repo. hashflag-skills is private, size 0, has no license, and was last pushed 2026-09-26. default_branch is set to main but no branch exists (gh api repos/Vets-Who-Code/hashflag-skills). Epic #1's sub-issues are #2 through #8. #9 is not linked, even though it supplies #3's fictional brands (gh api …/issues/1/sub_issues → 2 3 4 5 6 7 8).
Provenance of VFE. ~/.claude/skills is not a git repo, so this folder is the only copy (research: git rev-parse fails there). The owner's Claude Code session transcript d983056c-6624-4202-bb84-fcb6dc0ed6e9.jsonl shows how the files were made. It holds Write calls for brand/frame.md (2026-09-23T18:14:45Z), scripts/check-copy.mjs (18:15:08Z, rewritten 18:37:56Z) and SKILL.md (18:15:51Z), followed by Edit calls through 20:59:49Z. The last check-copy edit (19:35:22Z) matches the file's mtime (15:35:30 local). No other author appears (node scan of the transcript's tool_use blocks; stat). The transcript supports "no outside contributors", but there is no VCS history, so the owner should confirm it himself.
| Path | What it does | Source |
|---|---|---|
FE/scripts/build-frame.mjs |
Step 2. Copies a preset's FRAME.md to frame.md, remaps colors and fontFamily onto tokens.json, copies caption-skin.html, then validates. Flags: --preset, --tokens, --preset-dir. |
FE/scripts/build-frame.mjs:1-20,58 |
FE/scripts/lib/tokens.mjs |
parseColors/semanticColors/parseFonts. These decide which colors become ink, canvas, accent and accent2 for captions and #root. |
tokens.mjs:7,142,172 (exports); :28-60,140-170 |
FE/scripts/captions.mjs |
Karaoke caption builder. Takes --skin, --frame, --storyboard, --audio-meta, --out. Fills the skin's reserved holes and injects tokens. |
captions.mjs:67-79,229-281,402-421 |
FE/scripts/assemble-index.mjs |
Builds index.html and paints #root from the frame.md canvas role. |
assemble-index.mjs:535-567,606 |
FE/scripts/audio.mjs |
Faceless adapter over the shared engine. Flags: --voice, --speed, plus the sync-durations and fetch-sfx subcommands. It hard-codes provider: "auto". music: none in the storyboard YAML sets BGM mode none; any other value sets retrieve. |
FE/scripts/audio.mjs:129-134,145-175; FE/SKILL.md:104,142 |
FE/scripts/frame-packets.mjs, transitions.mjs |
Per-frame sub-agent packets; transition injection and verification. | FE/SKILL.md:148-200 |
MU/audio/scripts/audio.mjs, lib/tts.mjs, lib/heygen.mjs, heygen-tts.mjs |
Shared TTS/BGM/SFX engine. The engine takes --provider (auto, heygen, elevenlabs or kokoro), but the faceless adapter never passes it. heygen-tts.mjs --list lists Starfish voices. |
MU/audio/scripts/audio.mjs:18,130; lib/tts.mjs:37-76,299-345; heygen-tts.mjs:14-16,74-80 |
HC/frame-presets/ |
13 presets (FRAME.md + caption-skin.html + frame-showcase.html). Only code-editorial/fonts/ ships files: EB Garamond, Inter and JetBrains Mono woff2 at 400 and 700, each with its OFL text. |
ls 2026-09-29 |
HC/templates/design-picker.html |
Two-phase visual chooser (mood boards, palettes, type pairings). It proposes new directions. It does not record an existing brand. | HC/references/design-picker.md:1-40 |
MU/scripts/recipe.mjs |
freeze/use a named look (frame.md plus skeletons). It does not capture caption-skin.html or fonts. |
MU/scripts/lib/recipe-store.mjs:205-250,325-360 |
HF/references/brief-contract.md |
The upstream question protocol: ask one field per message, use native question UI when available, otherwise one plain-text question with one numbered list. Rendering stays user-gated in both modes. | brief-contract.md:51,92-103 |
HF/references/subagent-dispatch.md |
Harness-neutral dispatch: "never rely on the child seeing your conversation"; maps to Codex/OpenClaw; fallback is headless CLI workers, then inline execution. | subagent-dispatch.md:7,18-31 |
- Palette scales are at
APP/tailwind.config.js:84-137and fonts at:218-223. Token summary:APP/docs/DESIGN_DOC.md:305-324. Signature brand details::279-291. - The copy law lives at
APP/AGENTS.md:262-268. It makes Apply and Donate literal and uses "software engineering accelerator". - Logo variants and rules:
APP/src/pages/press-kit.tsx:14-43. The cream-reverse transforme_colorize:100,co_rgb:EEEDE9is atAPP/src/pages/api/og.tsx:34. - Cohort facts are in
APP/src/data/site-config.ts:3-27, and frame.md says to use them rather than memory (research: frame.md:383). - The voice pick (Orson) is an owner decision.
-
RUN/contains:- BRIEF.md;
- STORYBOARD.md (the silent cut);
- SCRIPT.md.bak (before the prosody lesson);
.narrated-backup/(narrated SCRIPT, STORYBOARD, audio_meta, caption_groups, frames);samples/(5 numbered voice auditions plus A/B takes);- audio.log;
index.htmlwith the#rootfix;- 3 renders.
(ls 2026-09-29)
-
Renders, in timestamp order (ffprobe 2026-09-29):
…15-56-29.mp4: 75.07 s, 9.61 MB, narrated take 1;…16-18-17.mp4: 73.10 s, 9.60 MB, narrated take 2;…16-59-37.mp4: 77.50 s, 9.20 MB, silent cut.
All are H.264 1920x1080 30 fps with AAC 48 kHz stereo (research ffprobe).
-
The project pins
npx --yes hyperframes@0.8.66(RUN/package.json).videos/is excluded only throughAPP/.git/info/exclude:21. -
The run holds the licensed fonts under
assets/fonts(shasum matches above), so it must not be copied as-is.
Step order as run by vwc-faceless-explainer:
- Step 0, setup.
- Run
export PATH=~/.nvm/versions/node/v24.14.1/bin:$PATH. HyperFrames needs Node >=22 and APP pins 20 (VFE/SKILL.md:40-45;npm view hyperframes engines.node→>=22). - Then run
npx hyperframes init "videos/<project>" --non-interactive --example=blank --skill=faceless-explainer, write BRIEF.md, and shownpx hyperframes auth statusverbatim (FE/SKILL.md:28-43).
- Run
- Step 1, overridden. Write
capture/extracted/visible-text.txt(the source, verbatim) andtokens.jsonwithcolors: []andfonts: [](VFE/SKILL.md:84-92). Then stage the brand (VFE/SKILL.md:60-72):cp brand/fonts/*.woff2 assets/fonts/cp brand/frame.md ./frame.mdcp brand/caption-skin.html .hyperframes/caption-skin.htmlcurlthe cream logotype from VWC's Cloudinary intopublic/vwc-logotype.png
- Step 2 is skipped.
build-frame.mjsis never run (VFE/SKILL.md:94-99). - Step 3, storyboard and script. Written under the copy law and the story shape (VFE/SKILL.md:106-153). Then
node VFE/scripts/check-copy.mjs --project . --numeralsmust exit 0 before the approval ask (VFE/SKILL.md:155-168). - Step 3.1, audio. Upstream runs unchanged:
node FE/scripts/audio.mjs --script ./SCRIPT.md --storyboard ./STORYBOARD.md --hyperframes . --out ./audio_meta.json --voice <id> &(FE/SKILL.md:104).- TTS order is HeyGen Starfish, then ElevenLabs, then Kokoro (MU/audio/scripts/lib/tts.mjs:37-50).
- The HeyGen body is
{text, voice_id, speed, language?}(tts.mjs:299-311). - BGM is
nonewhen the storyboard saysmusic: none, otherwiseretrieve. An explicitretrievewith no HeyGen credential becomesnoneand adds an anomaly. The MusicGengeneratepath is never chosen (FE/scripts/audio.mjs:145-174; MU/audio/scripts/audio.mjs:195-207).
- Steps 4-5, visual design and frames.
- Upstream runs
audio.mjs sync-durations, thenfetch-sfx, thenframe-packets.mjs, then one sub-agent per frame writescompositions/frames/NN-*.html. - After that come
captions.mjs build … &andassemble-index.mjs(FE/SKILL.md:142-166). - Finally,
#root { background: #091f40; }is edited intoindex.htmlby hand (VFE/SKILL.md:266-273).
- Upstream runs
- Step 6, gate and render.
check-copy.mjs --project .runs again.- Then
transitions.mjs inject,transitions.mjs verify,npx hyperframes lint,check,snapshot --at <midpoints>andpreview --background. - After approval:
render --skill=faceless-explainer --quality high --output renders/video.mp4(VFE/SKILL.md:275-293; FE/SKILL.md:178-200).
Constants and formats
- The default canvas is 1920x1080. Portrait 1080x1920 and square 1080x1080 are also supported (FE/scripts/lib/dimensions.mjs:8-45).
- The caption band is the bottom 16.67%, which is 180 px at 1080, so frame content stays above y=880 (dimensions.mjs; VFE/SKILL.md:225-226).
- Caption groups:
SILENCE_GAP0.18 s,TAIL_PAD0.12 s, 2-4 words depending on density (captions.mjs:48-55,121-141). - The faceless sweet spot is 30-90 s, with a hard cap near 3 min (HF/references/routes/faceless-explainer.md:4).
- The BGM bed sits at 0.12 linear under narration and 0.9 in a silent film (MU/audio/scripts/lib/bgm.mjs:23-28).
- Every
index.htmlloads GSAP 3.14.2 from jsDelivr (assemble-index.mjs:581). - The HeyGen Starfish endpoint is
POST /v3/voices/speech. It takestextof 1-5,000 characters,voice_id,speed0.5-2.0,language/localeandinput_type("keep at its defaulttext"). "<break>is the only markup to include" (developers.heygen.com/docs/voices/speech).
The voice sets the video length (the basis for a word budget)
sync-durationswrites each frame's duration from its voice line. In the run, the 8 narrated frame durations (7.445 … 7.236 s) equal the 8duration_svalues in.narrated-backup/audio_meta.json, which sum to 73.091 s. The render is 73.10 s (RUN/.narrated-backup/STORYBOARD.md:40-258; audio_meta.json; ffprobe). So in a narrated film, duration ≈ the sum of voice durations.- In a silent cut, duration is the sum of the storyboard
duration:values: about 0.4 s per supporting word after it settles, plus a hold of at least 1.2 s (VFE/SKILL.md:227-228; RUN/STORYBOARD.md:3duration: 77.5s). - Published and measured paces, with what 60-90 s means in words:
| Pace (words/s) | Source | Words for 60 s | Words for 90 s |
|---|---|---|---|
| 2.2 | pr-to-video/references/story-design.md:175,184 ("duration ≈ ceil(words/2.2)") | 132 | 198 |
| 2.3 | HC/references/narration.md:92 (worked example, 140 words in 62 s) | 138 | 207 |
| 2.45 | Orson, take 1: 184 words / 75.049 s (RUN/audio.log) | 147 | 221 |
| 2.5 | HC/references/narration.md:7 | 150 | 225 |
| 2.59 | Orson, take 2: 189 words / 73.091 s (RUN/.narrated-backup/audio_meta.json word arrays) | 155 | 233 |
- The band that lands inside 60-90 s at every pace above is 156-198 words (60×2.59 and 90×2.2; arithmetic).
- Pace differs between voices. The five numbered auditions in
RUN/samples/run 8.07-10.16 s (ffprobe). If they share one text, that is about a 26% spread (inference:samples/list.txtonly concatenates them and does not show the text). - Kokoro's pace was never measured.
The hidden brand contract (role mapping)
inkis the first key matching/(?:^|[-_])ink(?:[-_]|$)|black|charcoal|^text(?:-dark)?$|outline|noir/i.canvasis the first key matching/cream|paper|canvas|white|bg|ground|surface|base|sand|parchment|off-?white|bone/i.groundis in the canvas pattern.- Accents are the remaining colors ranked by chroma, excluding status keys (tokens.mjs:140-167).
- For VWC's frame.md this gives
{ink #1A1823, canvas #EEEDE9, accent #FDB330, accent2 #c5203e}and fonts{display "GothamPro", body "Gilroy", mono null}(node run of tokens.mjs, 2026-09-29). - Chroma values are gold 205, red 165 and navy 55, so navy can never be an accent (arithmetic on the hex values).
- The same
canvasfeeds#rootand--cap-canvas, the color of spoken caption text (assemble-index.mjs:537-545; captions.mjs:411, per research).
Caption skin contract
- The skin must contain exactly one each of
<style data-brand-tokens></style>,var GROUPS = [];,var DURATION = 0;,data-duration="0",data-width="0"anddata-height="0"(captions.mjs:17-31). - It receives
--cap-ink,--cap-canvas,--cap-accent,--cap-accent-2,--font-display,--font-body,--cap-band-topand--cap-band-height(captions.mjs:402-421). - Hooks keep their names:
.caption-group,.caption-word,.is-active,.is-spoken. State changes usegsap.set({className}), nevertl.call()(research: captions.mjs:398-419).
Copy gate mechanics
- ALLOW suppresses a rule only when the match sits inside an allowed span. Spans are resolved across the whole wrapped paragraph (check-copy.mjs:40-62).
numerals()looks only at lines with exactly 4 leading spaces, at- voiceover|vo|narration:bullets, and at quoted strings (check-copy.mjs:98-115).--numeralsreports on.mdtargets only (check-copy.mjs:183-192).
Question handling today
- Upstream intent capture asks in the main conversation, one field per message. It uses native question UI when available and otherwise sends plain text with one numbered list (HF/references/brief-contract.md:92-103).
- In Claude Code,
AskUserQuestion"is removed from every subagent, even when listed in thetoolsfield" (code.claude.com/docs/en/sub-agents). - Frame workers are sub-agents that get only a packet and a role file (FE/SKILL.md:148-152; subagent-dispatch.md:7), so nothing below the orchestrator can ask the user anything.
Generating the frame spec
build-frame.mjsgives display and body the same font:bDisplay = bBody = nonMono[0](build-frame.mjs:337-338).- It overwrites frame.md wholesale with
writeFileSync(framePath, md)(build-frame.mjs:517). - It has only 4 color roles ranked by chroma, which is why navy never lands (tokens.mjs:142-167; VFE/SKILL.md:30-34).
- Keep
tokens.jsoncolors and fonts as[]so nobody is tempted to run Step 2 (VFE/SKILL.md:84-86). - Inference, from the build-frame header comment ("Empty brand colors → the preset palette is kept … Empty brand fonts → kept"): with empty tokens,
build-frame.mjs --preset-dir <dir> --preset <name>copies a FRAME.md verbatim. A pack laid out as a preset directory could be installed without being overwritten. This is untried. - Key names and key order decide caption colors and the video ground. A key named
ground-navy, orcharcoalplaced aboveink, silently repoints them (tokens.mjs:140-167). A generator must never emit a key namedground/bg/basefor the video field (inference from the regex).
Ground and captions
#rootrenders cream between navy frames. The hand fix is lost on every re-assemble because index.html is rewritten wholesale (assemble-index.mjs:537-545,606).transitions.mjs injectpreserves it (research: transitions.mjs:274-300).- Renaming a color to force navy would make captions unreadable (VFE/SKILL.md:268-269).
- VFE/SKILL.md:269 says "
captions.mjshas no override flag". That is wrong:--skinand--frameexist (captions.mjs:73-79). Feeding captions their own frame file is an untried alternative fix (inference). - If the skin is missing, captions fall back to a built-in Roboto black pill (research: captions.mjs:30,468; VFE/SKILL.md:78-80).
- Don't trust the caption-skin comments. The fallbacks are broadside orange, not VWC colors (caption-skin.html:73-127).
.cap-ctaresolves to red and.cap-numto Gilroy, and no code applies either class (research grep).
Copy gate
- A false positive is already fixed: "register", because "NAVY register" is frame.md's own vocabulary. The rule was narrowed to
register (now|today|here)(check-copy.mjs:17-19,26). - Allowlists already added: "not tutors" and wrapped
never:lists (check-copy.mjs:43-51; self-check). - False negatives probed on 2026-09-29 all return
[](node probe ofscan/numerals):"Do not wait — sign up today."and"Never miss a cohort: enroll now", because the prohibition cue at :50 swallows the rest of the sentence;"Join our pipeline","No hand-holding here","learners and users","background: white","#ffffffcc","Support Our Mission"and"APPLY FOR 2027";- a tab-indented spoken figure, which
numerals()never lists.
- The docs and the gate have drifted. SKILL.md bans "pipeline" and "hand-holding" (VFE/SKILL.md:115,118), but RULES doesn't (check-copy.mjs:20-38).
- The "curly" quote class in
numerals()is plain ASCII, so curly-quoted figures are missed (research:od -con :106). - Frame HTML is never scanned for figures. In the real run, "2027 Cohort" on the end card was never listed (research: RUN/compositions/frames/08-end-card.html:80).
- Casing is written in sentence case and uppercased by CSS. The gate doesn't enforce this (VFE/SKILL.md:150-153; probe).
Length and word budget
-
The brief asked for 90 s (RUN/BRIEF.md:9,
length: 90s). The narrated storyboard then setduration: 75s(RUN/.narrated-backup/STORYBOARD.md:3), and the silent storyboard set 77.5 s (RUN/STORYBOARD.md:3). -
Measured against those targets:
- take 1 was 75.07 s (+0.07 s against 75);
- take 2 was 73.10 s (−1.90 s against 75);
- the silent cut was 77.50 s, on its own 77.5 s target.
Against the 90 s brief, every render came in 12.5-16.9 s short (ffprobe; arithmetic).
-
issue-4.md d5 says the run "overshot by 13-17 s". That doesn't match the measurements. The only render over 75 s is the silent cut, which had its own target. The real lesson is that nothing checks measured length against the brief. Nothing parses
length:after Step 0, and the voice, not the storyboard, sets the final duration (see "How it works now").
Narration and audio
-
The faceless adapter passes only
--voiceand--speed, and the engine body is{text, voice_id, speed, language?}. Punctuation is the only prosody control the wrapper uses, and a**Voice settings:**line is read by nothing (VFE/SKILL.md:174-197; tts.mjs:299-311). -
HeyGen's docs say
<break>markup is accepted intext(developers.heygen.com/docs/voices/speech). It is untested whether<break>survivesparseScript, shows up in captions, or breaks the Kokoro fallback. -
Write the direction for conviction, not calm. The before/after evidence is
RUN/SCRIPT.md.bakagainstRUN/.narrated-backup/SCRIPT.md(research diff). -
A partial TTS run exits 0. Failed lines become "non-fatal" anomalies and
voices = results.filter(Boolean)(MU/audio/scripts/audio.mjs:159-173). The adapter prints onlymeta.voices.length(FE/scripts/audio.mjs:181-184). A code comment cites "7/8 lines fail on first run" at concurrency 4 (research: audio.mjs:73-80). Concurrency isHYPERFRAMES_TTS_CONCURRENCY, default 4 (MU/audio/scripts/audio.mjs:80). -
Re-runs re-synthesize every line and pay again. Inference from
synthLine, which overwritesassets/voice/<id>.wav. -
The voice isn't pinned. Orson (
00e3d285aba44b27a83c47c02c9c2d9c) is recorded only as an owner decision, not in VFE (grep) and not in~/.media/preferences.json("preferences": {}). With no voice, HeyGen English defaults to Marcia05f19352e8f74b0392a8f411eba40de1(tts.mjs:57-66). -
Forcing Kokoro is not possible through the adapter. With any HeyGen credential visible,
autopicks HeyGen (tts.mjs:50). Credentials come from:HEYGEN_API_KEYorHYPERFRAMES_API_KEY;- the first
.envfound up to 5 parent directories up; ${HEYGEN_CONFIG_DIR:-~/.heygen}/credentials.
(heygen.mjs:20-60) Inference, untested: setting
HEYGEN_API_KEY=,HYPERFRAMES_API_KEY=,ELEVENLABS_API_KEY=(empty) andHEYGEN_CONFIG_DIR=<empty dir>falls through to Kokoro, because.envloading skips keys already inprocess.env(heygen.mjs:39) and empty strings are falsy (:51). -
Voice ids must be Starfish ids; v2 ids return HTTP 400 (MU/audio/references/tts.md:79, per research).
-
The Kokoro default is documented inconsistently:
am_michaelin code,af_heartin docs (tts.mjs:61 vs tts.md:103). -
Switching to music-only needs
rm -f SCRIPT.md audio_meta.json audio_engine_meta.json caption_groups.json compositions/captions.html; rm -rf assets/voice(VFE/SKILL.md:199-213). -
Storyboard
music:must be a short mood phrase. A long message returned "no music match" (RUN/audio.log; VFE/SKILL.md:215-218). -
A silent cut is a rebuild.
pad-frame-durationsilently does nothing unless a single tag carries bothdata-composition-idanddata-duration(VFE/SKILL.md:220-234; FE/scripts/lib/pad-frame-duration.mjs:16-35).
Frames and render
- Registry
grain-overlayuses an infinite CSS animation, which is non-deterministic under seek.comparison-splitships a 999cqw radius, shadows and color-mix. Use a static fractalNoise data-URI instead (VFE/SKILL.md:255-264). - The storyboard parser accepts Frame, Beat or Scene at H2 or H3, but frame-packets splits only on
^## Frame(FE/scripts/lib/storyboard.mjs:17; HF/scripts/lib/frame-packets-core.mjs:35-45). - The real run produced timestamped renders, not
renders/video.mp4. Inference:--outputwas omitted (RUN/renders ls; hyperframes-cli/references/preview-render.md:104-143). checkreports 1-4 pxtext_box_overflowon#caption-word-*, a known false positive (FE/SKILL.md:192).- Brand-correct tokens can still fail as text. cream-hint on navy is 3.84:1 and slate on cream 4.0:1, while
hyperframes checkenforces 4.5:1 (frame.md:190-201). Red on navy is 2.85:1 (research WCAG calc). - frame.md has drifted: the project copy lacks
charcoaland the WCAG section the skill copy has (research diff of RUN/frame.md vs VFE/brand/frame.md).
Fonts and licensing
- Every named family needs an
@font-facepointing at a shipped file (font_family_without_font_face). Webfont CDNs are forbidden (HF/references/frame-worker-core.md:79; frame.md:406-414). document.fonts.readyresolves even when a font URL 404s, so a missing file falls back silently. This was seen in the blog-graphic renderer (research: APP/scripts/generate-blog-graphic.ts:119). Not researched: whetherhyperframes checkcatches a missing font file.- URL capture downloads the site's fonts: the VWC reel capture pulled Gilroy and Font Awesome Pro files (ls APP/videos/vets-who-code-reel/capture/assets/fonts/).
- Font file names lie.
APP/videos/vets-who-code-reel/assets/fonts/Gilroy-ExtraBold.woff2is byte-identical toAPP/public/fonts/gilroy/Gilroy-UltraLight.woff2(sha11d2b2e9381e39512fa194f32bb3170ce48489765). The.woffpair matches too (sha11bf99f3c…) (shasum 2026-09-29). A guard keyed on file names or on a sibling OFL file would miss a renamed commercial file. - The commercial or restricted font files tracked in APP
public/fonts/are: Gilroy 30, Gotham 56, Font Awesome Pro 10, Rossela Demo 2. That makes 98 files, not the 4 in VFE. There are also HashFlag (8) and Linea (14) files, whose licenses were not researched (git ls-files public/fonts). - GothamPro weights in the pack are 700 and 900, and frame.md says never to request 600 or 800 (frame.md:226-227). Its claim that GothamPro "ships no true italic" is wrong: APP tracks GothamPro italic files (git ls-files public/fonts/gotham).
- Parts of the pipeline are not open-licensed:
- GSAP uses the "GSAP Standard License", which is not OSI (upstream CREDITS.md);
- MusicGen weights are CC-BY-NC (huggingface.co/facebook/musicgen-small);
- Pixabay SFX can't be redistributed standalone (pixabay license summary).
- Kokoro-82M weights are Apache-2.0 ("can be deployed anywhere from production environments to personal projects") (huggingface.co/hexgrad/Kokoro-82M).
- HeyGen output rights depend on the plan. On Creator/Pro/Business plans "you own all rights in your … User Output" and HeyGen "does not restrict … commercial purposes" (heygen.com/terms §3). Free Plan output is licensed "solely for personal, non-commercial, and internal evaluation purposes" and "may not be sold, sublicensed, redistributed, monetized…" (§4). The terms were last updated 2026-07-23.
- The terms don't say whether API or OAuth-allowance usage counts as the "Free Plan".
- Neither the terms nor the background-music API page (
/v3/audio/sounds) says anything about music licensing (heygen.com/terms; developers.heygen.com/background-music). - So a README render voiced through a free allowance, or scored with catalog BGM, is not cleared for a public repo (inference).
- The obvious neutral base looks like another org's brand.
HC/frame-presets/code-editorial/FRAME.mduses ink#141413and cream#FAF9F5. Those are exactly Dark#141413and Light#faf9f5in the installed Anthropic brand-guidelines skill (…/skills/synced/…/brand-guidelines/SKILL.md:21-22), and the preset adds a coral accent and a "✱ coral spike mark" (code-editorial/FRAME.md:4-8). Shipping it unchanged as the neutral default risks reading as another company's identity, and VWC keeps vendors off its brand surfaces (owner decision). The fonts are safe to reuse; the palette is not (inference).
Environment and upstream drift
- hyperframes went from 0.8.66 to 0.8.91 in 6 days (npm view, research).
initrefreshes installed skills unlessHYPERFRAMES_SKIP_SKILLS=1(HF/references/skill-lifecycle.md:12-14). - Faceless scripts import from sibling skills by relative path, and upstream #4356 is changing that (research: assemble-index.mjs:60; commit 29fc95395d).
- The HeyGen resolver loads the first
.envit finds, up to 5 parent directories up. Inference: a run inside a host repo bills that repo's key, and a run under~/.cache/hashflag/<skill>/<project>/reaches~/.envin 5 steps (MU/audio/scripts/lib/heygen.mjs:20-47). - The CLI sends telemetry, and its skill tells agents to post feedback publicly after each render. Opt out with
HYPERFRAMES_NO_TELEMETRY=1(hyperframes-cli/SKILL.md:116,120-126; references/upgrade-info-misc.md:100-113). npx hyperframes auth statusexits 1 when you're signed out, which is normal. Don't chain it with&&(product-launch-video/SKILL.md:38).- Chrome can die at startup inside macOS agent sandboxes (hyperframes-cli/references/doctor-browser.md:34-45).
doctor --jsonalways exits 0, so gate on.ok(same file :5-57).
Brand sources contradict each other (this matters when capture ingests docs)
- White is banned in video (frame.md:187-188; check-copy.mjs:36-37), required in image prompts (APP/scripts/image-prompts.ts:14,42,55), and used for the white logo (APP/src/pages/press-kit.tsx:31-33).
- The brand docs break the copy law themselves: "students" (APP/docs/brand-style-guide.md:12), "START YOUR JOURNEY" (APP/docs/DESIGN_DOC.md:20) and "pipeline" (APP/public/llms.txt:36).
- Verbatim testimonials say "frontend developer" and "signed up" (APP/src/data/homepages/index.json:149,160). A gate that rewrites copy would change real people's words (inference).
- Issues, and conflicts between their dossiers that must be settled first
- #9 (fixtures). It defines the minimum
brand.mdfields and wants two clearly different brands "so #3 can show the same script rendered in both". It bans binaries and isn't linked to the epic (gh issue view 9; sub_issues API). - brand.md format: three dossiers disagree. The newcomer on #9 will pick one unless the owner decides first.
- issue-9.md d6: fixed
##headings (Name, Mission, Colors, Fonts, Voice (writing tone), Copy rules, Logo), color linesink: #hexusingink/canvas/accent/accent-2, no YAML. - issue-2.md d3: YAML front matter with
image.*andvoice.tts.{provider, model, voice, style}. - issue-1.md d5: brand.md in #9's fields plus a machine-readable
brand.json, plus a generated frame.md and caption skin.
- issue-9.md d6: fixed
- #2. Its AC says "A brand style file controls the image look and the narration voice", and #2 is built first (gh issue view 2; owner build order).
- Consumers of the pack.
- #4 needs "the builder's own brand pack or a neutral default", and issue-4.md d9 assigns the neutral pack to #3.
- #5 wants 60-90 s videos and #7 wants 45-90 s.
- #8 wants brand, voice and CTAs "from the organization's configuration", and issue-8.md d7 adds a
kitsection to #3's pack. - #1 says "the repo ships only open-licensed defaults" (gh issue view 1, 4, 5, 7, 8).
- License: two dossiers disagree. issue-1.md d1 recommends AGPL-3.0 (code), CC0 (fixtures) and an Apache NOTICE. issue-2.md d2 recommends MIT or Apache-2.0 with written OK from two APP contributors.
- #9 (fixtures). It defines the minimum
- Repo bootstrap (blocking). No branch exists. LICENSE, .gitignore and CI must land before any PR (gh api; owner decision).
- Runtime
- Node >=22 (hyperframes engines). v22.6.0 and v24.14.1 are installed here (ls ~/.nvm/versions/node).
- FFmpeg/ffprobe.
- chrome-headless-shell in
~/.cache/hyperframes/chrome, 193 MB here (hyperframes-cli/references/doctor-browser.md). - The hyperframes CLI: 0.8.91 is latest, and 0.8.66 was used in the real run. issue-7.md d12 asks to coordinate the pin with #3.
- Upstream skills
faceless-explainer,hyperframes,hyperframes-core,hyperframes-creativeandmedia-use, installed side by side (cross-skill imports).
- Harness.
- Guided capture needs the main conversation, because Claude Code subagents have no
AskUserQuestion(code.claude.com/docs/en/sub-agents). issue-8.md d2 runs #8 in the main conversation for the same reason. - Other harnesses (issue-1.md d2 plans
.agents/and.opencode/symlinks) have noAskUserQuestion, so capture must also work as plain text (HF/references/brief-contract.md:103). - Not researched: whether
AskUserQuestionworks in headlessclaude -p.
- Guided capture needs the main conversation, because Claude Code subagents have no
- Offline narration. Python 3.8+ with
kokoro-onnx soundfile: about 311 MB of model plus 27 MB of voices, with Apache-2.0 weights. Word timings come from whisper.cpp throughnpx hyperframes transcribe(MU/audio/references/requirements.md:12,23,26; tts.mjs:278-284; huggingface.co/hexgrad/Kokoro-82M). Not researched: the licenses of kokoro-onnx and the Whisper model files. - Accounts and keys (bring your own)
- HeyGen (
HEYGEN_API_KEY, or OAuth throughnpx hyperframes auth): TTS with native word timestamps, plus BGM retrieval. Output is redistributable only on a paid plan (heygen.com/terms §3-4). - Optional
ELEVENLABS_API_KEY. Its output terms were not researched. - Gemini TTS exists upstream only on main (commit 01601d1105, #4377), and only by calling the engine directly, because the faceless adapter hard-codes
provider: "auto"(research: upstream FE/scripts/audio.mjs:165-177). - #3 needs no Cloudinary, but VWC's logo URL has to become a file in the pack.
- HeyGen (
- Licensing.
- The frame.md structure and the caption skin derive from the Apache-2.0 broadside preset (frame.md:5-6; research diff). Porting them brings Apache-2.0 §4 duties: the license text, the HeyGen copyright and a change notice (inference, legal reading).
- Apache-2.0 code "can … be included in GPLv3 projects", one way only (apache.org/licenses/GPL-compatibility.html). Inference: the same holds for AGPL-3.0, which that page doesn't mention.
- #3's own inputs (VFE, written in the owner's session; see Provenance) add no third-party consent requirement under any license.
1. A guided brand-pack step captures colors, fonts (by name or URL), logo files, voice and copy rules, and writes a brand/ folder that stays on the user's machine.
- Already satisfies: nothing is guided.
- VWC's pack is hand-written (frame.md, caption skin, fonts).
- Its copy rules live inside
check-copy.mjs. - The logo is fetched from VWC's Cloudinary at Step 1.
- The voice exists only as an owner decision, in no file (VFE/).
- Partial prior art:
hyperframes capturepulled CSS color variables from a live site (APP/videos/vets-who-code-reel/capture/extracted/tokens.json:1-30).- The upstream question protocol already covers native UI and a plain-text fallback (brief-contract.md:92-103).
- design-picker proposes directions rather than recording a brand.
- recipe.mjs omits skins and fonts.
- Missing:
- One schema agreed across #2, #8 and #9 (Dependencies).
- The question flow, with a mode that asks nothing and only validates a hand-written file (for subagents, #8 and CI).
- A validator: role prediction via tokens.mjs, WCAG pairs, font files present with a license, logo present.
- A default location outside any repo.
- Risk:
- "Voice" means both the TTS speaker id and the writing tone (#9 "3-5 adjectives"; issue-9.md d6 already heads it "Voice (writing tone)").
- A font "by URL" is usually a licensed webfont, and names lie (the reel's renamed Gilroy).
- Key naming silently changes caption colors.
- Capture started inside a subagent cannot ask anything.
2. The explainer reads the pack for design, captions and copy gating, and the gate enforces the pack's own rules.
- Already satisfies: the staging architecture (VFE/SKILL.md:56-92), captions deriving their tokens from frame.md (captions.mjs:402-421), and the gate engine (check-copy.mjs).
- Missing:
- frame.md and the caption skin generated or filled from the pack.
- RULES and ALLOW loaded from the pack.
--voicetaken from the pack.- An automated
#rootground fix. - A partial-audio and duration check.
- Figures in frame HTML included in
--numerals.
- Risk:
- The false negatives above.
- A brand's design vocabulary colliding with its own rules (the
registerprecedent), so a per-pack ALLOW is needed. - Rules drifting from the docs (pipeline, hand-holding).
3. No licensed fonts ship in the repo; only open-licensed defaults such as JetBrains Mono (OFL).
- Already satisfies: OFL JetBrains Mono with its license text (VFE/brand/fonts/, byte-identical to upstream). OFL Inter, EB Garamond and JetBrains Mono are in
HC/frame-presets/code-editorial/fonts/. - Missing:
.gitignorerules forbrand/,videos/,renders/andassets/fonts/.- A CI guard that doesn't trust file names. A "license file beside it" rule fails on a renamed Gilroy placed next to a copied OFL text, and APP already contains such a rename (shasum).
- Risk:
- Copying VFE or RUN as-is commits Gilroy and Gotham.
- Captures pull Font Awesome Pro too.
- The same files are public in AGPL APP
public/fonts/, and gotham.md points to a third-party gist (research). That's outside #3, but VWC's pack loads fonts from there.
4. An example brand pack for a fictional org renders a 60-90 s video end to end.
- Already satisfies: a VWC run of 73.1-77.5 s shows the pipeline works (ffprobe).
- Missing:
- The fictional pack. #9 hasn't started, and its fixtures ban binaries, so the font files and any logo file have to live in #3's example.
- A narration path that needs no account (Kokoro) and a BGM decision (
music: none). - A way to force Kokoro when a HeyGen credential is visible.
- A word budget and a measured duration gate. Target 156-198 words across the measured paces, then gate on the voice total and the ffprobe duration (How it works now). Nothing checks length today.
- Risk:
- Kokoro's pace is unmeasured.
- Needs Node >=22, ffmpeg, Chrome and a TTS engine.
- Upstream drift between the render and the README.
- Can't run in CI without keys or a heavy Kokoro setup (inference).
5. The README shows the same script rendered in two different brand packs.
- Already satisfies: nothing.
- Missing:
- A second pack.
- A brand-neutral SCRIPT.md that passes both packs' gates and fits the word band.
- Media hosting.
- Renders whose voice and music are cleared for a public repo.
- Risk:
- The script has to be written in sentence case so each pack's casing applies (VFE/SKILL.md:150-153).
- Each MP4 is about 9.6 MB (ffprobe), which bloats the repo if committed.
- HeyGen Free Plan voice output or uncleared catalog BGM would put redistribution-restricted media in the README (heygen.com/terms §4; inference).
- brand.md format. Decide before #9 is assigned. Default: follow issue-9.md d6.
brand.mduses fixed##headings in #9's order (Name, Mission, Colors, Fonts, Voice (writing tone), Copy rules, Logo) withkey: valuelines, and no YAML.- Colors use role names:
ink,canvas,accent,accent-2, plus an optionalgroundfor the video field, which defaults tocanvas. - #3 adds optional sections:
## Narration(one voice id per provider),## Video(casing, story shape),## Calls to action(verb and URL),## Copy gate(ban/fix and allow lines). #2 adds## Image style, and #8 adds## Kit. - The validator compiles brand.md into a derived
brand.jsonthat scripts read. It is never hand-edited, which satisfies issue-1.md d5's machine-readable file without a second source of truth. - frame.md and caption-skin.html are generated once and then owned by the user: overwrite only with
--forceand a diff, like the refuse-overwrite pattern in APP/scripts/generate-blog-graphic.ts:39-41. - Why:
- #9 is written first, by a newcomer, with no code, and headings are easier to write correctly than YAML.
- Explicit roles remove tokens.mjs's key-name guessing.
- issue-2.md d3's YAML fields map one-to-one onto sections.
- Alternative: YAML front matter (issue-2.md d3), but then #9 must be told before it starts.
- The owner posts the choice on #9.
- Field ownership across skills. Default:
- #3 owns
Colors,Fonts,Logo,Narration,Video,Calls to actionandCopy gate. - #2 owns
Image style. - #8 owns
Kit. Narrationis keyed by provider (heygen:,kokoro:,gemini:), and #9's fixtures leave it out (issue-9.md d8; issue-4.md d10; issue-8.md d5).- Why: #2 is built first, #8 reads everything, and one file stops the skills drifting apart.
- #3 owns
- Where packs and projects live. Default:
- Packs in
${XDG_CONFIG_HOME:-~/.config}/hashflag/brands/<slug>/, with a--brand <dir>override. - Projects in
${XDG_CACHE_HOME:-~/.cache}/hashflag/brand-explainer/<project>/, with--out. - Why:
- Projects follow pr-to-video's resolver (pr-to-video/scripts/project-dir.mjs:34-53) and match issue-1.md d12 and issue-8.md d10.
- Packs are user data, not cache, so they shouldn't live where caches get cleared (inference).
- The AC says packs stay on the user's machine, and VWC's
videos/is ignored only through a local exclude.
- Packs in
- Depend on or vendor HyperFrames. Default: depend on it.
- Pin the CLI (
npx --yes hyperframes@<ver>), setHYPERFRAMES_SKIP_SKILLS=1andHYPERFRAMES_NO_TELEMETRY=1, and record the tested upstream commit. - Use one pin for #3 and #7 (issue-7.md d12), and match issue-1.md d10.
- Why: 25 releases in 6 days, cross-skill imports are in flux, and vendoring adds Apache duties on top of the ones the template already carries.
- Pin the CLI (
- TTS and music for the example and README renders. Default:
- Both README renders use Kokoro and
music: none. - Force Kokoro by blanking the HeyGen/ElevenLabs env and pointing
HEYGEN_CONFIG_DIRat an empty dir (untested inference). If that fails, call the engine with--provider kokoro. - VWC's own pack records Orson under
heygen:. - Why:
- Free Plan HeyGen output can't be redistributed (heygen.com/terms §4).
- Catalog BGM has no stated license (developers.heygen.com/background-music).
- Kokoro weights are Apache-2.0.
- No account is needed.
- Both README renders use Kokoro and
- The copy gate. Default: port the engine and load rules from the pack.
- Limit the prohibition cue to design prose (frame.md and storyboard design bullets).
- Add frame HTML to
--numerals, and accept tab-indented spoken lines and curly quotes. - Keep all 23 assertions.
- Add a verbatim-quote marker that flags quotes instead of blocking them.
- Why: #5, #6 and #7 rely on the figure list, and a real CTA violation passes today.
- Story shape. Default: a pack field. The neutral default is hook, claim, mechanism, proof (sourced numbers only), then a mandatory CTA end card. VWC's pack keeps its 7. Why: VWC's shape is named after its own registers, and #4 and #7 run to different lengths.
- Automate the silent failures. Default: ship two scripts.
post-assemble.mjssets#rootto the pack's ground and is safe to re-run.verify-audio.mjsexits 1 in either case:voices.length≠ the number of SCRIPT lines;- the voice total is outside the duration band (decision 9).
- Why: both depend on the agent remembering them today, and #8 needs mechanical checks.
- Duration gate and word budget. Default: the gate applies the same idea as issue-4.md d5 (measured, not estimated), with #3's bounds, in three checks:
- (a) At the Step 3 gate, warn when the spoken words fall outside 156-198 (60-90 s at 2.2-2.59 words/s).
- (b) After TTS, fail unless the sum of
voices[].duration_sis within 60-90 s. This runs before any frame sub-agent is paid for. - (c) After render, fail unless the ffprobe
format=durationis within 60-90 s. - Bounds come from
--min/--max, so #4 can use 55-65 and #7 45-90. - The first time a pack's voice is used, measure its pace from one sample line and store it in
## Narration. - Why: video length equals the voice total (RUN evidence), pace varies by voice and Kokoro's is unknown, and nothing checked the brief's 90 s on VWC's run.
- Neutral default pack. Default: #3 ships
packs/neutral/, as issue-4.md d9 proposes and #7 needs.- Fonts: Inter 400/700 for display and body, and JetBrains Mono 400/700 for mono. Copy them byte-for-byte from code-editorial with their OFL texts.
- Palette: a new, validated one (dark ground, light canvas, one accent). Not code-editorial's ink/cream, which match Anthropic's brand hexes.
- Story shape: the neutral one from decision 7.
- Why: #1 says the repo ships only open-licensed defaults, and #4 and #7 need a pack for users who have none.
- How guided capture asks questions. Default:
- Capture runs only in the main conversation, one field per message.
- Use native question UI (
AskUserQuestionin Claude Code) when present. Otherwise send one plain-text question with one numbered list for factual fields, and an open question for tone (brief-contract.md:97-98,103). - Every field can instead be written straight into brand.md, followed by
brand-pack validate. - In a subagent, a headless run, CI or #8, capture never starts. The caller gets exit 1 with the list of missing fields.
- The explainer requires a valid pack and never starts capture itself.
- Why: subagents have no
AskUserQuestion(code.claude.com/docs/en/sub-agents), #8 already keeps its gates in the main conversation (issue-8.md d2), and other harnesses lack the tool.
- Logo in the example pack. Default: the logo is optional. The fictional pack ships an SVG wordmark (a text file). With no logo, the end card typesets the name in the heading font. That matches issue-9.md d7's text wordmark description. Why: #9 bans binaries, and frame workers refuse to improvise a mark (VFE/SKILL.md:74-76).
- Fonts by name or URL, and the CI guard. Default:
- Resolve names to downloadable OFL files, and accept local paths and URLs. Every font in brand.md records
license:andsource:. - CI runs
fonts.lock: every committed file whose magic bytes arewOF2,wOFF,OTTO,ttcf,trueor0x00010000(whatever its extension) must appear by sha256 with a license-file path and source URL. Anything unlisted fails. - A local-only test (never in CI, because the files can't be committed) runs the guard against VFE's Gilroy renamed to
Inter-400.woff2beside a copied OFL text, and against the 98 APP commercial files. - Why: a sibling-license rule and a four-hash denylist both miss renamed, re-encoded or other commercial files, and APP already holds a renamed Gilroy. An allowlist fails closed.
- Resolve names to downloadable OFL files, and accept local paths and URLs. Every font in brand.md records
- Repo license (blocks bootstrap). Default: follow issue-1.md d1: AGPL-3.0 for code, CC0 for
fixtures/, and Apache-2.0 notices kept on broadside-derived files. issue-2.md d2 (MIT or Apache-2.0 with contributor OK) is the alternative.- Why: #3 doesn't constrain the choice. Its template and skin are Apache-2.0-derived, and Apache-2.0 flows one way into GPLv3 (apache.org) and, by inference, into AGPL.
- check-copy.mjs, SKILL.md and frame.md were written in the owner's own Claude Code session (transcript d983056c). "No outside contributors" is the owner's assertion, backed by that transcript, not by VCS history.
- One repo-wide decision.
- README media. Default: commit one
snapshots/contact-sheet.jpgper brand, and attach the MP4s to a GitHub release (as in issue-1.md d18). Why: the pipeline already makes contact sheets, and each MP4 is about 9.6 MB. - Naming and packaging. Default: two skills,
brand-pack(capture and validate) andbrand-explainer(video), underskills/<name>/SKILL.mdin the single plugin from issue-1.md d2/d4. Why: five other issues read the pack, and names can't usevwc-*or upstream'sfaceless-explainer. - The VWC fonts already public in APP. Default: raise a separate vets-who-code-app issue and keep it out of #3. Why: it's a licensing exposure outside this epic (98 files; git ls-files), but VWC's pack points at those files. The owner files it.
- Owner decisions before #9 is assigned: the license (decision 14) and the brand.md format (decision 1), posted on #9 by the owner. Verify: #9 has a comment or body edit that names the format and the heading list.
- Bootstrap
main(outside #3, blocking): LICENSE, README stub,.gitignore(brand/,videos/,renders/,.hyperframes/,brag-output*/,assets/fonts/), commitlint. Verify:gh api repos/Vets-Who-Code/hashflag-skills/commitsreturns 200, andgit check-ignore -v brand/fonts/Gilroy-Regular.woff2prints a rule. - Font guard first (
scripts/check-fonts.mjs+fonts.lock), before any font file lands. Verify:- node:test shows an unlisted
wOF2blob namedlogo.binfails and the listed JetBrains Mono passes. - Locally, VFE's Gilroy-Regular copied as
Inter-400.woff2next toOFL-inter.txtfails.
- node:test shows an unlisted
- Back up VFE to a private, versioned place (it isn't in git), leaving out Gilroy and Gotham. Record the hyperframes version.
Verify:
diff -ragainst VFE shows only the excluded fonts, andnode check-copy.mjs --self-checkexits 0 from the copy. - brand.md parser and
validate.mjs. It checks required fields, fonts present and infonts.lockor local-only, and role prediction through upstreamtokens.mjson the generated frame.md. It enforces WCAG 4.5:1 for every declared text/ground pair and writesbrand.json. Verify: node:test shows:- a VWC-shaped brand.md predicts
{ink #1A1823, canvas #EEEDE9, accent #FDB330, accent2 #c5203e}with ground#091f40; - red-on-navy text fails at 2.85:1;
- a missing font fails;
- #9's heading format parses.
- a VWC-shaped brand.md predicts
- Generators: a frame.md template (structure from VFE/brand/frame.md with the broadside Apache notice, and no ground key that matches the canvas pattern) and a caption-skin template (casing, display and mono fallbacks, neutral color fallbacks). Both refuse to overwrite.
Verify: the generated VWC frame.md gives the same
semanticColors/parseFontsas the hand-written file, and a second run without--forceexits non-zero. - Port check-copy to read rules from the pack, with the fixes from decision 6.
Verify:
- All 23 original assertions pass with VWC rules loaded from brand.md.
"Do not wait — sign up today."gives 1 hit.- A tab-indented spoken figure, a curly-quoted figure and a figure in frame HTML are all listed.
post-assemble.mjsandverify-audio.mjs(count check plus duration band, with--min/--maxand--render <mp4>for the ffprobe check). Verify: node:test shows:- a cream
#rootbecomes the pack ground and is unchanged on a second run; audio_metawith 5 voices for 8 lines exits 1;- voices totalling 58 s exit 1 with
--min 60; - RUN's take-2 meta (73.091 s) passes 60-90.
- a cream
- Neutral default pack (decision 10).
Verify: the validator exits 0, every font is in
fonts.lock, and the accent and ground hexes don't match code-editorial's or VWC's. brand-packSKILL.md. Guided capture in the main conversation, a plain-text fallback, a validate-only mode, and a subagent refusal (decision 11). Verify:- Capturing a #9-style fictional brand ends with the validator at exit 0, and
git status --porcelainin the host repo is empty. - Run from a subagent, it asks nothing and exits 1 listing the missing fields.
- Capturing a #9-style fictional brand ends with the validator at exit 0, and
brand-explainerSKILL.md. The generalized override table: staging from the pack, the gate at Steps 3 and 6,--voicefrom the pack, the word-band warning, both new scripts, andrender --output. Verify: re-running the Labor Day script with the VWC pack gives:- check-copy exit 0;
- a clean
hyperframes checkexcept the caption-word false positive; - a contact sheet that opens on navy;
- a navy
#root; verify-audio --renderpassing.
- Fictional pack A (from #9's nonprofit brand.md, OFL fonts, SVG wordmark), rendered with Kokoro and
music: none. Measure Kokoro's pace first. Verify: ffprobe duration is 60-90 s, the gate exits 0,audio_engine_meta.jsonshowstts_providerkokoroand no BGM, andcheck-fontspasses. - Pack B (#9's small-business brand), the same SCRIPT.md, and the README. Verify: both renders come from the identical SCRIPT.md (same sha), both gates pass, both durations are 60-90 s, and the README shows both contact sheets with release links.
- CI with no keys: node:test for the parser, validator, generators, gate, scripts and font guard. Verify: CI is green, and a test PR that adds an unlisted font blob under any extension fails.
Not researched:
- whether
AskUserQuestionworks in headlessclaude -p; - whether HeyGen API or OAuth-allowance usage counts as the "Free Plan";
- the license of HeyGen catalog music (the terms and API docs are silent);
- ElevenLabs output terms;
- kokoro-onnx and Whisper model licenses;
- Kokoro's speaking pace;
- whether blanking env forces Kokoro;
- whether
<break>passes through parseScript and captions; - whether
hyperframes checkcatches a missing font file; - whether
renderfetches GSAP and fonts at render time (VFE/SKILL.md:69's "render machine has no network" is unverified); - HeyGen self-serve prices;
- agent token cost per run;
- faceless-explainer render time;
- HashFlag and Linea font licenses.
- gh (read 2026-09-29):
gh issue view 1,2,3,4,5,6,7,8,9 -R Vets-Who-Code/hashflag-skills;gh api repos/Vets-Who-Code/hashflag-skills;gh api repos/Vets-Who-Code/hashflag-skills/issues/1/sub_issues. - Sibling context docs: issue-1.md, issue-2.md, issue-4.md, issue-7.md, issue-8.md and issue-9.md (#1 d1, d2, d4, d5, d10, d12, d18; #2 d2, d3; #4 d5, d9, d10; #7 d8, d12; #8 d2, d5, d7, d10; #9 d6, d7, d8).
- VFE:
VFE/SKILL.md,VFE/brand/frame.md,VFE/brand/caption-skin.html,VFE/brand/fonts/,VFE/scripts/check-copy.mjs. - FE:
FE/SKILL.md;FE/scripts/build-frame.mjs,audio.mjs,captions.mjs,assemble-index.mjs,transitions.mjs,frame-packets.mjs;FE/scripts/lib/tokens.mjs,dimensions.mjs,pad-frame-duration.mjs,storyboard.mjs. - MU:
MU/audio/scripts/audio.mjs,lib/tts.mjs,lib/heygen.mjs,lib/bgm.mjs,heygen-tts.mjs;MU/audio/references/tts.md,requirements.md;MU/scripts/recipe.mjs,lib/recipe-store.mjs. - HC:
HC/frame-presets/(broadside, code-editorial/FRAME.md, code-editorial/fonts),HC/references/design-picker.md,narration.md,typography.md. - HF and other upstream skills:
HF/references/brief-contract.md,subagent-dispatch.md,frame-worker-core.md,skill-lifecycle.md,routes/faceless-explainer.md;HF/scripts/lib/frame-packets-core.mjs;~/.claude/skills/hyperframes-cli/SKILL.md,references/preview-render.md,cloud.md,doctor-browser.md,upgrade-info-misc.md;~/.claude/skills/product-launch-video/SKILL.md;~/.claude/skills/pr-to-video/scripts/project-dir.mjs,references/story-design.md. - Brand-guidelines skill:
~/.claude/skills/synced/bec712ab-35d6-4816-b44c-bf1b8563fa29_cc083925-4097-4287-80ae-c73bdf7e76d8/brand-guidelines/SKILL.md. - RUN and reel:
RUN/(BRIEF.md, STORYBOARD.md, SCRIPT.md.bak, .narrated-backup/{SCRIPT.md,STORYBOARD.md,audio_meta.json}, audio.log, audio_engine_meta.json, samples/, index.html, package.json, frame.md, renders/, assets/fonts/);APP/videos/vets-who-code-reel/(assets/fonts/, capture/assets/fonts/, capture/extracted/tokens.json, asset-descriptions.md, renders meta.json). - APP:
APP/tailwind.config.js,APP/docs/DESIGN_DOC.md,APP/docs/brand-style-guide.md,APP/AGENTS.md,APP/src/pages/press-kit.tsx,APP/src/pages/api/og.tsx,APP/src/data/site-config.ts,APP/src/data/homepages/index.json,APP/public/llms.txt,APP/scripts/image-prompts.ts,APP/scripts/generate-blog-graphic.ts,APP/public/fonts/,APP/LICENSE(AGPL-3.0),APP/.git/info/exclude; commit 47165207 (#1437). - Preferences:
~/.media/preferences.json. - Commands run 2026-09-29:
node check-copy.mjs --self-check; node probes ofscan/numerals; a count ofa(in check-copy.mjs:117-147;- an awk count of frame.md colors; a node run of
tokens.mjson frame.md; shasumacross VFE, APPpublic/fonts, RUN and the reel;git ls-files public/fonts;ffprobeof the renders and samples; a node sum of.narrated-backup/audio_meta.json;npm view hyperframes version engines.node;statof VFE files.
- URLs (read 2026-09-29):
- https://code.claude.com/docs/en/sub-agents
- https://www.heygen.com/terms (§3, §4, §10; Last Updated 2026-07-23)
- https://developers.heygen.com/docs/voices/speech
- https://developers.heygen.com/background-music
- https://developers.heygen.com/docs/enterprise-pricing.md
- https://developers.heygen.com/docs/usage-limits.md
- https://huggingface.co/hexgrad/Kokoro-82M
- https://www.apache.org/licenses/GPL-compatibility.html
- https://ai.google.dev/gemini-api/docs/pricing
- https://github.com/heygen-com/hyperframes (LICENSE, CREDITS.md, commits 01601d1105, 29fc95395d)
- https://huggingface.co/facebook/musicgen-small
- https://pixabay.com/service/license-summary/
- https://gsap.com/standard-license
