Skip to content

Commit ea40d8e

Browse files
gershamclaude
andcommitted
Teach local compose and whitelist canvas layout deps
Canonical skill teaches compose catalog and compose render. Do not emit native text, box, or line through playlist templates. Nested plugins/screenrig/skills/screenrig/SKILL.md is generated from that canonical skill, not hand-authored. scripts/check-public-repo.py now allows bundled CLI dependencies exactly @napi-rs/canvas and yoga-layout. Nested plugins/screenrig/scripts/check-public-repo.py is generated output. Generated skill copy only. Rebuilt from locked CLI artifact e34c6870 / sha256 7fa2dd6b. Bundled CLI tarball still has zero npm deps. Did not change components.lock.json. Gates: implementer reported python3 scripts/check-public-repo.py pass. Tree matches the reported paths. Public hygiene re-run before push. Skipped: validate-plugin against a new CLI artifact. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent dcba8be commit ea40d8e

5 files changed

Lines changed: 134 additions & 160 deletions

File tree

‎AGENTS.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ does not own the CLI source, players, backend, site, or production deployment.
6464
sources.
6565
- Screenshotting is in v1. `screen screenshot <id>` blocks on a still WebP and
6666
writes a file. Do not print pixels.
67+
- Teach local compose (`compose catalog`, `compose render`) for copy and
68+
chrome. Do not teach emitting native `text`, `box`, or `line` through
69+
playlist templates. Wire families are static (`image`), motion (`video`),
70+
and web (`iframe`, `application`). Compose is local and not billed.
6771
- Human `events list` and `events follow` print logfmt. Human logfmt omits
6872
canned server sentences. An `application.event` or `runtime.reported` with
6973
no remaining data is silent. `--json events list` is one JSON page

‎plugins/screenrig/scripts/check-public-repo.py‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,18 @@ def check_metadata(errors: list[str]) -> None:
7070
repository = package.get("repository")
7171
if not isinstance(repository, dict) or repository.get("url") != CLI_REPOSITORY:
7272
errors.append(f"{package_path.relative_to(ROOT)} repository.url must be {CLI_REPOSITORY}")
73-
for field in ("dependencies", "optionalDependencies", "peerDependencies"):
73+
allowed_cli_deps = {"@napi-rs/canvas", "yoga-layout"}
74+
deps = package.get("dependencies")
75+
if deps:
76+
if not isinstance(deps, dict):
77+
errors.append(f"{package_path.relative_to(ROOT)} dependencies must be an object")
78+
else:
79+
extra = sorted(set(deps) - allowed_cli_deps)
80+
if extra:
81+
errors.append(
82+
f"bundled CLI dependencies must be exactly {sorted(allowed_cli_deps)}; extra {extra}"
83+
)
84+
for field in ("optionalDependencies", "peerDependencies"):
7485
if package.get(field):
7586
errors.append(f"bundled CLI must not require unavailable {field}")
7687

‎plugins/screenrig/skills/screenrig/SKILL.md‎

Lines changed: 53 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,8 @@ remaining is 0:
337337
- `auth revoke --yes`
338338
- `screen toast`
339339
- `screen screenshot` (request, status poll, and WebP download)
340+
- `compose catalog`
341+
- `compose render`
340342

341343
Exempt listen-stream events (not billed after subscribe): `screen.*`,
342344
`runtime.*`, `application.event`, and heartbeats.
@@ -368,6 +370,8 @@ media show <id>
368370
media list [--tag TAG] [--kind image|video]
369371
media update <id> (--tag TAG | --clear-tag) --if-match REVISION
370372
media delete <id> --if-match REVISION
373+
compose catalog
374+
compose render <file> [--output FILE] [--open]
371375
playlist templates
372376
playlist create <file>
373377
playlist update <id> <file> --if-match REVISION
@@ -428,6 +432,9 @@ The default path is `./<id>.webp`. `--timeout` defaults to 35000 ms and
428432
`--poll-ms` defaults to 500 ms. There is no `--no-wait`. Do not print
429433
pixels.
430434

435+
`compose catalog` and `compose render` run locally. They do not enroll and
436+
they do not debit. See "Local compose" below.
437+
431438
`screen show <id>` prints the GET screen JSON. After a player reports a
432439
playback surface, the body may include optional `observation`: `observed_at`
433440
and `surfaces`. Each surface has `id`, `width`, `height`, `pixel_ratio`, and
@@ -447,11 +454,43 @@ Media upload keeps the signed transfer private and returns metadata only; see "M
447454
uploads and the ffmpeg toolchain" before the first upload of a session. Application
448455
K/V is binary-safe; use exactly one value mode.
449456

