Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Atomic capabilities the creation workflows compose against — pull one when you
- `/media-use` — the media OS: resolve any media need (BGM, SFX, image, icon, logo, voice, color grade, LUT) into a frozen local file or paste-ready block + ledger record; generate via TTS / music / image models when the catalog misses; transcribe, caption, remove backgrounds, and reuse assets across projects. One shared `scripts/audio.mjs` engine + manifest tracking; keeps search noise on disk.
- `/hyperframes-audio` — mix the audio already placed in a composition: voiceover carve (dip a music bed only in the bands the voice occupies, static or dynamic, level match included), the effect chain (EQ, compressor, limiter, gate, saturation, delay, reverb, chorus, phaser, bitcrush), automation envelopes on volume or any effect parameter, and submix buses (`<hf-audio-group>`) that carry one chain, fader and automation clock for several tracks at once. Sourcing the audio is `/media-use`; this is what happens to it afterwards.
- `/hyperframes-cli` — CLI dev loop: `init`, `add`, `lint`, `check`, `snapshot`, `preview`, `render`, `publish`, `doctor`, `lambda` (AWS Lambda cloud rendering).
- `/hyperframes-registry` — install and wire registry blocks and components into compositions via `hyperframes add`. Covers authoring a new block or component to contribute upstream.
- `/hyperframes-registry` — search, install and wire registry blocks and components into compositions via `hyperframes catalog` / `hyperframes add`. Load it before hand-building any named look, effect, treatment or transition: the search ranks the whole hosted registry with nothing installed. Covers authoring a new block or component to contribute upstream.
- `/figma` — import Figma assets, tokens, components, and storyboard sections → reconstructed motion (frames read as states, not slides) (REST/CLI) plus Motion animations (MCP) and shaders (MCP source / native export) into a composition.

## Skill catalog maintenance
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ Atomic capabilities the creation workflows compose against — pull one when you
| `/media-use` | The media OS — resolve any media need (BGM, SFX, image, icon, logo, voice, color grade, LUT) into a frozen local file or paste-ready block + ledger record, generate via TTS/music/image models when the catalog misses, transcribe, caption, remove backgrounds, and reuse assets across projects. One shared audio engine + manifest tracking. |
| `/hyperframes-cli` | CLI dev loop — `init`, `lint`, `check`, `snapshot`, `preview`, `render`, `publish`, `doctor`, plus HeyGen-hosted cloud rendering (`cloud render`) and AWS Lambda rendering (`lambda deploy / render / progress`). |
| `/hyperframes-audio` | Mix the audio already placed in a composition — voiceover carve (dip a music bed only in the bands the voice occupies, static or dynamic, level match included), the effect chain (EQ, compressor, limiter, gate, saturation, delay, reverb, chorus, phaser, bitcrush), automation envelopes on volume or any effect parameter, and submix buses (`<hf-audio-group>`) carrying one chain, fader and automation clock for several tracks at once. Sourcing the audio is `/media-use`. |
| `/hyperframes-registry` | Install and wire registry blocks and components into compositions via `hyperframes add`. Authoring a new block or component to contribute upstream. |
| `/hyperframes-registry` | Search, install and wire registry blocks and components into compositions via `hyperframes catalog` / `hyperframes add`. Load before hand-building any named look, effect, treatment or transition. Authoring a new block or component to contribute upstream. |
| `/figma` | Import Figma assets, tokens, components, and storyboard sections → reconstructed motion (frames read as states, not slides) (REST/CLI) plus Motion animations (MCP) and shaders (MCP source / native export) into a composition. |

For visual design handoff workflows, see the [Claude Design guide](https://hyperframes.heygen.com/guides/claude-design) and [Open Design guide](https://hyperframes.heygen.com/guides/open-design).
Expand Down
2 changes: 1 addition & 1 deletion docs/prompting/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ The installer shows a picker. Select the **core skills** below — every project
| `/hyperframes-creative` | Creative direction — design spec, palettes, typography, narration, beats |
| `/hyperframes-cli` | Dev-loop CLI — `init`, `lint`, `check`, `preview`, `render`, `doctor` |
| `/media-use` | Media OS — TTS voiceover (`tts`), `transcribe`, `remove-background`, plus BGM / SFX / image resolution |
| `/hyperframes-registry` | Block and component installation via `hyperframes add` |
| `/hyperframes-registry` | Search the catalog before hand-building a look; install via `hyperframes add` |
| `/hyperframes-keyframes`| Seek-safe keyframe authoring across runtimes, plus `hyperframes keyframes` diagnostics |
| `/general-video` | The general authoring workflow — multi-scene pieces, reels, montages, remixes, and the home of **companion mode**; the fallback when no workflow below fits |

Expand Down
67 changes: 67 additions & 0 deletions packages/cli/src/commands/coreSkillContent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,73 @@ describe("media treatment routing documentation", () => {
}
});

