astack-skills 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astack",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Skills for Claude Code and Codex that take product work from the thread to a proven, live release.",
5
5
  "author": {
6
6
  "name": "Andrew Wilkinson",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astack",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Skills for Claude Code and Codex that take product work from the thread to a proven, live release.",
5
5
  "author": {
6
6
  "name": "Andrew Wilkinson",
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0
4
+
5
+ Review gates and video from LangChain's open-source design agent, [Content Studio](https://github.com/langchain-ai/design-agent) (Apache-2.0), credited where they are used. Its idea is that the model makes the creative calls and code renders, measures and refuses.
6
+
7
+ - New `render-gate.mjs` in `web-design`, `web-graphics` and `launch`. It renders a page, card or slide at each size as a fresh layout, then fails:
8
+ - a brand face that silently fell back (Chrome reports which font actually painted each run);
9
+ - supplied copy that changed, or a deliberate clamp that cuts required copy;
10
+ - text clipped by its box or cropped by the canvas edge;
11
+ - painted colours and faces outside the DESIGN.md tokens.
12
+
13
+ Exit 2 means it could not check and is never a pass.
14
+ - New `film.mjs` in `launch`. It renders an HTML composition to MP4 frame by frame: every CSS and Web Animations animation is paused and seeked, so a demo clip is code and every take is identical. It fails:
15
+ - a black or flat opening;
16
+ - a film where nothing moves;
17
+ - clock-driven motion, found by re-seeking frames and comparing them;
18
+ - the wrong size or duration;
19
+ - failed fonts or requests.
20
+ - New `web-design` review rubric:
21
+ - three gates before scoring (accuracy, the deterministic gates, the ask);
22
+ - six areas scored 1 to 5 with evidence;
23
+ - a ship / fix / rework verdict;
24
+ - a failure-pattern table.
25
+
26
+ `astack`'s independent verifier and `launch` use it for visual work.
27
+ - `launch` builds share cards with `render-gate.mjs` and demo clips with `film.mjs`. A new film reference covers storyboard, animatic, motion, contact sheets and changelog clips from a pull request.
28
+ - `web-design` anti-patterns gain a section for sets and assets: scaled instead of recomposed, template sameness, silent font fallback, faked weight.
29
+ - New `launch-assets` comparison eval task. Scored blind: astack 19, baseline 17. Both fixed every planted problem; astack's clip re-rendered bit-identically and its cards stayed on tokens.
30
+ - `shoot.mjs`, `render-gate.mjs` and `film.mjs` now stop Chrome's helper processes along with the browser, so the temporary profile is removed cleanly on Linux instead of failing the run.
31
+
3
32
  ## 0.6.0
4
33
 
5
34
  Motion and mobile craft from [Emil Kowalski's skills](https://github.com/emilkowalski/skills) and animated stickers from [fun-isometric](https://skills.klvn.dev/fun-isometric) by Kelvin Adithya, both MIT and credited where they are used.
package/README.md CHANGED
@@ -78,6 +78,8 @@ Most skill packs tell the model to be thorough. These come with small scripts so
78
78
  - `slop-scan.mjs` finds the usual suspects, from "seamless" to gradient headlines to emoji bullets, and explains why each one reads as generated. <!-- slop-scan: ignore-line -->
79
79
  - `page-audit.mjs` shows what crawlers and link previews see, like a forgotten noindex or a broken share card.
80
80
  - `iso.mjs` turns a small scene description into clean isometric SVG with the geometry right, because nobody should hand-roll isometric maths.
81
+ - `render-gate.mjs` renders a card, slide or page at each size and fails it when the brand font quietly fell back, the copy drifted from what was asked, text got clipped, or a colour isn't one of your tokens.
82
+ - `film.mjs` turns an HTML page with CSS animations into an MP4, frame by frame, so a demo clip is code you can fix and re-render. It refuses films that open on black, never move, or come out different each take.
81
83
 
82
84
  ## Does it actually help?
83
85
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astack-skills",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "private": false,
5
5
  "description": "Skills for Claude Code and Codex that take product work from the thread to a proven, live release.",
6
6
  "license": "MIT",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "astack",
4
- "version": "0.6.0",
4
+ "version": "0.7.0",
5
5
  "description": "Skills for Claude Code and Codex that take product work from the thread to a proven, live release.",
6
6
  "author": {
7
7
  "name": "Andrew Wilkinson",
@@ -19,7 +19,7 @@ Copy these eight lines into your todo list before any task-specific items. Close
19
19
  4. **Use it**: the real journey, in a browser or runtime, to the real outcome; fix and replay. (`dogfood`)
20
20
  5. **Measure**: the success action fires once, on real success, and arrives. (`instrument`, when there is a success action)
21
21
  6. **Clean**: `deslop` scan on changed copy, UI and code; every medium or high hit fixed or dismissed with a reason.
22
- 7. **Verify independently**: a fresh agent that did not write the change exercises it and returns PASS or FAIL with evidence. (`ship`; docs-only or trivial changes may skip)
22
+ 7. **Verify independently**: a fresh agent that did not write the change exercises it and returns PASS or FAIL with evidence. For visual work it also scores its own renders with `web-design`'s review rubric and gives ship, fix or rework. (`ship`; docs-only or trivial changes may skip)
23
23
  8. **PR ready**: `node <skill-dir>/scripts/pr-state.mjs` reports no blockers on the current head; the PR body says what changed, how it was proven, and what was not verified. (`ship`)
24
24
 
25
25
  For visual changes, capture 390 and 1440 in light and dark with [`scripts/shoot.mjs`](scripts/shoot.mjs) and look at the images. Use [completion checks](references/completion-checks.md) as the final gate.
@@ -94,6 +94,12 @@ async function launch() {
94
94
  catch (e) { await sleep(1000); try { return await launchOnce(); } catch { throw e; } }
95
95
  }
96
96
 
97
+ // Best effort: a leftover temp profile must never fail the check itself.
98
+ function removeProfile(profile) {
99
+ try { rmSync(profile, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 }); }
100
+ catch (e) { process.stderr.write(`warning: could not remove ${profile}: ${e.code ?? e.message}\n`); }
101
+ }
102
+
97
103
  async function launchOnce() {
98
104
  const profile = mkdtempSync(join(tmpdir(), "shoot-profile-"));
99
105
  const proc = spawn(findChrome(), [
@@ -104,11 +110,13 @@ async function launchOnce() {
104
110
  ...(process.env.CI || process.getuid?.() === 0 ? ["--no-sandbox"] : []),
105
111
  ...(process.env.SHOOT_CHROME_FLAGS ? process.env.SHOOT_CHROME_FLAGS.split(/\s+/) : []),
106
112
  "about:blank",
107
- ], { stdio: ["ignore", "ignore", "pipe"] });
113
+ ], { stdio: ["ignore", "ignore", "pipe"], detached: process.platform !== "win32" });
108
114
  let stderr = "";
109
115
  proc.stderr.on("data", d => { stderr = (stderr + d).slice(-2000); });
110
116
  const port = await new Promise((ok, fail) => {
111
- const timer = setTimeout(() => fail(new Error("Chrome did not expose a DevTools port")), 15000);
117
+ const timer = setTimeout(() => { clearInterval(poll);
118
+ try { process.kill(process.platform === "win32" ? proc.pid : -proc.pid, "SIGKILL"); } catch { /* already exited */ }
119
+ fail(new Error("Chrome did not expose a DevTools port")); }, 15000);
112
120
  const poll = setInterval(() => {
113
121
  const file = join(profile, "DevToolsActivePort");
114
122
  if (existsSync(file)) {
@@ -117,14 +125,15 @@ async function launchOnce() {
117
125
  }
118
126
  }, 50);
119
127
  proc.on("exit", code => { clearInterval(poll); clearTimeout(timer);
120
- rmSync(profile, { recursive: true, force: true });
128
+ removeProfile(profile);
121
129
  fail(new Error(`Chrome exited (${code})${stderr.trim() ? `: ${stderr.trim().split("\n").slice(-3).join(" | ")}` : ""}`)); });
122
130
  });
123
131
  const exited = new Promise(r => proc.once("exit", r));
124
132
  const close = async () => {
125
- try { proc.kill("SIGKILL"); } catch { /* already exited */ }
133
+ // Chrome's helpers (crashpad, zygote) outlive the main process and keep writing the profile.
134
+ try { process.kill(process.platform === "win32" ? proc.pid : -proc.pid, "SIGKILL"); } catch { /* already exited */ }
126
135
  await Promise.race([exited, sleep(3000)]);
127
- rmSync(profile, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 });
136
+ removeProfile(profile);
128
137
  };
129
138
  return { port, close };
130
139
  }
@@ -94,6 +94,12 @@ async function launch() {
94
94
  catch (e) { await sleep(1000); try { return await launchOnce(); } catch { throw e; } }
95
95
  }
96
96
 
97
+ // Best effort: a leftover temp profile must never fail the check itself.
98
+ function removeProfile(profile) {
99
+ try { rmSync(profile, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 }); }
100
+ catch (e) { process.stderr.write(`warning: could not remove ${profile}: ${e.code ?? e.message}\n`); }
101
+ }
102
+
97
103
  async function launchOnce() {
98
104
  const profile = mkdtempSync(join(tmpdir(), "shoot-profile-"));
99
105
  const proc = spawn(findChrome(), [
@@ -104,11 +110,13 @@ async function launchOnce() {
104
110
  ...(process.env.CI || process.getuid?.() === 0 ? ["--no-sandbox"] : []),
105
111
  ...(process.env.SHOOT_CHROME_FLAGS ? process.env.SHOOT_CHROME_FLAGS.split(/\s+/) : []),
106
112
  "about:blank",
107
- ], { stdio: ["ignore", "ignore", "pipe"] });
113
+ ], { stdio: ["ignore", "ignore", "pipe"], detached: process.platform !== "win32" });
108
114
  let stderr = "";
109
115
  proc.stderr.on("data", d => { stderr = (stderr + d).slice(-2000); });
110
116
  const port = await new Promise((ok, fail) => {
111
- const timer = setTimeout(() => fail(new Error("Chrome did not expose a DevTools port")), 15000);
117
+ const timer = setTimeout(() => { clearInterval(poll);
118
+ try { process.kill(process.platform === "win32" ? proc.pid : -proc.pid, "SIGKILL"); } catch { /* already exited */ }
119
+ fail(new Error("Chrome did not expose a DevTools port")); }, 15000);
112
120
  const poll = setInterval(() => {
113
121
  const file = join(profile, "DevToolsActivePort");
114
122
  if (existsSync(file)) {
@@ -117,14 +125,15 @@ async function launchOnce() {
117
125
  }
118
126
  }, 50);
119
127
  proc.on("exit", code => { clearInterval(poll); clearTimeout(timer);
120
- rmSync(profile, { recursive: true, force: true });
128
+ removeProfile(profile);
121
129
  fail(new Error(`Chrome exited (${code})${stderr.trim() ? `: ${stderr.trim().split("\n").slice(-3).join(" | ")}` : ""}`)); });
122
130
  });
123
131
  const exited = new Promise(r => proc.once("exit", r));
124
132
  const close = async () => {
125
- try { proc.kill("SIGKILL"); } catch { /* already exited */ }
133
+ // Chrome's helpers (crashpad, zygote) outlive the main process and keep writing the profile.
134
+ try { process.kill(process.platform === "win32" ? proc.pid : -proc.pid, "SIGKILL"); } catch { /* already exited */ }
126
135
  await Promise.race([exited, sleep(3000)]);
127
- rmSync(profile, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 });
136
+ removeProfile(profile);
128
137
  };
129
138
  return { port, close };
130
139
  }