457+
## Local compose
458+
459+
Write JSON, render a PNG, look at that PNG, iterate. Compose is not billed.
460+
461+
```bash
462+
"$SR" --json compose catalog
463+
"$SR" --json compose render ./spec.json --output ./still.png
464+
# agent reads ./still.png with vision; do not cat pixels into chat
465+
# iterate the JSON and re-render
466+
"$SR" --json media upload ./still.png
467+
# playlist page: one image placement, rect = canvas, content_fit fill
468+
```
469+
470+
`compose catalog` prints the fail-closed node catalog: types
471+
`Frame`/`Column`/`Row`/`Box`/`Spacer`/`Text`/`Image`, roles
472+
`display|title|body|caption|label`, spaces `xs|s|m|l|xl`, pins
473+
`top|bottom|left|right`. Do not author `x`/`y` except on the Frame canvas.
474+
Do not author `fontSize`. `Image.src` is a local filesystem path relative to
475+
the spec file. The CLI does not fetch URLs. The envelope is structured JSON,
476+
not pixels.
477+
478+
`compose render` writes a PNG and `<output>.layout.json`. Default `--output`
479+
replaces a `.json` suffix with `.png`, or appends `.png`. Never print PNG
480+
bytes, pixels, or image data. `--open` opens the local PNG path on this
481+
computer only when the user asked to view the still here. Agent vision uses
482+
the file path, not `--open`.
483+
450484
## Playlist writes
451485

452486
A page is one of two shapes. Do not mix `template` and `placements` on the
453487
same page.
454488

489+
Wire placement families are three: static (`image`), motion (`video`), and
490+
web (`iframe`, `application`). Do not author native `text`, `box`, or `line`
491+
on the wire. Compose copy and chrome locally, upload the still as `image`,
492+
and place that image.
493+
455494
### Full page
456495

457496
A full page is `id`, `canvas`, `transition`, `advance`, optional `visibility`,
@@ -529,98 +568,33 @@ mode, write a full page and follow "Putting a web app on a screen" below.
529568

530569
### Templated page
531570

532-
The CLI expands `template` + `slots` into an ordinary page and sends that.
533-
The server never sees `template`. `playlist show` and `playlist get` return
534-
the expanded placements, not the template id.
535-
536-
List the closed ids from the command:
571+
Do not emit native `text`, `box`, or `line` through templates. Slide layouts
572+
with copy or chrome are composed locally, uploaded as `image`, then placed
573+
as one image. See "Local compose".
537574

538575
```bash
576+
"$SR" --json compose catalog
539577
"$SR" --json playlist templates
540578
```
541579

542-
Use only those ids. Do not invent a template id.
580+
`playlist templates` is a local catalog of slide ids. Templates that would
581+
emit vector chrome fail with `usage_error` pointing at `compose catalog`
582+
and `compose render`. Do not silently rasterize and upload. Picture-only
583+
templates (`slide-full-bleed`, and `slide-photo` without a caption) still
584+
expand to image or video placements with selectors. A `logo` slot is image
585+
only; video is a `usage_error`.
543586

544587
Allowed keys on a templated page: `id`, `template`, `slots`, optional
545588
`canvas.background` only, optional `text_color`, optional `transition`,
546589
optional `advance`, optional `visibility`. Any other page key is a
547590
`usage_error`. `canvas.width`, `canvas.height`, and `canvas.viewport_fit` on
548591
a templated page are a `usage_error`.
549592