it("makes the motion-graphics build path run a live catalog search, not read a snapshot", () => {
// The failure this pins: a user inside /motion-graphics asked for "CRT scanlines
// and glitch effects" and the agent hand-authored both, while `caption-glitch-rgb`
// ("RGB chromatic aberration with CRT scanline overlay") ranks first for that exact
// query on either tier. Every reuse instruction in the workflow pointed at
// catalog-map.md, a hand-maintained snapshot, and none named the search. The search
// needs nothing installed, so "I forgot to install the components" was never the cause.
for (const file of [
["skills", "motion-graphics", "catalog-map.md"],
["skills", "motion-graphics", "agents", "director.md"],
["skills", "motion-graphics", "agents", "builder.md"],
]) {
expect(read(...file)).toContain("npx hyperframes catalog --query");
}
// And it must say the search stands alone, or the next reader re-derives the
// creator's wrong diagnosis: that a catalog you have not installed cannot be searched.
expect(read("skills", "motion-graphics", "catalog-map.md")).toContain(
"needs nothing installed",
);
});

it("routes every authoring workflow through the live catalog search, or documents why not", () => {
// The same failure one layer up. The search instruction lived only in
// hyperframes-cli and hyperframes-registry, both loaded on demand, and the
// registry skill's own trigger named the command ("use when running
// hyperframes catalog") — circular, because the agent that never thought to
// search could not reach the doc telling it to search. All ten workflow
// skills carried zero mentions of the command.
for (const file of [
["skills", "motion-graphics", "agents", "director.md"],
["skills", "product-launch-video", "SKILL.md"],
["skills", "faceless-explainer", "SKILL.md"],
["skills", "pr-to-video", "SKILL.md"],
["skills", "music-to-video", "SKILL.md"],
["skills", "general-video", "SKILL.md"],
["skills", "slideshow", "SKILL.md"],
["skills", "remotion-to-hyperframes", "SKILL.md"],
]) {
const doc = read(...file);
expect(doc, file.join("/")).toContain("npx hyperframes catalog --query");
// "I forgot to install the components" was the wrong self-diagnosis that
// hid this bug. Every copy of the instruction has to kill it on the spot.
expect(doc, file.join("/")).toContain("nothing installed");
}

// The two workflows that deliberately do NOT search. Both compile their
// output through a closed authoring vocabulary (embedded-captions' locked
// caption engines, talking-head-recut's `data-anim` card kinds), and a
// registry item is a standalone composition with no place to mount. The
// exemption is written into each skill so the next reader does not close
// the gap with an instruction that would be false there.
for (const file of [
["skills", "embedded-captions", "SKILL.md"],
["skills", "talking-head-recut", "SKILL.md"],
]) {
expect(read(...file), file.join("/")).toContain(
"does not search the HyperFrames component registry",
);
}

// The symptom-triggered description is the other half of the fix: the skill
// has to be reachable from the user naming an effect, not from the command.
const registrySkill = read("skills", "hyperframes-registry", "SKILL.md");
expect(registrySkill).toContain("Use BEFORE hand-building any named visual");
expect(registrySkill).toContain("CRT scanlines");
});

