-
Confirm the lib qualifies. Visual/TUI libs only — components, prompts, charts, dashboards, spinners. If the lib is candy-pty, an FFI binding, a parser, or a codec, STOP. Tell the user: "<slug> is matrix-exempt — non-visual primitives don't get VHS demos." Verify by reading <slug>/README.md and checking for screen-output APIs. Verify before proceeding.
-
Author the example script. Create <slug>/examples/<demo>.php if it does not already exist. Top of file: <?php declare(strict_types=1); require __DIR__ . '/../vendor/autoload.php';. The script must self-terminate in under ~3 seconds (no while (true) loops without a break). Run it once locally to confirm it exits cleanly: cd <slug> && php examples/<demo>.php. Verify the script exits with status 0 before proceeding.
-
Create the .tape file at <slug>/.vhs/<demo>.tape. Use this canonical skeleton (mirrors existing tapes under sugar-bits/.vhs/, candy-shine/.vhs/, etc.):
Output <demo>.gif
Set Theme "TokyoNight"
Set FontSize 14
Set Width 800
Set Height 480
Set Padding 20
Type "php examples/<demo>.php"
Enter
Sleep 2s
Output is relative to the .tape file — emit a bare filename, never an absolute path.
- Keep the theme
TokyoNight unless the demo is about theming (then use the theme being demoed).
- For interactive demos, replace
Sleep 2s with a scripted sequence: Sleep 500ms · Type "j" · Sleep 300ms · Enter · Sleep 2s.
- Widen to 1000–1200 ONLY if the demo wraps at 800; never go below 800x480.
Verify the file parses by running vhs <slug>/.vhs/<demo>.tape locally if vhs is on PATH. If not, move on — CI will catch syntax errors.
-
Add the slug to .github/workflows/vhs.yml in the all=(...) bash array. The array is alphabetical within prefix groups (Candy first, then Honey, then Sugar, then super-candy). Read the file, locate all=(, insert <slug> in the correct group. Verify the slug is present before proceeding.
-
Register non-default PHP extensions if the example requires any. Open .github/workflows/vhs.yml, find the step that calls shivammathur/setup-php (or the matrix's extensions: key). The default set typically includes mbstring, intl, tokenizer. Add any of these the lib needs:
ssh2 — ext-ssh2 (used by candy-wish, sugar-skate)
gd — ext-gd (image rendering)
ffi — ext-ffi (FFI bindings — but recall non-visual FFI libs are exempt)
shmop — ext-shmop (shared memory; gate with os: ubuntu-latest if Windows-incompatible)
sockets — ext-sockets (networking demos)
Match the existing format in the file (comma-separated string, e.g. extensions: mbstring, intl, ssh2). Verify the extension is listed before proceeding.
-
Reference the rendered GIF from the lib's README.md under a ## Demo heading using the canonical URL pattern:
## Demo

Skip this step if the README already has a demo block and you're only adding a secondary tape. Verify the README still renders (preview the markdown).
-
Commit the new files. Stage <slug>/.vhs/<demo>.tape, <slug>/examples/<demo>.php, .github/workflows/vhs.yml, and (if touched) <slug>/README.md. Do NOT stage a .gif — CI generates it on push. Confirm pre-commit hook handles the Caliber refresh (per CLAUDE.md). The first push triggers .github/workflows/vhs.yml; the resulting .gif lands in a follow-up commit by the CI bot.
-
"Workflow ran but no GIF appeared." The lib is missing from the all=(...) array in .github/workflows/vhs.yml. Re-read the file, confirm the slug literal is in the array, push a fix commit.
-
"GIF rendered but is empty / blank screen." The examples/<demo>.php script is missing, throws an uncaught exception, or doesn't produce visible output before Sleep 2s elapses. Run the script locally: cd <slug> && php examples/<demo>.php — fix any errors, increase Sleep if the script needs more time, or add an explicit final render call.
-
"Workflow fails with extension not found or Class \"SSH2\" not found." The required PHP extension is missing from .github/workflows/vhs.yml's extensions: key. Add the bare extension name (e.g. ssh2, not ext-ssh2) to the comma-separated list.
-
"VHS errors unknown command "Set Themes"." Tape grammar is case-sensitive and singular: Set Theme, not Set Themes; Type, not type; Enter, not enter. Re-check the tape against the canonical skeleton in step 3.
-
"GIF renders but text is cut off on the right." Width too small. Bump Set Width 800 to 1000 or 1200. Keep height proportional (Set Height ~ 60% of width).
-
"composer validate --strict fails after my edits." Unrelated — --strict flags every "sugarcraft/*": "@dev" path-repo. Drop --strict (per CLAUDE.md Gotchas).
-
"Tape runs forever / GIF is 10MB." The example script has an infinite loop or hangs on stdin. Either add a hard timeout inside the script (pcntl_alarm(3)), or have the script auto-exit after N frames. Don't try to fix this with a shorter Sleep — the tape will still record the hung process up to the sleep duration.
-
"User asked for a VHS demo on candy-pty / an FFI binding." Refuse politely: "candy-pty is matrix-exempt — it's a non-visual primitive (PTY allocator). VHS demos are reserved for libs with TUI output. Add a ## Why no demo note to its README if needed."