docs(skills): gate blocked website captures

This commit is contained in:
Miguel Ángel
2026-07-31 20:30:07 +00:00
parent 9ae0007261
commit 30f3830741
5 changed files with 96 additions and 8 deletions
+4 -4
View File
@@ -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",
@@ -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
@@ -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: <url>, 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: <url>, 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
+23 -3
View File
@@ -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 "<URL>" -o ./capture`
Run capture with: `npx hyperframes capture "<URL>" -o ./capture --json`. Keep the default
post-navigation budget unless the caller owns a smaller deadline; then pass a positive
`--capture-budget <milliseconds>` 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.
---
@@ -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);
});