@mehshekari/gsap-motion 3.6.0 → 4.1.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.
package/README.md CHANGED
@@ -10,7 +10,7 @@ npx @mehshekari/gsap-motion audit src # find silent GSAP failures; exits 1
10
10
  npx @mehshekari/gsap-motion doctor # what is installed, and is your GSAP recent enough
11
11
  ```
12
12
 
13
- No dependencies, no install scripts, Node.js 18 or later.
13
+ No dependencies, no install scripts, Node.js 22 or later.
14
14
 
15
15
  ## Commands
16
16
 
@@ -24,7 +24,7 @@ No dependencies, no install scripts, Node.js 18 or later.
24
24
  | `explain <file...|url>` | Maps an animation and changes nothing: where it lives, its preference branches, its beats in order with the reasons their comments give, its scroll configuration, and what the audit says. Given a URL it reads the running page instead, where the timeline is the one GSAP actually built. |
25
25
  | `review <url>` | Measures what the motion actually did, by sampling the DOM every frame rather than by reading GSAP's timeline — which a bundled app never exposes, and which would make it work on demo pages and fail on real ones. Reports time to first motion, the share of the screen moving at its busiest, loop seams, and motion over text being read. Sampling forces layout, so it does not report layout counts; `inspect` measures those. Takes the same `--scroll`, `--reduced`, `--dark`, `--mobile` and `--cpu`. |
26
26
  | `patterns check [file]` | Validates the Patterns section of your `ANIMATION.md`: the fields each status needs, the recorded uses behind `validated`, the named reviewer behind `canonical`, and entries verified against an older GSAP than the one installed. Exits 1 on a claim the evidence does not support, so it belongs in `lint` beside `audit`. A project with no Patterns section passes. |
27
- | `doctor` | Checks Node.js, the installed skill's version for this project and your user, your project's `gsap` and `@gsap/react`, and whether an `ANIMATION.md` exists. |
27
+ | `doctor` | `--json` for the same report a script can read. Checks Node.js, the installed skill's version for this project and your user, your project's `gsap` and `@gsap/react`, and whether an `ANIMATION.md` exists. |
28
28
 
29
29
  ## In CI
30
30
 
@@ -32,7 +32,7 @@ The audit needs no install. Pin the version, so a new rule cannot fail your
32
32
  build without warning:
33
33
 
34
34
  ```json
35
- "lint": "eslint && npx @mehshekari/gsap-motion@3.6.0 audit src --quiet"
35
+ "lint": "eslint && npx @mehshekari/gsap-motion@4.0.0 audit src --quiet"
36
36
  ```
37
37
 
38
38
  Or install it with `npm install --save-dev --save-exact @mehshekari/gsap-motion`.
@@ -100,6 +100,7 @@ Commands
100
100
  validated, the reviewer behind canonical
101
101
  --json Machine-readable output
102
102
  doctor Check Node, the installed skill and your project's GSAP
103
+ --json Machine-readable output
103
104
  --version Print the version
104
105
 
105
106
  Docs: https://github.com/mehShekari/gsap-motion`;
@@ -217,32 +218,88 @@ function runScript(script, args) {
217
218
  process.exit(result.status ?? 1);
218
219
  }
219
220
 
