diff --git a/skills-manifest.json b/skills-manifest.json index e529b7607..11db85e55 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -26,7 +26,7 @@ "files": 121 }, "hyperframes-cli": { - "hash": "42b621ca580541b6", + "hash": "3eab4c5fc1ae8e26", "files": 11 }, "hyperframes-core": { @@ -50,7 +50,7 @@ "files": 152 }, "motion-graphics": { - "hash": "0212a19069119e72", + "hash": "50db172cad89b1c7", "files": 23 }, "music-to-video": { @@ -62,8 +62,8 @@ "files": 30 }, "product-launch-video": { - "hash": "a0f6b1f4c8131ed2", - "files": 27 + "hash": "01fc75da8492f749", + "files": 28 }, "remotion-to-hyperframes": { "hash": "3a0e6c2affb9f74e", diff --git a/skills/hyperframes-cli/references/init-and-scaffold.md b/skills/hyperframes-cli/references/init-and-scaffold.md index 9b2e0a868..f51f11eec 100644 --- a/skills/hyperframes-cli/references/init-and-scaffold.md +++ b/skills/hyperframes-cli/references/init-and-scaffold.md @@ -37,12 +37,32 @@ npx hyperframes capture https://stripe.com # scaffold from a we npx hyperframes capture https://linear.app -o linear-video # custom output directory npx hyperframes capture https://example.com --json # JSON output for agents npx hyperframes capture https://example.com --skip-assets # skip image/SVG download +npx hyperframes capture https://example.com --skip-vision # skip optional AI captions npx hyperframes capture https://example.com --max-screenshots 12 npx hyperframes capture https://example.com --timeout 60000 # page-load timeout in ms +npx hyperframes capture https://example.com --capture-budget 90000 # post-navigation budget ``` Captures a live URL as an editable HyperFrames project: screenshots become layered scenes, assets are downloaded locally, and the result is a normal project you can `lint` / `preview` / `render`. Use this when the user supplies a URL as the starting point for a video. +`--timeout` bounds page navigation; `--capture-budget` is the separate cooperative budget for work +after navigation (fonts, assets, vision, and contact sheets). The latter is not a hard wall-clock +watchdog and cannot interrupt native work already in flight. An outer caller deadline is therefore a +third, distinct timeout. An outer caller timeout leaves the capture result unknown; it does not prove +HyperFrames hung or that the navigation timeout should be increased. Preserve the last phase and +classify the boundary that fired. `--skip-vision` disables only optional AI image captioning. + +For agents, use `--json`. The result includes `ok`, warnings, and `lastPhase`. The command also emits +stable `HYPERFRAMES_CAPTURE_PHASE` records so a watchdog can report the last started, completed, or +degraded phase without retaining sensitive payloads. + +Treat a non-zero exit, JSON `ok: false`, or an output `BLOCKED.md` as a **hard stop**. Do not render, +build, or infer brand/design data from partial files in a blocked capture. A successful capture may +degrade an optional phase within budget, but its structural output still has to satisfy the owning +workflow's gate. Exit zero and file existence alone are not semantic success: require the current +invocation's JSON `ok: true`, no `BLOCKED.md`, and artifacts usable for that workflow. Run each retry +into a fresh output directory; never merge or reuse a blocked attempt's partial output. + ## skills ```bash diff --git a/skills/motion-graphics/categories/webpage/module.md b/skills/motion-graphics/categories/webpage/module.md index 666dd2d95..040a321a8 100644 --- a/skills/motion-graphics/categories/webpage/module.md +++ b/skills/motion-graphics/categories/webpage/module.md @@ -4,7 +4,16 @@ ## Source (Step 2) -Fetch the page via `hyperframes capture` (DOM + screenshots) or a provided screenshot → frozen project-local image(s)/DOM. `asset_needs`: `{ kind: web, source: , treatment: none }`. +Fetch the page via `hyperframes capture --json` (DOM + screenshots) or a provided screenshot → frozen project-local image(s)/DOM. `asset_needs`: `{ kind: web, source: , treatment: none }`. + +Inspect the capture result before building. If `BLOCKED.md` exists, JSON reports `ok: false`, or the +command exits non-zero, stop the URL-capture path and report the reason. Continue with a provided +screenshot only when it was an explicit source from the user (or the user explicitly chooses it +after the failure). Do not animate a blocked partial capture, invent missing DOM, or eyeball element +coordinates from protection/challenge pages. + +This web need cannot use the generic asset-free fallback: it requires a usable captured screenshot +or DOM, or a provided screenshot explicitly selected by the user. If none is available, stop. ## Vocabulary / leans on diff --git a/skills/product-launch-video/SKILL.md b/skills/product-launch-video/SKILL.md index 074a12d7c..1fd786de9 100644 --- a/skills/product-launch-video/SKILL.md +++ b/skills/product-launch-video/SKILL.md @@ -52,15 +52,35 @@ Goal: Collect the source material, brand signals, and usable assets for the vide Classify the input and choose the path. Explicit URL -> capture it and use the site for narration and assets. Pasted script/brief -> save verbatim as `user_script.txt`; `VO_MODE` (verbatim or restructured) comes from `BRIEF.md` — the intent layer asks it when a script arrives (ask once here only if the brief somehow lacks it). Then resolve capture target: URL in text -> use it; brand name only -> `WebSearch`, confirm URL in one line, then crawl; no URL/site (or the brief says don't scrape) -> no-capture path. -Run capture with: `npx hyperframes capture "" -o ./capture` +Run capture with: `npx hyperframes capture "" -o ./capture --json`. Keep the default +post-navigation budget unless the caller owns a smaller deadline; then pass a positive +`--capture-budget ` that leaves time for downstream work. `--timeout` controls page +navigation only. Use `--skip-vision` only when optional image captioning is intentionally disabled. -For a site tour or show-it-as-is brief, the captured page is the visual source of truth. Use the real screenshot instead of rebuilding the full website in HTML. If the shot needs internal movement, keep the screenshot as the base and overlay real captured assets at measured positions, or rebuild only the one component that moves. For a scroll shot, animate the viewport over `capture/screenshots/full-page.png` — the 1x plate of the whole document, pixel-exact for a 1920-wide viewport travelling down it. It is absent when the page was too tall to capture in one piece; fall back to the overlapping scroll-position shots in the same directory. Pushing in past 1:1 wants its own 2x capture of that region instead, since the plate has no headroom above 1x. Recreate the whole page only when the user explicitly asks for a stylized interpretation or the capture is unusable. +Inspect the command result and output directory immediately. A non-zero exit, JSON `ok: false`, or +`capture/BLOCKED.md` is a **hard stop** for the capture path: report the recorded reason and do not +consume partial screenshots, DOM, tokens, or assets. Do not manufacture a synthetic no-capture +fallback after a failed URL capture. Continue through the no-capture path only when the original +brief supplied the source material, or when the user explicitly switches to a provided screenshot +or brief after the failure. + +Warnings such as `very little text content` together with an empty asset catalog are not proof of a +usable page. For a site tour or show-it-as-is brief, require trustworthy captured structure or a +provided screenshot; if neither exists, stop. Do not invent or rebuild the page merely because the +capture is unusable. + +For a site tour or show-it-as-is brief, the captured page is the visual source of truth. Use the real screenshot instead of rebuilding the full website in HTML. If the shot needs internal movement, keep the screenshot as the base and overlay real captured assets at measured positions, or rebuild only the one component that moves. For a scroll shot, animate the viewport over `capture/screenshots/full-page.png` — the 1x plate of the whole document, pixel-exact for a 1920-wide viewport travelling down it. It is absent when the page was too tall to capture in one piece; fall back to the overlapping scroll-position shots in the same directory. Pushing in past 1:1 wants its own 2x capture of that region instead, since the plate has no headroom above 1x. Recreate the whole page only when the user explicitly asks for a stylized interpretation; an unusable capture alone is not authorization. If `GEMINI_API_KEY`, `GOOGLE_API_KEY`, or an OpenRouter key exists, capture auto-captions assets into `capture/extracted/asset-descriptions.md`. This is not a review gate. Without a vision key, use DOM context and continue. No-capture path: create `capture/extracted/tokens.json`, `capture/extracted/visible-text.txt`, `capture/extracted/asset-descriptions.md`, and `capture/assets/` by hand. `tokens.json` should be `{ "title": "", "description": "", "colors": [], "fonts": [] }`; fill title/description from the brief when possible. `visible-text.txt` contains the full brief or script. `asset-descriptions.md` should say no assets were captured unless the user gave asset notes. -**Gate:** `capture/extracted/tokens.json`, `capture/extracted/visible-text.txt`, `capture/extracted/asset-descriptions.md`, and `capture/assets/` exist; you can state the brand in one clear sentence. Treat `asset-descriptions.md` as the main asset inventory. If it is missing after real capture, stop and report capture incomplete. If `capture/BLOCKED.md` exists, follow it. +**Gate:** capture JSON reported `ok: true`; `capture/BLOCKED.md` does not exist; +`capture/extracted/tokens.json`, `capture/extracted/visible-text.txt`, +`capture/extracted/asset-descriptions.md`, and `capture/assets/` exist; and you can state the brand in +one clear sentence. Treat `asset-descriptions.md` as the main asset inventory. If it is missing after +real capture, stop and report capture incomplete. Warnings about a degraded optional phase are +acceptable only when this structural gate still passes. --- diff --git a/skills/product-launch-video/scripts/capture-skill-guardrails.test.mjs b/skills/product-launch-video/scripts/capture-skill-guardrails.test.mjs new file mode 100644 index 000000000..1c477d24a --- /dev/null +++ b/skills/product-launch-video/scripts/capture-skill-guardrails.test.mjs @@ -0,0 +1,39 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +function read(relativePath) { + return readFileSync(fileURLToPath(new URL(relativePath, import.meta.url)), "utf8"); +} + +test("product launch capture treats blocked output as a hard gate", () => { + const skill = read("../SKILL.md"); + + assert.match(skill, /hyperframes capture[^\n]+--json/); + assert.match(skill, /capture\/BLOCKED\.md[^\n]+hard stop/i); + assert.match(skill, /do not[\s\S]{0,120}synthetic[\s\S]{0,120}fallback/i); + assert.match(skill, /very little text[\s\S]{0,180}empty asset/i); + assert.match(skill, /show-it-as-is[\s\S]{0,240}provided screenshot/i); +}); + +test("CLI capture reference documents the two budgets and machine diagnostics", () => { + const reference = read("../../hyperframes-cli/references/init-and-scaffold.md"); + + assert.match(reference, /--capture-budget/); + assert.match(reference, /--skip-vision/); + assert.match(reference, /HYPERFRAMES_CAPTURE_PHASE/); + assert.match(reference, /BLOCKED\.md[^\n]+hard stop/i); + assert.match(reference, /--timeout[^\n]+navigation[^\n]+--capture-budget/i); + assert.match(reference, /fresh output directory/i); + assert.match(reference, /outer caller timeout[\s\S]{0,180}does not prove/i); +}); + +test("webpage motion workflow refuses blocked capture artifacts", () => { + const module = read("../../motion-graphics/categories/webpage/module.md"); + + assert.match(module, /BLOCKED\.md[\s\S]{0,120}stop/i); + assert.match(module, /provided[\s\S]{0,120}screenshot[\s\S]{0,120}explicit/i); + assert.match(module, /do not[^\n]+blocked[^\n]+capture/i); + assert.match(module, /asset-free fallback/i); +});