it("gives agents a process-owned preview lifecycle in new project instructions", () => {
for (const file of ["AGENTS.md", "CLAUDE.md"]) {
const template = read("packages", "cli", "src", "templates", "_shared", file);
Expand Down
57 changes: 56 additions & 1 deletion scripts/lint-skills.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import test from "node:test";
import assert from "node:assert/strict";
import { lintFrontmatter } from "./lint-skills.ts";
import { lintFrontmatter, lintRegistryItemRefs } from "./lint-skills.ts";

const wrap = (frontmatter) => `---\n${frontmatter}\n---\n\n# body\n`;

Expand Down Expand Up @@ -125,3 +125,58 @@ test("invalid: top-level scalar (frontmatter is not a mapping)", () => {
// be non-empty.
assert.ok(violations.length > 0);
});

// ---------------------------------------------------------------------------
// Registry-snapshot drift guard
// ---------------------------------------------------------------------------

const KNOWN = new Set(["caption-glitch-rgb", "code-diff", "data-chart"]);
const MARKER = "<!-- registry-items: -->";

test("registry refs: unmarked file is never checked", () => {
// Opt-in is the whole design. Most kebab-case backticks in skill docs are CSS
// properties, data-* attributes or skill directory names, and a check that
// flags those gets switched off. null (not []) distinguishes "not a snapshot"
// from "a snapshot with nothing wrong", which is what the counter reports.
assert.equal(lintRegistryItemRefs("Use `not-a-real-item` here.\n", KNOWN), null);
});

test("registry refs: a marker inside a fenced block does not arm the check", () => {
// Otherwise a doc that documents this marker's own syntax arms the check on
// itself, and every identifier in it starts failing for no stated reason.
const doc = ["# Doc", "", "```md", MARKER, "```", "", "Use `not-a-real-item`."].join("\n");
assert.equal(lintRegistryItemRefs(doc, KNOWN), null);
});

test("registry refs: marked file passes when every id is real", () => {
const doc = `${MARKER}\n\nUse \`caption-glitch-rgb\` or \`code-diff\`.\n`;
assert.deepEqual(lintRegistryItemRefs(doc, KNOWN), []);
});

test("registry refs: marked file flags an id the registry does not have", () => {
const doc = `${MARKER}\n\nInstall \`text-wave-distort\` for the wobble.\n`;
const violations = lintRegistryItemRefs(doc, KNOWN);
assert.equal(violations.length, 1);
assert.equal(violations[0].line, 3);
assert.ok(violations[0].message.includes("text-wave-distort"));
});

test("registry refs: allow= exempts a legitimately non-item identifier", () => {
const doc = `<!-- registry-items: allow=dark-plus,pin-rollout -->\n\n\`dark-plus\` and \`pin-rollout\`.\n`;
assert.deepEqual(lintRegistryItemRefs(doc, KNOWN), []);
});

test("registry refs: non-id backticks are ignored", () => {
const doc = `${MARKER}\n\n\`--json\`, \`Foo-Bar\`, \`a b-c\`, \`UPPER-CASE\`.\n`;
assert.deepEqual(lintRegistryItemRefs(doc, KNOWN), []);
});

test("registry refs: single-word ids are a KNOWN blind spot, not an accident", () => {
// The pattern requires a hyphen, so real single-word registry items (glitch,
// flowchart, typewriter, confetti, separator, vignette, vignelli) are never
// checked. Pinned here so the tradeoff is visible in code, not just in a
// comment: dropping the hyphen would cost 46 allow= entries of prose nouns
// across the marked files to monitor 3 more items. See lint-skills.ts header.
const doc = `${MARKER}\n\n\`glitch\` was renamed and this doc was not updated.\n`;
assert.deepEqual(lintRegistryItemRefs(doc, KNOWN), []);
});
99 changes: 95 additions & 4 deletions scripts/lint-skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import { readFileSync, readdirSync, statSync } from "node:fs";
import { join, relative } from "node:path";
import { parse as parseYaml, YAMLParseError } from "yaml";
import type { RegistryManifest } from "../packages/core/src/index.js";

const REPO_ROOT = join(import.meta.dirname, "..");
// Every location that ships SKILL.md files gets linted. `skills/` is the
Expand Down Expand Up @@ -256,6 +257,79 @@ function lintInlinePatterns(file: string, stripped: string): Violation[] {
.flatMap((line, i) => (line ? matchDangerousPatterns(file, line, i + 1) : []));
}

// ---------------------------------------------------------------------------
// Registry-item references in hand-maintained snapshots
// ---------------------------------------------------------------------------
//
// A skill doc that snapshots part of the component registry rots in silence:
// nothing fails when an item is renamed or dropped, and the agent that follows
// the doc runs `hyperframes add <gone>` and dies. A doc opts into this check
// with a marker line, after which every registry-item-shaped identifier in a
// backtick span must name a real item in registry/registry.json:
//
// <!-- registry-items: allow=some-suffix,another-suffix -->
//
// Opt-in rather than repo-wide on purpose. Kebab-case backticks are also CSS
// properties, `data-*` attributes, skill directory names, script names, and
// motion-graphics category names, and a check that flags those is a check
// people turn off. `allow=` carries the few non-item identifiers a snapshot
// legitimately names (bare suffixes under a spelled-out prefix, ids the doc
// itself marks as hand-authored).
//
// TWO KNOWN BLIND SPOTS, both deliberate, both false NEGATIVES (this check
// never invents a violation, it only misses some):
//
// 1. Identifiers outside a backtick span are not seen. A bare `bar-chart-race`
// in prose slipped past this check while it was a live defect elsewhere.
// 2. Single-word item names are not seen, because the pattern below requires a
// hyphen. Measured on the six currently-marked files: dropping the hyphen
// requirement would monitor 3 more real items (`glitch`, `flowchart`,
// `typewriter`) and force 46 new allow= entries for ordinary prose words
// ("add", "line", "name", "height", "text"). A 15:1 noise ratio is how a
// check gets switched off, so the hyphen requirement stays.
const REGISTRY_MARKER = /<!--\s*registry-items:\s*(?:allow=([^\s]*))?\s*-->/;
const REGISTRY_ITEM_ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)+$/;

function registryItemNames(): Set<string> {
const raw = readFileSync(join(REPO_ROOT, "registry", "registry.json"), "utf-8");
// The cast describes the file; it does not validate it. The runtime filter and
// the throw below are what actually stop us linting against an empty set.
const parsed = JSON.parse(raw) as RegistryManifest;
const names = (parsed.items ?? []).map((item) => item.name).filter((name) => Boolean(name));
if (names.length === 0) {
throw new Error("registry/registry.json parsed to zero item names — refusing to lint blind.");
}
return new Set(names);
}

/** `null` when the file is not marked as a registry snapshot; otherwise its violations. */
export function lintRegistryItemRefs(content: string, known: Set<string>): LineViolation[] | null {
// Marker detection ignores fenced blocks so a doc that *documents* the marker
// syntax in an example does not arm the check on itself. The scan below still
// reads full content, so ids inside fenced examples stay covered.
const marker = stripFencedBlocks(content).match(REGISTRY_MARKER);
if (!marker) return null;
const allowed = new Set((marker[1] ?? "").split(",").filter(Boolean));
return content.split("\n").flatMap((line, index) => {
const dead = [...new Set([...line.matchAll(/`([^`\n]+)`/g)].map((m) => (m[1] ?? "").trim()))]
.filter((token) => REGISTRY_ITEM_ID.test(token))
.filter((token) => !known.has(token) && !allowed.has(token));
return dead.map((token) =>
violation(
index + 1,
`"${token}" is not an item in registry/registry.json, but this file is marked as a registry snapshot. Correct the name, remove it, or add it to the marker's allow= list if it is legitimately not an item.`,
line.trim(),
),
);
});
}

function collectMarkdownFiles(dir: string): string[] {
return readdirSync(dir, { withFileTypes: true, recursive: true })
.filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
.map((entry) => join(entry.parentPath, entry.name));
}

function lintFile(filePath: string): Violation[] {
const raw = readFileSync(filePath, "utf-8");
const file = relative(process.cwd(), filePath);
Expand All @@ -281,18 +355,35 @@ if (files.length === 0) {

let totalViolations = 0;

for (const file of files) {
const violations = lintFile(file);
function report(file: string, violations: LineViolation[]): void {
for (const v of violations) {
console.error(`${v.file}:${v.line}: ${v.message}`);
console.error(`${file}:${v.line}: ${v.message}`);
console.error(` ${v.text}\n`);
totalViolations++;
}
}

for (const file of files) {
report(relative(process.cwd(), file), lintFile(file));
}

const knownItems = registryItemNames();
let snapshotsChecked = 0;
for (const dir of SKILLS_DIRS) {
if (!statSync(dir, { throwIfNoEntry: false })?.isDirectory()) continue;
for (const path of collectMarkdownFiles(dir)) {
const found = lintRegistryItemRefs(readFileSync(path, "utf-8"), knownItems);
if (found === null) continue;
snapshotsChecked++;
report(relative(process.cwd(), path), found);
}
}

if (totalViolations > 0) {
console.error(`\n${totalViolations} skill lint error(s) found.`);
process.exit(1);
} else {
console.log(`Checked ${files.length} skill file(s) — no issues found.`);
console.log(
`Checked ${files.length} skill file(s) and ${snapshotsChecked} registry snapshot(s) against ${knownItems.size} registry items — no issues found.`,
);
}
Loading
Loading