@@ -31,11 +31,13 @@ Fix before announcing:
31
31
 
32
32
  Put it in one folder (`launch/` or `docs/launch/`). Contents and formats in [kit](references/kit.md):
33
33
 
34
- - **Share card** rendered from HTML/SVG source by script, committed with its source.
35
- - **Demo media**: 3 to 5 screenshots at real sizes, and a 6 to 15 second clip of the core action if motion helps. Real product, real-looking data, no fake metrics.
34
+ - **Share card** rendered from HTML/SVG source with `render-gate.mjs`, committed with its source. The gate fails a card whose font fell back, whose copy drifted from `copy.md`, or whose text is cropped.
35
+ - **Demo media**: 3 to 5 screenshots at real sizes, and a 6 to 15 second clip of the core action if motion helps. Build the clip as an HTML composition and render it with `film.mjs`, so a fix is an edit and a re-render ([film](references/film.md)). Real product, real-looking data, no fake metrics.
36
36
  - **Copy**: one-line description (60 characters or fewer), a short paragraph, a longer description, the X/LinkedIn post, a Show HN or community post if it fits, a changelog entry. Rules in [copy](references/copy.md).
37
37
  - **Tracker**: a CSV of where you will post or submit.
38
38
 
39
+ Before anything goes out, score the card, screenshots and clip with `web-design`'s review rubric if it is installed, from your own renders at feed size. Only a ship verdict goes out.
40
+
39
41
  ## 3. Write copy that survives scrutiny
