mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-08-31 02:41:44 +00:00
Packages Jake Moran's changelog-video pipeline (v1, validated end-to-end
by Home on the Jun 23-29 range) as a repo-native skill set that Claude
Code (.claude/skills/) and Codex CLI (.agents/skills/) auto-discover the
moment the repo is opened. No install step; run the skill against a
changelog markdown for a given git range and it produces a lint-clean,
seam-gate-green 1080x1080 MP4 (~45-60s, Annie VO, mock-UI visualizations,
caption rail) end-to-end.
Six skills added byte-identical in both mirror dirs:
- changelog-video (pipeline entry point)
- motion-doctrine (carries seam-stamp.mjs + seam-gate.mjs)
- cut-the-curve, captions-overlay, seam-craft, oversized-cursor
Layout:
- .claude/skills/ - Claude Code project-local auto-discover
- .agents/skills/ - Codex CLI project-local auto-discover (verified via
Magi's clean-home Codex 0.144.3 repro; NOT .codex/skills/)
Fonts, animated background (12 MB), house BGM (5 MB), lexicon, and
align-captions ship inside the skill dirs. .gitattributes routes only
.claude/skills/**/*.{mp4,mp3} + .agents/skills/**/*.{mp4,mp3} through
LFS — narrowly scoped so unrelated Player, Studio, registry, and
marketplace media stay put. HeyGen CLI auth is the one credential the
skill needs; Node >= 22, ffmpeg, and headless Chrome are documented
alongside in both READMEs.
.gitignore: rewrites .claude/ and .agents/ blocks to keep agent-installed
skill hygiene while re-including the six repo-native skill dirs plus
README.md.
CI:
- Extends changes.skills filter to match .claude/skills/**,
.agents/skills/**, scripts/lint-skills.ts, and scripts/check-skill-mirror.mjs.
- New 'Skills: project-native lint + mirror' job runs the extended
lint-skills.ts (schema-driven; required { name, description } + optional
{ license, allowed-tools, metadata }, name pattern check, description
length check) plus a new check-skill-mirror.mjs byte-integrity script
(24 mirrored files must match; README.md deliberately per-CLI).
- Wired into 'bun run lint' locally.
Frontmatter validator:
- Rejects unsupported top-level keys (catches category:-style drift).
- Requires name + description.
- Validates name pattern (^[a-z][a-z0-9-]{0,63}$) and description shape
(non-empty, <=1024 chars).
- Missing frontmatter block itself is a first-class error.
Also strips unsupported top-level 'category:' frontmatter from Jake's
motion-doctrine and cut-the-curve SKILL.mds (both mirrors), rewrites the
TTS invocation from ~/.claude/skills/media-use/... to the tracked
skills/hyperframes-media/scripts/heygen-tts.mjs, swaps npx hyperframes@latest
for the repo-local CLI in the gate step, and fixes a lint issue in Jake's
seam-gate.mjs (ternary-for-side-effect -> if/else).
Validated end-to-end by Home on Jun 23-29 (MP4 posted in C0ACCNHLG3U
thread 1784181166.041319). Independently reviewed R1/R2/R3 by Magi.
Co-authored-by: Jake Moran <jake@heygen.com>
128 lines
5.0 KiB
JavaScript
128 lines
5.0 KiB
JavaScript
// Positive / negative fixture tests for the SKILL.md frontmatter drift guard
|
|
// in scripts/lint-skills.ts. Runs the exported `lintFrontmatter` against known
|
|
// inputs and asserts the violation set matches expectation.
|
|
//
|
|
// Kept in .mjs (not .ts) so `node --test` can execute it via the same runner
|
|
// the rest of scripts/*.test.mjs use, without needing tsx. `bun scripts/…`
|
|
// runs .ts directly at lint-time; tests import the compiled export via tsx.
|
|
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { lintFrontmatter } from "./lint-skills.ts";
|
|
|
|
const wrap = (frontmatter) => `---\n${frontmatter}\n---\n\n# body\n`;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Positive fixtures — must pass with zero violations
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("valid: bare required keys", () => {
|
|
const violations = lintFrontmatter(wrap("name: foo\ndescription: bar"));
|
|
assert.deepEqual(violations, []);
|
|
});
|
|
|
|
test("valid: with license (optional string)", () => {
|
|
const violations = lintFrontmatter(wrap("name: foo\ndescription: bar\nlicense: MIT"));
|
|
assert.deepEqual(violations, []);
|
|
});
|
|
|
|
test("valid: allowed-tools as YAML sequence", () => {
|
|
const violations = lintFrontmatter(
|
|
wrap("name: foo\ndescription: bar\nallowed-tools:\n - Bash\n - Read"),
|
|
);
|
|
assert.deepEqual(violations, []);
|
|
});
|
|
|
|
test("valid: allowed-tools as single string", () => {
|
|
const violations = lintFrontmatter(wrap('name: foo\ndescription: bar\nallowed-tools: "Bash"'));
|
|
assert.deepEqual(violations, []);
|
|
});
|
|
|
|
test("valid: metadata as nested mapping", () => {
|
|
const violations = lintFrontmatter(
|
|
wrap("name: foo\ndescription: bar\nmetadata:\n version: 1\n tags:\n - a\n - b"),
|
|
);
|
|
assert.deepEqual(violations, []);
|
|
});
|
|
|
|
test("valid: multi-line description via block scalar", () => {
|
|
const violations = lintFrontmatter(
|
|
wrap("name: foo\ndescription: |\n Multi\n line\n description"),
|
|
);
|
|
assert.deepEqual(violations, []);
|
|
});
|
|
|
|
test("valid: description with a colon inside a quoted string", () => {
|
|
const violations = lintFrontmatter(wrap('name: foo\ndescription: "read: file, then: write"'));
|
|
assert.deepEqual(violations, []);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Negative fixtures — must produce at least one matching violation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const has = (violations, needle) =>
|
|
violations.some((v) => v.message.toLowerCase().includes(needle.toLowerCase()));
|
|
|
|
test("invalid: missing frontmatter block", () => {
|
|
const violations = lintFrontmatter("# just a body, no dashes\n");
|
|
assert.ok(has(violations, "Missing SKILL.md YAML frontmatter"));
|
|
});
|
|
|
|
test("invalid: missing name", () => {
|
|
const violations = lintFrontmatter(wrap("description: bar"));
|
|
assert.ok(has(violations, `Missing required frontmatter key "name"`));
|
|
});
|
|
|
|
test("invalid: missing description", () => {
|
|
const violations = lintFrontmatter(wrap("name: foo"));
|
|
assert.ok(has(violations, `Missing required frontmatter key "description"`));
|
|
});
|
|
|
|
test("invalid: unsupported key (the 'category' drift case)", () => {
|
|
const violations = lintFrontmatter(wrap("name: foo\ndescription: bar\ncategory: motion"));
|
|
assert.ok(has(violations, `Unsupported frontmatter key "category"`));
|
|
});
|
|
|
|
test("invalid: name as a list (was silently accepted by the pre-YAML version)", () => {
|
|
const violations = lintFrontmatter(wrap("name: [a, b]\ndescription: bar"));
|
|
assert.ok(has(violations, `"name" must be a string`));
|
|
});
|
|
|
|
test("invalid: description as a number", () => {
|
|
const violations = lintFrontmatter(wrap("name: foo\ndescription: 42"));
|
|
assert.ok(has(violations, `"description" must be a string`));
|
|
});
|
|
|
|
test("invalid: empty description string", () => {
|
|
const violations = lintFrontmatter(wrap('name: foo\ndescription: ""'));
|
|
assert.ok(has(violations, `"description" must not be empty`));
|
|
});
|
|
|
|
test("invalid: allowed-tools as a mapping (must be sequence or string)", () => {
|
|
const violations = lintFrontmatter(
|
|
wrap("name: foo\ndescription: bar\nallowed-tools:\n Bash: true"),
|
|
);
|
|
assert.ok(has(violations, `"allowed-tools" must be a string or a list of strings`));
|
|
});
|
|
|
|
test("invalid: metadata as a scalar (must be a mapping)", () => {
|
|
const violations = lintFrontmatter(
|
|
wrap('name: foo\ndescription: bar\nmetadata: "just a string"'),
|
|
);
|
|
assert.ok(has(violations, `"metadata" must be a mapping`));
|
|
});
|
|
|
|
test("invalid: malformed YAML (unmatched brace)", () => {
|
|
const violations = lintFrontmatter(wrap("name: foo\ndescription: {"));
|
|
assert.ok(has(violations, `Malformed YAML frontmatter`));
|
|
});
|
|
|
|
test("invalid: top-level scalar (frontmatter is not a mapping)", () => {
|
|
const violations = lintFrontmatter("---\njust-a-string\n---\n\nbody");
|
|
// Either parse succeeds and the top-level check catches it, or the parser
|
|
// errors — either is an acceptable rejection, but the violation list must
|
|
// be non-empty.
|
|
assert.ok(violations.length > 0);
|
|
});
|