550-
The 15 templates share one 1920×1080 canvas, `viewport_fit: contain`,
551-
background `#1B2632FF`, default text `#EEE9DFFF`, and family `sans`. Every
552-
template draws a mustard `#F8B334FF` bar across the top. There is no
553-
automatic wrapping; put a line feed in the string or pass `text` as an array
554-
of lines. `text_color` tints slots that do not already have a template color
555-
(eyebrow, stat values, and other mustard or dim slots keep theirs). It is
556-
CLI sugar and is not a REST field. Override the background with
557-
`canvas.background` only, using the same solid-or-linear union as a full
558-
page. The default template background stays the solid `#1B2632FF`.
559-
560-
If you omit `transition` or `advance`, the CLI fills
561-
`{ "type": "crossfade", "duration_ms": 200 }` and
562-
`{ "mode": "duration", "after_ms": 8000 }`. You may replace either. Templates
563-
do not set `visibility`.
564-
565-
Slots do not take `rect`, `layer`, `font_*`, or `color`. A text slot is
566-
`{ "text": "…" }` or `{ "text": ["line", "line"] }`. It may set `align` to
567-
`left`, `center`, or `right`, and `vertical_align` to `top`, `middle`, or
568-
`bottom`. Omit either key to keep the template default. `playlist templates`
569-
prints those defaults. An image or video slot uses the same write content as
570-
a full page (`type` + `selector`, optional `alt` / video flags). A `picture`
571-
slot may set `content_fit` to `contain` or `cover`. A `logo` slot is image
572-
only; video is a `usage_error`. Omit an optional slot to omit that placement.
573-
574-
The expander packs each text slot to `lineCount * line_height`, then stacks
575-
present slots from the template start `y`. Centered families (`slide-intro`,
576-
`slide-text-only-1`, `slide-text-only-2`, `slide-quote`, `slide-callout`)
577-
group-center that packed stack in the stage hole. If the copy has more lines
578-
than the hole allows, the expander keeps the lines that fit and appends `…`
579-
to the last kept line. It does not ellipsize by width. Author-time fit may
580-
shrink or grow `font_size` and `line_height` so the longest line meets the
581-
hole width, within 50%–150% of the template size and never below 12. The
582-
player does not scale live. There is no wrapping.
583-
584-
The 15 ids and their slots:
585-
586-
- `slide-intro`: `title` text required; `subtitle` text optional; `logo` image optional
587-
- `slide-text-only-1`: `eyebrow` and `headline` text required; `subhead`, `body`, `footnote` text optional; `logo` image optional
588-
- `slide-text-only-2`: `eyebrow` and `headline` text required; `subhead` text optional; `logo` image optional
589-
- `slide-text-photo-1`: `headline` text required; `eyebrow`, `subhead`, `body`, `footnote` text optional; `picture` image or video optional; `logo` image optional. Text left, picture right.
590-
- `slide-text-photo-2`: same slots as `slide-text-photo-1`. Picture left, text right.
591-
- `slide-text-photo-3`: same slots. Wider picture left (contain by default), text right.
592-
- `slide-half-bleed-1`: `headline` text required; `eyebrow`, `subhead`, `body`, `footnote` text optional; `picture` image or video required; `logo` image optional. Picture fills the left half.
593-
- `slide-half-bleed-2`: same slots. Picture fills the right half.
594-
- `slide-quote`: `quote` text required; `author` text optional; `logo` image optional
595-
- `slide-callout`: `headline` text required; `body` text optional; `logo` image optional
596-
- `slide-bullets`: `headline` text required; `eyebrow` text optional; `b1` text required; `b2`–`b6` text optional; `picture` image or video optional; `logo` image optional
597-
- `slide-stat-grid`: `headline` text required; `eyebrow` text optional; `v1` and `l1` text required; `v2`+`l2`, `v3`+`l3`, `v4`+`l4` text optional; `logo` image optional
598-
- `slide-three-up`: `headline` text required; `eyebrow` text optional; `t1` and `b1` text required; `t2`+`b2`, `t3`+`b3` text optional; `logo` image optional
599-
- `slide-photo`: `picture` image or video required; `caption` text optional; `logo` image optional
600-
- `slide-full-bleed`: `picture` image or video required; `logo` image optional
601-
602-
```json
603-
{
604-
"name": "Welcome",
605-
"pages": [
606-
{
607-
"id": "intro",
608-
"template": "slide-intro",
609-
"slots": {
610-
"title": { "text": "Welcome" },
611-
"subtitle": { "text": ["Today's briefing", "Conference room A"] }
612-
}
613-
}
614-
]
615-
}
616-
```
593+
Wire families:
617594

618-
Emit `text` and `line` placements only through these templates. Templates
619-
may emit a `box` placement as plate chrome behind `slide-quote` and
620-
`slide-callout`. Agents still must not hand-author `box` or free vector
621-
geometry. If the API returns `invalid_request` saying `text, box, and line
622-
placements are not enabled`, stop and report that. Do not invent an image
623-
fallback.
595+
- static: `image`
596+
- motion: `video`
597+
- web: `iframe`, `application`
624598

625599
Looking at the screen stays the only proof of layout. Do not claim a
626600
template is hardware-validated.

‎scripts/check-public-repo.py‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,18 @@ def check_metadata(errors: list[str]) -> None:
7070
repository = package.get("repository")
7171
if not isinstance(repository, dict) or repository.get("url") != CLI_REPOSITORY:
7272
errors.append(f"{package_path.relative_to(ROOT)} repository.url must be {CLI_REPOSITORY}")
73-
for field in ("dependencies", "optionalDependencies", "peerDependencies"):
73+
allowed_cli_deps = {"@napi-rs/canvas", "yoga-layout"}
74+
deps = package.get("dependencies")
75+
if deps:
76+
if not isinstance(deps, dict):
77+
errors.append(f"{package_path.relative_to(ROOT)} dependencies must be an object")
78+
else:
79+
extra = sorted(set(deps) - allowed_cli_deps)
80+
if extra:
81+
errors.append(
82+
f"bundled CLI dependencies must be exactly {sorted(allowed_cli_deps)}; extra {extra}"
83+
)
84+
for field in ("optionalDependencies", "peerDependencies"):
7485
if package.get(field):
7586
errors.append(f"bundled CLI must not require unavailable {field}")
7687

0 commit comments

Comments
 (0)