40
42
 
41
43
  Specific, verifiable, in the product's voice. Lead with what someone can do now. Every number has a source; someone else's numbers are labelled as theirs. No guarantees you cannot keep. Run `deslop`'s scan on every piece. Vary the angle per audience rather than pasting one blurb everywhere.
@@ -0,0 +1,77 @@
1
+ # Film: demo and changelog clips as code
2
+
3
+ A clip built as an HTML page on a timeline can be reviewed, diffed and re-rendered. A fix is an edit, not a re-shoot. `film.mjs` pauses the page, seeks every animation to each frame time, captures it and encodes with ffmpeg. The approach follows LangChain's [Content Studio](https://github.com/langchain-ai/design-agent) and [HyperFrames](https://hyperframes.heygen.com); `film.mjs` is a dependency-free take on the same idea.
4
+
5
+ ## The order of work
6
+
7
+ 1. **Storyboard.** Write it in `launch/media/STORYBOARD.md`, one line per beat. Give each: its time, what is on screen, the one thing the viewer should read, and the source (a real screenshot, a real number with its query). 6 to 15 seconds is 3 to 5 beats. Cut any beat that has nothing to read.
8
+ 2. **Material.** Capture real screens with `shoot.mjs` at the size they will appear, at `--scale 2`. No mock dashboards, no invented numbers.
9
+ 3. **Animatic.** Build the composition with every beat in place and almost no motion. Render it, and look at a contact sheet (below) before adding motion. Most problems are layout and copy, and they are cheaper to fix here.
10
+ 4. **Motion.** Add only motion that explains: a cursor that travels to the button, a value that changes, a panel that opens. Ease out, 200 to 600ms per move, one thing moving at a time.
11
+ 5. **Render and gate.** `film.mjs` must exit 0. Then watch it once at full size and once at feed size, and score it with `web-design`'s review rubric if it is installed.
12
+
13
+ ## The composition contract
14
+
15
+ - Size the page to the frame: `body { width: 1920px; height: 1080px; margin: 0; overflow: hidden }`.
16
+ - Declare the length: `<html data-duration="8">`.
17
+ - Drive everything from CSS animations or the Web Animations API, started on load and placed with `animation-delay`. `film.mjs` pauses all of them and sets their time per frame, so they render identically every take.
18
+ - `<video>` elements are seeked to the frame time. GSAP's global timeline is seeked if `window.gsap` exists.
19
+ - Anything else (canvas loops, counters on `setInterval`, `requestAnimationFrame`) must expose `window.__film = { duration, seek(seconds) }` and draw the exact state for that time. `film.mjs` re-seeks two frames after rendering and fails the film if they changed: that is how clock-driven motion is caught.
20
+ - Use the product's real fonts and tokens, and run `render-gate.mjs` on the composition at its frame size to catch font fallback and off-token colours before rendering.
21
+
22
+ A minimal composition:
23
+
24
+ ```html
25
+ <!doctype html>
26
+ <html data-duration="8">
27
+ <style>
28
+ @font-face { font-family: "Brand"; src: url(../../public/fonts/brand.woff2) }
29
+ body { width: 1920px; height: 1080px; margin: 0; overflow: hidden; background: #F6F8FA; color: #0E1726; font: 600 72px/1.1 Brand, sans-serif }
30
+ .beat { position: absolute; inset: 0; display: grid; place-items: center; opacity: 0 }
31
+ .b1 { animation: leave 2.8s both }
32
+ .b2 { animation: show 3s 2.8s both }
33
+ .b3 { animation: show 2.2s 5.8s both }
34
+ .shot { width: 1400px; border-radius: 16px; box-shadow: 0 24px 60px rgb(14 23 38 / .18) }
35
+ .b2 .shot { animation: rise .5s 2.8s cubic-bezier(.2, .8, .2, 1) both }
36
+ @keyframes leave { 0%, 92% { opacity: 1 } 100% { opacity: 0 } }
37
+ @keyframes show { 0%, 100% { opacity: 0 } 6%, 94% { opacity: 1 } }
38
+ @keyframes rise { from { transform: translateY(40px) } }
39
+ </style>
40
+ <div class="beat b1">Sell USDC for cash</div>
41
+ <div class="beat b2"><img class="shot" src="shots/1440-light.png" alt=""></div>
42
+ <div class="beat b3">usdctofiat.xyz</div>
43
+ </html>
44
+ ```
45
+
46
+ ```sh
47
+ node <skill-dir>/scripts/film.mjs launch/media/demo.html --size 1920x1080 --out launch/media/demo.mp4 --poster launch/media/demo-poster.png --poster-at 3
48
+ ```
49
+
50
+ The first beat is fully visible at 0s and only fades on its way out, so the film opens on words. `film.mjs` fails a film that opens on a flat or black frame.
51
+
52
+ ## Look at it
53
+
54
+ A contact sheet shows the whole film at once:
55
+
56
+ ```sh
57
+ ffmpeg -loglevel error -i launch/media/demo.mp4 -vf "fps=1,scale=480:-1,tile=4x2" -frames:v 1 launch/media/sheet.png
58
+ ```
59
+
60
+ Read every beat: is the one thing to read legible at 480 wide? Does each beat hold long enough to read it aloud? `film.mjs` notes holds over 2 seconds, which are right for a beat with words and wrong between beats.
61
+
62
+ ## Changelog clip from a pull request
63
+
64
+ For "make a clip of what shipped":
65
+
66
+ 1. Read the PR: title, description, diff and linked issue. Find the one change a user would notice. A refactor has no clip.
67
+ 2. Capture before and after with `shoot.mjs` at the same size and state: before from the base revision or production, after from the preview or the merged deploy.
68
+ 3. Three beats: what changed in the user's words, before and after (side by side or as a wipe), where to try it.
69
+ 4. Every claim in the clip traces to the PR or a real measurement.
70
+
71
+ ## Sound
72
+
73
+ Silent by default; feeds autoplay muted. If speech matters, burn the captions into the composition as text. Use `--audio` only for a track you have rights to, and normalise it first: `ffmpeg -i in.wav -af loudnorm=I=-16:TP=-1.5 out.m4a`.
74
+
75
+ ## Heavier jobs
76
+
77
+ Long films, many scenes or a team review loop may outgrow one HTML file. [HyperFrames](https://hyperframes.heygen.com) (HeyGen, Apache-2.0) is a full framework for the same model, with a CLI, a linter and its own agent skills. It installs third-party code, so ask the user before adding it to a project.
@@ -20,7 +20,7 @@ launch/
20
20
 
21
21
  ## Share card
22
22
 
23
- - Author in HTML or SVG at 1200x630 with the product's fonts and colours; render with `node <skill-dir>/scripts/shoot.mjs launch/card/card.html --widths 1200 --height 630 --themes light --scale 1 --out launch/card`. Rename the output to `og.png`.
23
+ - Author in HTML or SVG at 1200x630 with the product's fonts and colours; render and gate with `node <skill-dir>/scripts/render-gate.mjs launch/card/card.html --size 1200x630 --size 1200x1200 --design DESIGN.md --expect-file launch/card/copy.txt --out launch/card`. Rename the outputs to `og.png` and `og-square.png`. Each size is a fresh layout; recompose the square, never scale it.
24
24
  - One message: the product name, what it does in under eight words, and either the product itself (a real screenshot crop) or one strong graphic. No paragraphs.
25
25
  - Keep important content inside the central ~1000x500; platforms crop edges.
26
26
  - Test it on a dark and a light feed background; a white card with no border disappears on a white feed.
@@ -36,9 +36,10 @@ launch/
36
36
  ## Demo clip
37
37
 
38
38
  - 6 to 15 seconds, one action from start to result. Start on the meaningful frame, not a blank page.
39
- - Record at a fixed viewport (1920x1080 or 1080x1920 for vertical) with reduced motion off and a steady pace; avoid cursor wandering.
40
- - Deterministic: same data and same timing each take, so you can re-record after a fix.
41
- - Check the export: duration, dimensions, no black first frame, file size within each platform's limit.
39
+ - Build it as an HTML composition and render it with `film.mjs` ([film](film.md)): real screenshots from `shoot.mjs` as the material, captions and motion in CSS. 1920x1080, or 1080x1920 for vertical.
40
+ - Deterministic: the same composition renders the same film every time, so a fix is an edit and a re-render. `film.mjs` fails a clip that does not.
41
+ - `film.mjs` checks size, duration, a black or flat opening frame, a film where nothing moves, and failed fonts or requests. Then check the file size against each platform's limit.
42
+ - Record the live product instead only when the interaction itself is the point; keep the viewport fixed, the pace steady and the cursor purposeful.
42
43
 
43
44
  ## Tracker
44
45