220
- function doctor() {
221
+ function doctor(args = []) {
222
+ const json = args.includes("--json");
221
223
  const bundled = skillInfo(SKILL_DIR);
222
224
  const mark = { ok: green("✓"), warn: yellow("!"), info: dim("·") };
225
+ /**
226
+ * Every check is collected, so `--json` reports the same run the text one
227
+ * does rather than a second, thinner version of it. A half-JSON doctor is
228
+ * worse than none: it invites a script to trust a field nobody filled in.
229
+ */
230
+ const checks = [];
223
231
  const report = (state, message, hint) => {
232
+ checks.push({ state, message, ...(hint ? { hint } : {}) });
233
+ if (json) return;
224
234
  console.log(`${mark[state]} ${message}`);
225
235
  if (hint && state !== "ok") console.log(` ${dim(hint)}`);
226
236
  };
227
237
 
228
- console.log(`${bold("gsap-motion")} ${pkg.version}, carrying ${SKILL_NAME} ${bundled.version}\n`);
238
+ if (!json) {
239
+ console.log(`${bold("gsap-motion")} ${pkg.version}, carrying ${SKILL_NAME} ${bundled.version}\n`);
240
+ }
229
241
 
230
242
  const nodeMajor = Number(process.versions.node.split(".")[0]);
231
243
  report(
232
- nodeMajor >= 18 ? "ok" : "warn",
244
+ nodeMajor >= 22 ? "ok" : "warn",
233
245
  `Node.js ${process.versions.node}`,
234
- "The audit scripts need Node.js 18 or later.",
246
+ "The audit scripts need Node.js 22 or later.",
235
247
  );
236
248
 
249
+ /**
250
+ * A plugin may already be providing the skill, and then a copy in
251
+ * `.claude/skills` is not a missing install, it is the thing that shadows the
252
+ * plugin and goes stale. Found in a real project: a checked-in copy three
253
+ * releases behind a plugin that was current, while doctor recommended the
254
+ * command that creates exactly that.
255
+ */
256
+ const pluginProvides = () => {
257
+ for (const root of [process.cwd(), homedir()]) {
258
+ for (const file of ["settings.json", "settings.local.json"]) {
259
+ try {
260
+ const settings = JSON.parse(readFileSync(join(root, ".claude", file), "utf8"));
261
+ const enabled = Object.entries(settings.enabledPlugins ?? {})
262
+ .filter(([, on]) => on)
263
+ .map(([name]) => name)
264
+ .filter((name) => name.startsWith("gsap-motion@"));
265
+ if (enabled.length) return enabled[0];
266
+ } catch {
267
+ /** No settings file, or not JSON: nothing is claimed either way. */
268
+ }
269
+ }
270
+ }
271
+ return null;
272
+ };
273
+
274
+ const provider = pluginProvides();
275
+ if (provider) {
276
+ report("ok", `Provided by the ${provider} plugin`);
277
+ }
278
+
237
279
  for (const [scope, root, command] of [
238
280
  ["this project", process.cwd(), `npx ${pkg.name} add`],
239
281
  ["your user", homedir(), `npx ${pkg.name} add --global`],
240
282
  ]) {
241
283
  const location = join(root, ".claude", "skills", SKILL_NAME);
242
284
  const installed = skillInfo(location);
243
- if (!installed) report("info", `Not installed for ${scope}`, `${command} installs it at ${location}`);
244
- else if (installed.version === bundled.version) report("ok", `Installed for ${scope}: ${installed.version}`);
245
- else report("warn", `Installed for ${scope}: ${installed.version}`, `${command} updates it to ${bundled.version}.`);
285
+ if (!installed) {
286
+ /**
287
+ * Recommending an install on top of a plugin is how a second, ageing copy
288
+ * gets created, so with a plugin present this is a fact, not a suggestion.
289
+ */
290
+ if (provider) report("info", `No separate copy for ${scope}`);
291
+ else report("info", `Not installed for ${scope}`, `${command} installs it at ${location}`);
292
+ } else if (installed.version === bundled.version) {
293
+ report("ok", `Installed for ${scope}: ${installed.version}`);
294
+ } else {
295
+ report(
296
+ "warn",
297
+ `Installed for ${scope}: ${installed.version}`,
298
+ provider
299
+ ? `The ${provider} plugin also provides it, and a local copy shadows the plugin. Remove ${location}, or ${command} to update it to ${bundled.version}.`
300
+ : `${command} updates it to ${bundled.version}.`,
301
+ );
302
+ }
246
303
  }
247
304
 
248
305
  const manifest = join(process.cwd(), "package.json");
@@ -310,6 +367,26 @@ function doctor() {
310
367
  rules ? "ANIMATION.md found — the skill follows it" : "No ANIMATION.md",
311
368
  "Optional. Add one so the skill follows your project's rules; see reference/project-rules.md in the skill.",
312
369
  );
370
+
371
+ if (json) {
372
+ console.log(
373
+ JSON.stringify(
374
+ {
375
+ cli: pkg.version,
376
+ skill: bundled.version,
377
+ node: process.versions.node,
378
+ adapters: ORDER.filter((name) => picked.has(name)),
379
+ plugin: provider,
380
+ animationMd: rules,
381
+ /** Nothing here is a verdict on the project; `worst` is only the highest state reached. */
382
+ worst: checks.some((check) => check.state === "warn") ? "warn" : "ok",
383
+ checks,
384
+ },
385
+ null,
386
+ 2,
387
+ ),
388
+ );
389
+ }
313
390
  }
314
391
 
315
392
  const [command, ...args] = process.argv.slice(2);
@@ -344,7 +421,7 @@ switch (command) {
344
421
  runScript("patterns.mjs", args);
345
422
  break;
346
423
  case "doctor":
347
- doctor();
424
+ doctor(args);
348
425
  break;
349
426
  case "--version":
350
427
  case "-v":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mehshekari/gsap-motion",
3
- "version": "3.6.0",
3
+ "version": "4.1.0",
4
4
  "description": "Install the gsap-creative-animation Agent Skill, and audit GSAP animation for leaks, jank and accessibility failures — with npx.",
5
5
  "keywords": [
6
6
  "gsap",
@@ -35,7 +35,7 @@
35
35
  "NOTICE.md"
36
36
  ],
37
37
  "engines": {
38
- "node": ">=18"
38
+ "node": ">=22"
39
39
  },
40
40
  "scripts": {
41
41
  "bundle": "node scripts/bundle-skill.mjs",
package/skill/SKILL.md CHANGED
@@ -2,9 +2,9 @@
2
2
  name: gsap-creative-animation
3
3
  description: Senior motion engineer for GSAP. Turns a visual or interaction idea into production-ready animation across DOM, SVG, text, images, scroll, pointer, layout and 3D — entrance reveals, scroll-driven sequences, pinned and horizontal scroll, magnetic cursors, kinetic typography, marquees, card stacks, page and modal transitions, loaders and site intros, SVG draw/morph/path, Flip layout transitions, and GSAP orchestration of Three.js/R3F. React and Next.js first; also vanilla JS, Vue, Svelte and Astro. Use whenever the user wants something animated, wants motion to feel cinematic/premium/alive/futuristic, wants an existing animation retimed, audited, or made cheaper, or mentions gsap, ScrollTrigger, DrawSVG, MorphSVG, MotionPath, SplitText, Flip, Observer, انیمیشن, موشن, اسکرول, لودینگ, مورفینگ.
4
4
  license: MIT. See LICENSE; GSAP itself is licensed separately, see NOTICE.md
5
- compatibility: Any Agent Skills client; written for Claude Code. GSAP 3.13+, where every plugin is free. React guidance targets @gsap/react 2.x, React 18/19 and the Next.js App Router. The audit scripts need Node.js 18+ and nothing else.
5
+ compatibility: Any Agent Skills client; written for Claude Code. GSAP 3.13+, where every plugin is free. React guidance targets @gsap/react 2.x, React 18/19 and the Next.js App Router. The audit scripts need Node.js 22+ and nothing else.
6
6
  metadata:
7
- version: "3.6.0"
7
+ version: "4.1.0"
8
8
  verified-gsap: "3.15"
9
9
  verified-gsap-react: "2.1"
10
10
  ---
@@ -60,33 +60,25 @@ Core principles:
60
60
 
61
61
  ## Commands
62
62
 
63
- | Command | Category | Description | Reference |
64
- |---|---|---|---|
65
- | `animate [target]` | Build | General entry — analyse the intent, then route | [reference/routing.md](reference/routing.md) |
66
- | `reveal [target]` | Build | Entrance and in-view reveals, staggers, masks | [preset/reveal.md](preset/reveal.md) |
67
- | `scroll [target]` | Build | Scroll-driven sequences, pin, scrub, horizontal | [reference/scrolltrigger.md](reference/scrolltrigger.md) |
68
- | `text [target]` | Build | Kinetic typography, split reveals, scramble | [reference/text.md](reference/text.md) |
69
- | `svg [file]` | Build | Draw, morph, path-follow, SVG timelines | [reference/svg.md](reference/svg.md) |
70
- | `interact [target]` | Build | Pointer, magnetic, cursor, drag, inertia | [reference/interaction.md](reference/interaction.md) |
71
- | `ambient [target]` | Build | Marquees, orbits, floating, breathing loops | [preset/marquee.md](preset/marquee.md) |
72
- | `transition [target]` | Build | Page, route, modal and layout transitions | [reference/flip.md](reference/flip.md) |
73
- | `loader` | Build | Loading and progress animation | [preset/loader.md](preset/loader.md) |
74
- | `intro` | Build | Site or section entry sequence | [preset/cinematic.md](preset/cinematic.md) |
75
- | `three [target]` | Build | GSAP orchestrating Three.js / R3F | [adapter/three.md](adapter/three.md) |
76
- | `audit [target]` | Evaluate | Check an existing animation: leaks, cost, a11y | [reference/performance.md](reference/performance.md) |
77
- | `tune [target]` | Refine | Retime, re-ease, fix rhythm — no new motion | [reference/motion-design.md](reference/motion-design.md) |
78
- | `strip [target]` | Fix | Remove motion that is not earning its place | [reference/motion-design.md](reference/motion-design.md) |
79
-
80
- Routing:
63
+ | Group | Commands |
64
+ |---|---|
65
+ | **Build** | `create` — the front door: classify the request, then route · `design` — plan only, no code · `build` — implement an approved plan · and the ten domain commands, unchanged: `reveal` `scroll` `text` `svg` `interact` `ambient` `transition` `loader` `intro` `three` |
66
+ | **Evaluate** | `audit` — a verdict, from the source · `inspect` — what a browser saw · `explain` — the architecture · `review` — what the motion measured |
67
+ | **Refine** | `tune` — timing and easing only · `strip` — remove motion that is not earning its place · `evolve` — audit, fix, tune, then offer patterns |
68
+ | **Knowledge** | `patterns` — what this project has proven, by status · `learn` — offer a verified finding as a candidate |
69
+
70
+ `animate` is an alias of `create`. Routing:
81
71
 
82
72
  - **No argument, or no target:** read [routing.md](reference/routing.md) and
83
73
  ask what should move. "Add some animation to my homepage" names no target:
84
74
  never invent one.
85
75
  - **A command, explicit or implied:** load its reference and follow it.
86
76
  - **A description** ("make this feel like Apple", "the hero is boring"): that is
87
- `animate`. Classify with routing.md's table, then load what that needs.
77
+ `create`. Classify with routing.md's table, then load what that needs.
88
78
  - **Two commands fit:** ask once. A scroll-driven text reveal is `scroll` and
89
79
  `text` together, not a coin toss.
80
+ - **`design` stops at the plan.** `build` implements one that was approved, and
81
+ needs it in hand. `evolve` improves what is there and never adds motion.
90
82
 
91
83
  ## The pipeline
92
84
 
@@ -119,7 +111,8 @@ intent → precedent → targets → trigger → motion language → technique
119
111
 
120
112
  For anything beyond a one-line tweak, answer in this shape, each part short:
121
113
 
122
- - **Analysis** — what moves, why, in what style, on what trigger.
114
+ - **Analysis** — what moves, why, on what trigger, and the intent you read it
115
+ as, with the values that follow.
123
116
  - **Motion strategy** — the sequence, as numbered beats.
124
117
  - **Architecture** — timeline shape, plugins and why each is needed, refs,
125
118
  handlers, what cleans up.
@@ -43,14 +43,14 @@ default.
43
43
  | Ease | Feel | Use |
44
44
  |---|---|---|
45
45
  | `power2.out` | quick start, soft landing | the default for entrances |
46
- | `power3.out` / `power4.out` | sharper, more expensive-feeling | premium, cinematic |
46
+ | `power3.out` / `power4.out` | sharper, more expensive-feeling | see intents |
47
47
  | `power2.in` | gathers then leaves | exits |
48
48
  | `power2.inOut` | symmetric | moves between two resting states |
49
49
  | `none` | mechanical, constant | **anything looping**, scrub, marquee |
50
- | `back.out(1.4)` | overshoot | playful — never on more than one thing at a time |
50
+ | `back.out(1.4)` | overshoot | playful — never two at once |
51
51
  | `elastic.out(1, 0.4)` | wobble | rarely; reads as a toy |
52
- | `sine.inOut` | gentle breath | ambient loops, pulses |
53
- | `expo.out` | dramatic arrival | one hero moment, not a list |
52
+ | `sine.inOut` | gentle breath | ambient loops |
53
+ | `expo.out` | dramatic arrival | one hero moment |
54
54
 
55
55
  Two failures worth naming. **An ease other than `none` on a repeating tween**
56
56
  puts a visible stutter at the seam — the loop decelerates into its own restart.
@@ -119,16 +119,21 @@ takeover — in [routing.md](routing.md).
119
119
 
120
120
  ## Reading a request
121
121
 
122
- | They say | They usually mean |
123
- |---|---|
124
- | "smoother" | longer duration, softer ease — or a cost problem; check which |
125
- | "snappier" | shorter duration, `power3.out`, less stagger |
126
- | "more premium" | fewer elements moving, longer eases, no overshoot |
127
- | "more playful" | overshoot on one element, tighter stagger |
128
- | "cinematic" | slower, sequential, one thing at a time, generous holds |
129
- | "subtle" | reduce distance before reducing duration |
130
- | "it feels cheap" | usually linear easing or uniform duration |
131
- | "it feels slow" | usually too sequential, not too long — try overlap first |
122
+ A feeling resolves to values from the tables above. **Say them in Analysis**: a
123
+ reviewer can argue with a reading, not with code.
124
+
125
+ | Intent | Duration | Ease | Competing | Deliberately still |
126
+ |---|---|---|---|---|
127
+ | cinematic | 600–900ms, 1s+ for one hero beat | `power3.out`, `expo.out` once | one | the background |
128
+ | premium | 350–500ms | `power3.out`, no overshoot | few | anything that would bounce |
129
+ | playful | 200–350ms | `back.out(1.4)`, one element | few | every second overshoot |
130
+ | alive | 1s+, looping | `sine.inOut`, `none` | ambient only | the content itself |
131
+ | technical | 200–500ms | `none`, `power2.out`, stepped | a grid, together | curves |
132
+ | organic | 600–900ms, overlapping | `sine.inOut`, soft | several | straight lines |
133
+
134
+ **They compose, one column at a time.** "Cinematic but fast" keeps the ease and
135
+ the focus, and moves down a duration band. [routing.md](routing.md) reads the
136
+ common adjustments.
132
137
 
133
138
  ## Validation
134
139
 
@@ -151,6 +151,25 @@ It exits 1 on a claim the evidence does not support, so it belongs in `lint`
151
151
  beside `audit`. Where a candidate has earned `validated`, it says so and leaves
152
152
  the promotion to you.
153
153
 
154
+ ### `patterns` and `learn`
155
+
156
+ - **`patterns`** lists what the project has proven, by status, and what has
157
+ earned a promotion. Asked to apply one, it uses `validated` and `canonical`
158
+ entries by default and a `candidate` only when you name it — a candidate is an
159
+ option to mention, not a default to reach for.
160
+ - **`learn`** offers a finding as a `candidate`, once you agree, and writes
161
+ nothing before that. It offers only what it has verified: the claim tested
162
+ with a minimal repro, the audit clean, and the entry carrying its concept,
163
+ what is fixed, when not to use it, its reduced-motion branch, and what it was
164
+ verified against. **An unverified finding is not offered at all**, and neither
165
+ command ever changes a status — `validated` is counted by `patterns check`,
166
+ and `canonical` is a person's signature.
167
+
168
+ A new shape is composed from what exists before anything is invented, and a
169
+ behaviour claim is verified one variable at a time before it is written down.
170
+ Something that turns out to be general is worth an upstream issue; something
171
+ mechanically detectable is worth an audit rule, with its fires and quiet cases.
172
+
154
173
  ---
155
174
 
156
175
  ## Rules worth knowing without a file
@@ -25,19 +25,34 @@ offer the menu below — grouped, so the question is easy to answer.
25
25
 
26
26
  Most requests are a feeling, not a command. Classify, then route.
27
27
 
28
- | What they say | What they mean | Route to |
28
+ **A named feeling routes here, and resolves to values** in
29
+ [motion-design](motion-design.md)'s *Reading a request* — duration, ease, how
30
+ many move at once, and what stays still. They compose, so a request that names
31
+ two is not a coin toss.
32
+
33
+ | What they say | Route to |
34
+ |---|---|
35
+ | "make it cinematic" | `intro` · [cinematic](../preset/cinematic.md) |
36
+ | "make it feel premium" | `tune` · [motion-design](motion-design.md) |
37
+ | "make it feel alive" | `ambient` · [marquee](../preset/marquee.md) |
38
+ | "futuristic / technical" | `text` + `svg` |
39
+ | "liquid / organic" | `svg` · [loader](../preset/loader.md) |
40
+ | "like Apple" | `scroll` · [scroll-storytelling](../preset/scroll-storytelling.md) |
41
+
42
+ **A diagnosis is not a feeling**, and routes on what is actually wrong:
43
+
44
+ | What they say | What it usually is | Route to |
29
45
  |---|---|---|
30
- | "make it cinematic" | slow, weighted, sequenced, one thing at a time | `intro` · [cinematic](../preset/cinematic.md) |
31
- | "make it feel premium" | fewer moving parts, longer eases, no bounce | `tune` · [motion-design](motion-design.md) |
32
- | "make it feel alive" | ambient continuous motion under static content | `ambient` · [marquee](../preset/marquee.md) |
33
- | "make it pop / bolder" | more contrast in timing, not more elements moving | `tune` · [motion-design](motion-design.md) |
34
- | "like Apple" | scroll-scrubbed, pinned, one idea per screen | `scroll` · [scroll-storytelling](../preset/scroll-storytelling.md) |
35
- | "futuristic / technical" | scramble, draw-on, mechanical easing, grid reveals | `text` + `svg` |
36
- | "liquid / organic" | morph, path motion, overlapping soft easing | `svg` · [loader](../preset/loader.md) |
37
- | "something crazy" | layered techniques with one clear concept | see below |
46
+ | "make it pop / bolder" | contrast in timing, not more elements moving | `tune` |
47
+ | "the hero is boring" | a hierarchy problem before a motion problem | `tune`, then `reveal` |
48
+ | "it feels janky" | cost or trigger | `audit` · [performance](performance.md) |
38
49
  | "smooth image reveal" | clip-path or mask, not opacity | [reveal](../preset/reveal.md) |
39
- | "the hero is boring" | hierarchy problem before a motion problem | `tune`, then `reveal` |
40
- | "it feels janky" | cost or trigger problem | `audit` · [performance](performance.md) |
50
+ | "something crazy" | layered techniques, one clear concept | see below |
51
+ | "smoother" | longer and softer — or a cost problem, so check which | `tune`, or `audit` |
52
+ | "snappier" | shorter, less stagger | `tune` |
53
+ | "subtle" | less distance, before less duration | `tune` |
54
+ | "it feels cheap" | linear easing, or one uniform duration | `tune` |
55
+ | "it feels slow" | usually too sequential, not too long: try overlap | `tune` |
41
56
 
42
57
  ### "Something crazy"
43
58
 
@@ -90,6 +105,47 @@ single hover takes [interaction](interaction.md) and nothing else from its row.
90
105
  Do not load a domain file "for completeness". Loading `adapter/three.md` for a
91
106
  button hover is the failure this map exists to prevent.
92
107
 
108
+ ## `create`, `design` and `build`
109
+
110
+ Three commands for one piece of work, separated so that the expensive part —
111
+ writing code — is not where a disagreement about the idea surfaces.
112
+
113
+ - **`create`** is the front door, and the alias `animate` still reaches it. It
114
+ classifies the request and routes: a domain command if one fits, `design`
115
+ first if the request is large or vague, straight to the work if it is small
116
+ and clear. A one-element hover does not need a plan; a site intro does.
117
+ - **`design`** produces a plan and **no code**: what moves and why, the beats in
118
+ order, the device and reduced-motion strategy, the techniques with the reason
119
+ each is needed, and what it will cost against the project's motion budget.
120
+ It ends by asking whether to build it. Writing code here defeats the point —
121
+ the plan is cheap to argue with, and an implementation is not.
122
+ - **`build`** implements a plan that was approved, and needs one in hand. Given
123
+ no plan it asks for one rather than inventing something to build; given a plan
124
+ it follows it through the pipeline, and says plainly where it departed and
125
+ why. A departure is not a failure — a plan meets the real markup for the first
126
+ time during `build` — but a silent one is.
127
+
128
+ ## `evolve`
129
+
130
+ Improve motion that already exists: `audit`, fix what it found, `tune` the
131
+ timing, then offer what was learned. It adds no new motion; that is `create`.
132
+
133
+ Its guards, all four of which exist because an improvement loop is the easiest
134
+ place for an assistant to do damage while feeling useful:
135
+
136
+ - **It needs a target, and keeps to one component or scene.** "Evolve the site"
137
+ is not a request, it is an invitation to churn.
138
+ - **It shows its plan before a large change.** A retime is not a large change;
139
+ restructuring a timeline is.
140
+ - **It stops after three rounds** and reports what is left, rather than trying a
141
+ fourth shape of the same idea.
142
+ - **It leaves approved motion alone.** Something the project accepted is not a
143
+ defect because this run would have done it differently.
144
+ - **It offers at most an `experimental` pattern, and never changes a status.**
145
+ See [project-rules.md](project-rules.md): standing comes from evidence, and a
146
+ loop that promotes its own output is the failure the maturity model exists to
147
+ prevent.
148
+
93
149
  ## `audit`
94
150
 
95
151
  **Run the checker first.** It is deterministic, it costs a second, and it finds
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * It drives Chrome over the DevTools Protocol through a pipe — `stdin` and
13
13
  * `stdout` of two extra file descriptors — rather than a WebSocket, because
14
- * Node 18 has no global WebSocket and this skill installs nothing. It uses
14
+ * Node 22 has no global WebSocket and this skill installs nothing. It uses
15
15
  * whatever Chrome the machine already has.
16
16
  *
17
17
  * Usage, from the project root:
@@ -17,7 +17,7 @@
17
17
  * needs a name, because those are the two a keen assistant would otherwise
18
18
  * grant itself. This command only reads and reports; it never edits the file.
19
19
  *
20
- * Node.js 18+, no dependencies.
20
+ * Node.js 22+, no dependencies.
21
21
  */
22
22
  import { readFileSync } from "node:fs";
23
23
  import { resolve } from "node:path";
@@ -19,7 +19,7 @@
19
19
  * for a reviewer looking at the filmstrip; this reports evidence, with the time
20
20
  * and the element, and leaves the verdict to them.
21
21
  *
22
- * Node.js 18+, no dependencies, whatever Chrome the machine already has.
22
+ * Node.js 22+, no dependencies, whatever Chrome the machine already has.
23
23
  */
24
24
  import { mkdirSync, writeFileSync } from "node:fs";
25
25
  import { join, resolve } from "node:path";