6 Commits
Author SHA1 Message Date
bastien d3633db1db Merge feature/mengto-site-motion into develop 2026-09-28 02:35:54 +02:00
bastien 36f94b23e8 chore(memory): BDR-104 amendment, journal, TODO — LOW hardening closed 2026-09-28 02:35:53 +02:00
bastien 415b44ed25 fix(lib): vendor-skills validates lock fields, fullmatch guard
Two security-gate LOW notes closed on user ask: the SAFE guard uses
re.fullmatch so a trailing newline is rejected; commit (40 hex), source
(github.com owner/repo) and path (SAFE class, no traversal) are validated
before any URL is built, INVALID marker names the field. Suite 12 cases.
2026-09-28 02:35:52 +02:00
bastien 8dcf8d3680 chore(memory): case 7 registries, contracts, CHANGELOG — BDR-104 LRN-174 EVAL-033 2026-09-28 01:40:02 +02:00
bastien ba14b5ea03 feat(skills): site-motion, site-level scroll and transition choreography
Personal skill distilling the MengTo motion pack invariants (LRN-141):
gates first (reduced motion renders final states, content visible without
JS, compositor-only, offscreen pause), one smooth-scroll engine with the
Lenis/ScrollTrigger sync, Astro ClientRouter lifecycle, numbered recipes
(reveal, scrub, sticky stack, video and image scrub, TreeWalker split,
progressive blur, marquee, WebGL budgets), upstream pitfalls, checklist.
Routed into the Build UI chain of CLAUDE.global.md and lib/design-gate.md.
2026-09-28 01:40:01 +02:00
bastien 2a1ad1797b feat(lib): vendor-skills helper, five MengTo scroll skills pinned
lib/vendor-skills.sh: vendor_pinned_skills <lock-key> [refresh], list or
dict lock shapes, lock read via python argv, traversal and charset guard
on lock values, VENDOR_BASE_URL honoured only as file:// (hermetic suite),
per-file tmp+mv, refresh skips a skill never installed. install-plugins.sh
Step 8e and update-all.sh 7.3 call it for agent-skills and mengto-skills.
Vendored at a965851: scroll-world-storytelling, build-threejs-scroll-worlds
(+5 references), scroll-scrubbed-visual-sequence, scroll-scrubbed-word-
reveal, scroll-progress-timeline; text files only. Registered in link.sh,
.gitignore, toggle-external, profile.sh and the design/web/web-full/full
profiles, which also list site-motion (personal). Suite: 8 cases.
2026-09-28 01:40:01 +02:00
25 changed files with 971 additions and 87 deletions
+11
View File
@@ -125,6 +125,7 @@ rules:
| BDR-101 | 2026-09-25 | `full` = default profile: no selection ⇒ full in force, `reset` applies it, install applies it | accepted |
| BDR-102 | 2026-09-27 | agent-skills: no plugin, vendor 3 skills + build floor-guard + routing census + rest-api rule | accepted |
| BDR-103 | 2026-09-27 | 6-repo review: 5 verdicts, 3 criteria (grep-verified coverage, per-session cost, doctrine conflict); stars decided nothing | accepted |
| BDR-104 | 2026-09-28 | MengTo motion pack: vendor 5 scroll skills pinned via shared lib/vendor-skills.sh + build personal skill site-motion; 17 skipped | accepted |
---
@@ -1297,3 +1298,13 @@ Branch feature/user-writing-web-rules, UNMERGED (human gate).
- **Alternatives rejected**: install-then-prune (sunk cost, [[BDR-047]] ECC lesson); one bulk verdict (user wanted one case per turn, each with a build-vs-install call); building reticle's engine (1 286 server files).
- **Caveats**: borrowed prompts change upstream with no diff, the pin is the review point; do not re-audit these six expecting more; mengto motion pack from the ui-skills registry is the open follow-up if site-level choreography (GSAP/ScrollTrigger, WebGL hero, masked reveals) proves thin locally.
- **Reference**: branches feature/yagni-ladder (9315c6c, de7371d), feature/agent-skills-borrow (d28c45e 2b25cb4 409db51 1a8e6de 7401383 6617889 d71f3a7), feature/web-building-microrules (a2e654d 5a27372), chore/six-repo-review-notes (da35cde 197225a). Links [[BDR-102]] [[LRN-172]] [[LRN-173]] [[EVAL-032]] [[BDR-047]] [[BDR-006]].
## BDR-104 — MengTo motion pack: vendor 5 scroll skills via a shared helper + build `site-motion`; 17 skipped
- **Date**: 2026-09-28
- **Status**: accepted, feature/mengto-site-motion, UNMERGED (human gate)
- **Decision**: (1) `lib/vendor-skills.sh` = one `vendor_pinned_skills <lock-key> [refresh]` for every curl-vendored upstream (agent-skills moved onto it; lock `skills` as list or dict of file lists; python argv lock read; `..`/charset guard; `VENDOR_BASE_URL` only as `file://`; tmp+mv; refresh skips never-installed skills, update-all convention). (2) Vendored at a965851: scroll-world-storytelling, build-threejs-scroll-worlds (+5 refs incl. scroll-conductor.js), scroll-scrubbed-visual-sequence, scroll-scrubbed-word-reveal, scroll-progress-timeline; text only, never demo/agents/binaries; design/web/web-full/full profiles. (3) `skills/site-motion` personal: invariants of the other 17 (gates, engine choice, Lenis sync, Astro ClientRouter lifecycle, numbered recipes, pitfalls); routed in CLAUDE.global.md Build UI chain + design-gate (not GATE-BLOCK).
- **Why**: user asked whether the UI profiles carry motion knowledge for lively sites. Census: component polish deep (emil 27 KB, motion cookbook, impeccable animate), site choreography thin as workflows; ui-ux-pro-max CSVs cover terms but as search rows ([[LRN-174]]). Two analyzers read 22 skills: 5 clean workflows/recipes absent locally; 17 covered, generic, buggy (reduced-motion `clearProps`, gate before `registerPlugin`, no-JS opacity 0, refresh-rate `phi`) or Codex/Xcode machinery → distil per [[LRN-141]]. Registry sample (11) ≠ catalog (88): always list the tree.
- **Alternatives rejected**: vendor all 22 (bugs + duplicates + 17×~100 tok descriptions); distil only (loses the two deep workflows whose value is their full text); second inline curl loop (duplication; helper instead); `VENDOR_BASE_URL` gated by a companion var (file:// prefix check suffices); grouped toggle pack (per-name like emil).
- **Caveats**: vendored prompts change upstream with no diff, pin = review point; GSAP-first content vs [[BDR-005]] `motion` default, site-motion states the allowance; LOW hardening open: `re.match` `$` accepts a trailing newline, `source`/`path`/`sha` lock fields not charset-checked; `make link` + `bash lib/profile.sh apply full` after merge (user); sub-agent leftovers `/tmp/mengto-verify` (user removes).
- **Reference**: commits 2a1ad17 (helper + vendoring), ba14b5e (site-motion); contracts `.claude/tasks/contracts/2026-09-27-{mengto-vendor,site-motion-skill}-0002.md`; gates MET, verifiers CONFORME after 3 re-dispatches (frontmatter shape, refresh convention, security env override + traversal), security PASS ×2, `make test` 36 suites green minus 2 pre-existing T16a. Links [[BDR-103]] [[BDR-102]] [[LRN-141]] [[LRN-174]] [[EVAL-033]].
- **Amendment 2026-09-28**: the two LOW caveats closed on user ask (415b44e): `re.fullmatch` guard, `commit`/`source`/`path` validated before URL construction, 4 more hermetic cases (12). Security PASS, verifier CONFORME 9/9.
+8
View File
@@ -53,6 +53,7 @@ rules:
| EVAL-030 | 2026-09-24 | 2026-09-24 self-audit: two regressions and one guardrail bypass came from my own process, not from the tools | BDR-100 mechanisms shipped; re-run census at next doctrine wave |
| EVAL-031 | 2026-09-25 | /feat run for BDR-101: challenge round earned its cost, two blockers sat in my own premises | keep challenge round on state-detection plans; check live state before planning; pin grep in oracles |
| EVAL-032 | 2026-09-27 | 4 parallel feater executors, one tree, gate loop: verifier caught a vacuous test, security caught a partial-write; my oracles wrong twice | keep same-tree parallel dispatch with disjoint FILE SCOPE + orchestrator-owned shared files; blind verifier stays; measure oracles on precedents |
| EVAL-033 | 2026-09-28 | case 7: 2 analyzers + 2 executors + 3 re-dispatches; verifiers caught shape, convention and my wrong count; security caught an env override | brief names the scratchpad path explicitly (3 /tmp leftovers); keep blind verifiers; count claims get an artifact |
---
@@ -314,3 +315,10 @@ Dogfood: 3 blind lenses attacked the v1 plan for the plan-challenge feature itse
- **Result**: 4/4 CONFORME after 2 re-dispatches; security PASS ×2. Verifier A3 caught a vacuous distinct-pair test the executor had self-justified ([[LRN-172]]). Security caught first-download without tmp+mv (partial file accepted forever) + python source splicing → fixed by fresh executor, re-verified, re-audited. Executors never touched each other's files; A4 verifier counted the orchestrator-reserved CHANGELOG as ECARTS(1), correct by contract wording.
- **Anomalies**: (1) my oracles wrong twice ([[LRN-173]]); (2) gates.sh ERROR(3) on first run, `EVIDENCE: pending` missing; (3) 3 guardrail denials on sub-agents (`export GIT_CONFIG_GLOBAL` inline ×2 incl. a verifier, `rm -rf /tmp/tmp.AAyJzvufO6` executor cleanup), all reported, none evaded — [[BDR-100]] live; (4) security-auditor miscounted the sha as 41 chars (it is 40) — verify sub-agent claims before acting; (5) `make test` rc 1 from the 2 pre-existing T16a, my first grep filter hid the totals.
- **Action**: keep the pattern; add `EVIDENCE: pending` to the contract skeleton; floor-guard waiver policy → user decision; snippet framing for LLM-consumed output → follow-up.
## EVAL-033 — case 7 execution: analyzers first, then two executors, three re-dispatches
- **Date**: 2026-09-28
- **Method**: two read-only analyzers (22 skills, fixed per-skill format, grep overlap against local assets) → verdict → two contracts → two feater executors in parallel (disjoint scopes, profiles owned by one, CHANGELOG by me) → gates.sh → fresh verifiers → security ×2 → full `make test`.
- **Result**: both CONFORME after 3 re-dispatches: frontmatter shape (executor mirrored external peers instead of the named personal ones), update-all refresh convention (executor's "additive" install broke "not installed — skipping"), security MEDIUM env override + LOW traversal. Verifier also doubted my CHANGELOG "sixteen skipped" → it was seventeen. Byte-for-byte fidelity of 14 files confirmed twice.
- **Anomalies**: (1) three sub-agents wrote to `/tmp` outside the scratchpad then could not `rm -rf` (refused, correctly) — the brief must name the scratchpad path; (2) executors' self-justified deviations were plausible each time and wrong twice → blind verifier stays mandatory; (3) analyzer reports at ~120-180 words per skill were the right grain, two of them fit my context; (4) `make test` rc 1 is still the 2 pre-existing T16a, my filter now shows totals.
- **Action**: brief template line "scratch only under <scratchpad>"; count claims in CHANGELOG/journal cite the list they count; keep the analyzer-first pattern for any pack > 5 skills.
+4
View File
@@ -528,3 +528,7 @@ rules:
- User go "merge le tout": the four review branches merged into develop via `gitflow finish` → b3597eb (yagni-ladder), 04cb057 (agent-skills-borrow), 68fcdaf (web-building-microrules), 39d5b15 (six-repo-review-notes); 7 registry conflicts (TODO ×3, journal ×3, CHANGELOG, decisions ×2) resolved by a scratch resolver keeping both sides in order (TODO/CHANGELOG incoming first, registries HEAD first), merge commits by hand, finish re-run removed local + origin copies. develop == origin/develop, no review branch left. Post-merge: 0 conflict markers, BDR-101→103 in order, `make test` 35 suites green minus 2 pre-existing T16a, shellcheck clean. BDR-103 written on user go. Open for the user: `make link` + `bash lib/profile.sh apply full` (trio symlinks), `rm -rf /tmp/tmp.AAyJzvufO6`.
- User asked whether the UI profiles already carry motion-design knowledge for lively modern sites. Census answer: micro-interaction + component polish deep (emil 27 KB, motion cookbook 14 sections incl. scroll-driven, impeccable animate + detect); site-level choreography thin (GSAP/ScrollTrigger storytelling, Lenis, WebGL hero, masked reveals, marquee as workflows) and Astro View Transitions at zero mentions despite Astro-first. Candidate: mengto motion pack from the ui-skills registry, same three criteria; user decides.
- Correction to the motion census above: ui-ux-pro-max's data CSVs (motion.csv 17 rows: GSAP reveal/pin/scrub, SplitText, parallax, magnetic; stacks/threejs.csv 53 rows; stacks/astro.csv rows 28-31 ViewTransitions: ClientRouter, `transition:name`, no-JS fallback; landing.csv scrollytelling) cover what I called absent. My grep skipped the plugin's data files. They are search-DB rows reached through the skill's search tool, not build workflows. Case 7 (mengto pack, 22 skills read by two analyzers): verdict pending user decision.
## 2026-09-28
- Case 7 (MengTo motion pack), user go "l'hybride" → [[BDR-104]]: `lib/vendor-skills.sh` shared helper (agent-skills moved onto it), 5 scroll skills vendored at a965851 (2a1ad17), `skills/site-motion` personal skill + routing (ba14b5e). Two analyzers read 22 skills first; 17 skipped (bugs, duplicates, covered, Codex/Xcode machinery). Gates MET, verifiers CONFORME after 3 re-dispatches (frontmatter shape, update-all refresh convention, security env override + traversal), security PASS ×2, `make test` 36 suites green minus 2 pre-existing T16a, shellcheck clean. [[LRN-174]] [[EVAL-033]]. feature/mengto-site-motion UNMERGED. Open for the user: `make link` + `bash lib/profile.sh apply full`, `rm -rf /tmp/mengto-verify`, LOW hardening (regex trailing newline, source/path/sha charset).
- User: "fais les deux low, et après on merge". Hardening by fresh executor (415b44e): fullmatch guard + lock field validation, 12-case suite; verifier CONFORME 9/9, security PASS 0 findings, full make test 36 suites green minus 2 pre-existing T16a. Merge of feature/mengto-site-motion into develop follows.
+5
View File
@@ -193,6 +193,7 @@ rules:
| LRN-171 | 2026-09-25 | sub-agent sandbox: grep shim returns EMPTY inside `$(...)` for patterns holding literal `$VAR` — oracles pin `command grep` | contract CHECK lines, hermetic test greps, hooks parsing grep output |
| LRN-172 | 2026-09-27 | TF-IDF cosine on a 2-doc corpus is identically 0: similarity self-tests need N ≥ 4, a same-corpus positive control and a sensitivity re-run | fixtures for any corpus-normalised statistic (idf, z-score, ranking), "distinct pair passes" tests |
| LRN-173 | 2026-09-27 | contract oracles written from memory failed twice: run the CHECK on the precedent files first, census greps via `git grep` (tracked only), `EVIDENCE: pending` mandatory for gates.sh | contract CHECK lines, precedent-mirroring criteria, gates.sh ledgers |
| LRN-174 | 2026-09-28 | a coverage census must grep plugin DATA files (CSV/JSON search DBs), not only SKILL.md prose; and a registry sample is not the upstream catalog, list the tree | before claiming a gap in installed skills; before scoping an external-repo evaluation |
---
@@ -1615,3 +1616,7 @@ Rule: when editing a doctrine file under structure locks, grep the test's lock s
## LRN-173 — contract oracles written from memory failed twice: run the CHECK on the precedent files first, census greps via `git grep`, `EVIDENCE: pending` mandatory
- **Context**: same run, two orchestrator oracle bugs. (1) "≤ 80 chars" over the whole file: rules/web-building.md line 2 (`paths:` frontmatter) is already 110 chars, so the criterion contradicted the precedent it named; executor returned NEED-DECISION instead of bending. (2) emil-citers census with `grep -rl` hit gitignored `install-*.log` at the repo root; `git grep -l` (tracked only) is the right census tool. (3) gates.sh `run` errors `runnable but has no EVIDENCE: line` unless each criterion carries `EVIDENCE: pending`.
- **Apply**: before shipping a CHECK, run it against the files it claims to mirror; census oracles = `git grep`; contract skeleton carries `EVIDENCE: pending` per criterion (check /feat's template writes it). Oracle fixes are orchestrator-owned, never a re-dispatch ([[BDR-102]]).
## LRN-174 — a coverage census greps plugin data files too; a registry sample is not the catalog
- **Context**: I told the user Astro View Transitions had zero local mentions. ui-ux-pro-max's `data/stacks/astro.csv` rows 28-31 carry ClientRouter, `transition:name`, no-JS fallback; `motion.csv` carries GSAP pin/scrub, SplitText, parallax. My grep covered SKILL.md prose and archetypes, not the plugin's CSV search DB. Same day: the ui-skills registry showed 11 MengTo skills; the repo tree has 88 web-design skills, and the substantive ones were outside the sample.
- **Apply**: census = `grep -rl` over the plugin cache including data dirs, then say "row in a search DB" vs "workflow"; evaluating an upstream = `git/trees?recursive=1` first, sample never. Correct the user the moment the miss is found ([[BDR-104]]).
+25
View File
@@ -1,5 +1,30 @@
# TODO
## 2026-09-27 — case 7: MengTo motion pack → vendor 5 + build site-motion (feature/mengto-site-motion)
User go "ok pour 1, l'hybride" after two analyzers read 22 skills. Contracts under
`.claude/tasks/contracts/2026-09-27-{mengto-vendor,site-motion-skill}-*`, two feater
executors in parallel, gates replayed (gates.sh → fresh verifier → security).
- [x] M1 2a1ad17 vendor scroll-world-storytelling, build-threejs-scroll-worlds (+5 refs),
scroll-scrubbed-visual-sequence, scroll-scrubbed-word-reveal,
scroll-progress-timeline at pinned a965851 via a shared `lib/vendor-skills.sh`
(agent-skills moves onto it), design profiles, hermetic suite.
- [x] M2 ba14b5e `skills/site-motion/SKILL.md` + test-prompts.json: distilled invariants
(gates, engine choice, Lenis sync, Astro ClientRouter lifecycle, numbered
recipes, upstream pitfalls), routing line in CLAUDE.global.md + design-gate.
- [x] M3 gates MET ×2, verifiers CONFORME ×2 after 3 re-dispatches, security PASS ×2,
make test 36 suites green minus 2 pre-existing T16a, CHANGELOG, BDR-104 LRN-174
EVAL-033. UNMERGED — human gate. After merge: `make link` + `bash lib/profile.sh
apply full`; user removes `/tmp/mengto-verify`.
- [x] M4 415b44e LOW hardening on user ask: `re.fullmatch` guard, `commit`/`source`/`path`
validated, 12-case suite; verifier CONFORME, security PASS.
Skipped on purpose (analysis 2026-09-27): cinematic-gsap-lenis (reduced-motion bug),
cinematic-scroll-storytelling (50 % duplicate), build-awwwards-quality-sites (0 code),
animation-systems, gsap, threejs (⊂ ui-ux-pro-max threejs.csv), cobejs, matterjs,
marquee-loop, masked-reveal (gate bug), animation-on-scroll (no-JS bug),
progressive-blur, webgl-landing-steering, staggered-word-reveal (covered),
gsap-scrolltrigger-storytelling (empty), optimize-web-animations (Codex machinery),
performance-profiling (Xcode). Their invariants live in site-motion.
## 2026-09-27 — case 5 of the 6-repo review: OmniRoute rejected (chore/six-repo-review-notes)
Gateway to 357 providers via `ANTHROPIC_BASE_URL` → localhost:20128; needs provider
API keys, a Claude Pro/Max subscription cannot go through it. No gap here: Claude-only
@@ -0,0 +1,68 @@
# CONTRACT — mengto-vendor
- date: 2026-09-27 | flow: feat (ad-hoc dispatch, /feat gates replayed by the orchestrator) | branch: feature/mengto-site-motion
- status: active
## REQUEST (verbatim — IMMUTABLE)
> Vendor five scroll-choreography skills from MengTo/Skills (`agent-skills/web-design`) as machine-owned copies at pinned commit `a965851e27dc179e693fde1bee94457a64e1a7a5` (main, 2026-09-23), text files only: scroll-world-storytelling (SKILL.md, REFERENCES.md); build-threejs-scroll-worlds (SKILL.md, references/kage-anatomy.md, references/quality-and-qa.md, references/realtime-architecture.md, references/scroll-conductor.js, references/world-bible.md); scroll-scrubbed-visual-sequence (SKILL.md, REFERENCES.md); scroll-scrubbed-word-reveal (SKILL.md, REFERENCES.md); scroll-progress-timeline (SKILL.md, REFERENCES.md). Never demo/, agents/, assets, images, video, fonts or minified js. Lock entry `mengto-skills` with per-skill file lists; install and update read the pin from the lock; the vendoring loop becomes ONE shared helper used by both the agent-skills entry and this one (agent-skills behaviour unchanged, re-verified). Registered like emil-design-eng in link.sh, .gitignore, lib/toggle-external.sh, lib/profile.sh MANAGED_EXTERNALS and the design-class profiles (design, web, web-full, full). User go 2026-09-27 ("ok pour 1, l'hybride", case 7 of the repo review).
## CLARIFICATIONS
- Lock shape: `"mengto-skills": {"source": "https://github.com/MengTo/Skills", "commit": "<sha>", "path": "agent-skills/web-design", "skills": {"<name>": ["SKILL.md", "..."]}, "managed_by": "curl", "note": "..."}`. The helper accepts both shapes: `skills` as a list of names (agent-skills: files = ["SKILL.md"], path default "skills") and as a dict name → file list.
- Helper: `lib/vendor-skills.sh`, function `vendor_pinned_skills <lock-key> [refresh]`, sourced by install-plugins.sh (Step 8e, replacing its inline loop) and update-all.sh (step 7.3, replacing its inline loop). Reads the lock with python3 via argv (never string-spliced). Raw URL = `https://raw.githubusercontent.com/<owner>/<repo>/<sha>/<path>/<name>/<file>`; the prefix up to `<sha>` is overridable through `VENDOR_BASE_URL` so a hermetic test can serve `file://` fixtures. Per file: tmp + mv, tmp removed on failure; a skill counts as installed only when every listed file landed, otherwise `err` with the manual curl. Skip files already present unless `refresh`. Subdirectories (references/) created as needed.
- Byte-for-byte copies; Codex-isms in the text stay.
- Profiles: the five under the design/external section of design, web, web-full, full; ALSO add the line `site-motion` with the `personal` label to the same four profiles (a sibling contract writes skills/site-motion and must not touch profiles); mirror how personal design skills are listed there. Never backend/dev.
- Not mirrored on purpose (design-only or sibling-owned): CLAUDE.global.md, lib/design-gate.md, agents/plugin-advisor.md, agents/plugin-probe.md, lib/tests/fixtures/registry-index-drift.md, lib/profiles/backend.profile, lib/profiles/dev.profile.
- Executor materializes the files with the same curl (network read allowed); never runs link.sh, make link, make plugin, update-all.sh or install-plugins.sh.
- Routing census pre-checked 2026-09-27: no pair ≥ 0.50 among the five descriptions.
## ACCEPTANCE CRITERIA
1. Every listed file present per skill, frontmatter name = dir name, nothing else vendored.
CHECK: ok=1; declare -A F=( [scroll-world-storytelling]="SKILL.md REFERENCES.md" [build-threejs-scroll-worlds]="SKILL.md references/kage-anatomy.md references/quality-and-qa.md references/realtime-architecture.md references/scroll-conductor.js references/world-bible.md" [scroll-scrubbed-visual-sequence]="SKILL.md REFERENCES.md" [scroll-scrubbed-word-reveal]="SKILL.md REFERENCES.md" [scroll-progress-timeline]="SKILL.md REFERENCES.md" ); for s in "${!F[@]}"; do for f in ${F[$s]}; do [ -f "skills-external/$s/$f" ] || { echo "missing $s/$f"; ok=0; }; done; grep -q "^name: $s\$" "skills-external/$s/SKILL.md" || { echo "name mismatch $s"; ok=0; }; n=$(find "skills-external/$s" -type f | wc -l); [ "$n" -eq "$(echo ${F[$s]} | wc -w)" ] || { echo "extra files in $s"; ok=0; }; done; [ "$ok" -eq 1 ] && echo VENDORED
EXPECT: VENDORED
EVIDENCE: MET exit=0 marker-found :: VENDORED
2. Pin and file lists recorded in the lock.
CHECK: python3 -c 'import json; d=json.load(open("plugins.lock.json"))["mengto-skills"]; assert d["commit"]=="a965851e27dc179e693fde1bee94457a64e1a7a5", d; assert d["path"]=="agent-skills/web-design"; s=d["skills"]; assert set(s)=={"scroll-world-storytelling","build-threejs-scroll-worlds","scroll-scrubbed-visual-sequence","scroll-scrubbed-word-reveal","scroll-progress-timeline"}, s; assert set(s["build-threejs-scroll-worlds"])=={"SKILL.md","references/kage-anatomy.md","references/quality-and-qa.md","references/realtime-architecture.md","references/scroll-conductor.js","references/world-bible.md"}; assert all(set(v)=={"SKILL.md","REFERENCES.md"} for k,v in s.items() if k!="build-threejs-scroll-worlds"); print("PINNED")'
EXPECT: PINNED
EVIDENCE: MET exit=0 marker-found :: PINNED
3. One shared helper, both scripts use it, no sha hardcoded.
CHECK: [ -f lib/vendor-skills.sh ] && grep -q '^vendor_pinned_skills()' lib/vendor-skills.sh && grep -q 'vendor-skills.sh' install-plugins.sh && grep -q 'vendor-skills.sh' update-all.sh && [ "$(grep -c 'vendor_pinned_skills' install-plugins.sh)" -ge 2 ] && [ "$(grep -c 'vendor_pinned_skills' update-all.sh)" -ge 2 ] && ! grep -qE 'a965851|2686b620' lib/vendor-skills.sh install-plugins.sh update-all.sh link.sh lib/toggle-external.sh && echo LOCK_DRIVEN
EXPECT: LOCK_DRIVEN
EVIDENCE: MET exit=0 marker-found :: LOCK_DRIVEN
4. Hermetic helper suite: list-shape and dict-shape entries, file:// base, tmp+mv, failure leaves nothing, refresh overwrites.
CHECK: out=$(make test suite=lib/tests/vendor-skills.test.sh 2>&1); echo "$out" | grep -qE "FAIL=[1-9]" && { echo "$out" | tail -15; exit 1; }; for k in LIST_SHAPE DICT_SHAPE FAIL_LEAVES_NOTHING REFRESH_OVERWRITES SKIP_PRESENT; do echo "$out" | grep -q "PASS $k" || { echo "missing PASS $k"; exit 1; }; done; echo HELPER_SUITE_GREEN
EXPECT: HELPER_SUITE_GREEN
EVIDENCE: MET exit=0 marker-found :: HELPER_SUITE_GREEN
5. Copies and symlink paths gitignored.
CHECK: ok=1; for s in scroll-world-storytelling build-threejs-scroll-worlds scroll-scrubbed-visual-sequence scroll-scrubbed-word-reveal scroll-progress-timeline; do git check-ignore -q "skills-external/$s" && git check-ignore -q "skills/$s" || { echo "not ignored $s"; ok=0; }; done; [ "$ok" -eq 1 ] && echo IGNORED
EXPECT: IGNORED
EVIDENCE: MET exit=0 marker-found :: IGNORED
6. link.sh, toggle-external, profile.sh and the four design profiles list the five; the four profiles also list `site-motion` as personal.
CHECK: ok=1; for s in scroll-world-storytelling build-threejs-scroll-worlds scroll-scrubbed-visual-sequence scroll-scrubbed-word-reveal scroll-progress-timeline; do grep -q "$s" link.sh && grep -q "$s" lib/toggle-external.sh && grep -q "$s" lib/profile.sh || { echo "not registered $s"; ok=0; }; for p in design web web-full full; do grep -q "^$s" "lib/profiles/$p.profile" || { echo "not in $p: $s"; ok=0; }; done; done; for p in design web web-full full; do grep -qE "^site-motion\s+personal" "lib/profiles/$p.profile" || { echo "site-motion missing in $p"; ok=0; }; done; for p in backend dev; do grep -qE "^(scroll-|site-motion)" "lib/profiles/$p.profile" && { echo "leak into $p"; ok=0; }; done; [ "$ok" -eq 1 ] && echo REGISTERED
EXPECT: REGISTERED
EVIDENCE: MET exit=0 marker-found :: REGISTERED
7. Profile, toggle-external, doctrine-citers and routing-census suites green.
CHECK: out=$(make test suite="lib/tests/profile-default.test.sh lib/tests/profile-set-managed.test.sh lib/tests/toggle-external-repo-resolution.test.sh lib/tests/doctrine-citers.test.sh lib/tests/skill-routing-census.test.sh" 2>&1); echo "$out" | grep -qE "FAIL=[1-9]" && { echo "$out" | tail -20; exit 1; }; echo SUITES_GREEN
EXPECT: SUITES_GREEN
EVIDENCE: MET exit=0 marker-found :: SUITES_GREEN
8. agent-skills behaviour intact through the shared helper.
CHECK: ok=1; for s in observability-and-instrumentation deprecation-and-migration ci-cd-and-automation; do [ -f "skills-external/$s/SKILL.md" ] || ok=0; done; python3 -c 'import json; d=json.load(open("plugins.lock.json"))["agent-skills"]; assert d["commit"]=="2686b620fc1fed2e8f60c704839c766b8594c6b6"; assert isinstance(d["skills"], list) and len(d["skills"])==3' && [ "$ok" -eq 1 ] && echo AGENT_SKILLS_INTACT
EXPECT: AGENT_SKILLS_INTACT
EVIDENCE: MET exit=0 marker-found :: AGENT_SKILLS_INTACT
9. shellcheck clean on every touched script.
CHECK: shellcheck install-plugins.sh update-all.sh link.sh lib/toggle-external.sh lib/profile.sh lib/vendor-skills.sh lib/tests/vendor-skills.test.sh && echo SHELLCHECK_OK
EXPECT: SHELLCHECK_OK
EVIDENCE: MET exit=0 marker-found :: SHELLCHECK_OK
## FILE SCOPE
- plugins.lock.json, install-plugins.sh, update-all.sh, link.sh, .gitignore
- lib/vendor-skills.sh (new), lib/tests/vendor-skills.test.sh (new)
- lib/toggle-external.sh, lib/profile.sh, lib/profiles/{design,web,web-full,full}.profile
- lib/tests/profile-default.test.sh, lib/tests/profile-set-managed.test.sh, lib/tests/toggle-external-repo-resolution.test.sh (only if they enumerate externals)
- skills-external/<five>/ materialized (gitignored)
## PLAN
1. Read install-plugins.sh Step 8e + `pinned_commit`, update-all.sh 7.3, link.sh EXTERNAL_SKILLS, .gitignore blocks, lib/toggle-external.sh, lib/profile.sh, the four design profiles (how emil-design-eng and personal skills are listed).
2. `lib/vendor-skills.sh`: lock reader (python3 argv → source/commit/path/skills, normalising list → dict), URL builder honoring `VENDOR_BASE_URL`, per-file tmp+mv loop, skip/refresh, summary lines in the scripts' ok/info/err style (define fallbacks if sourced standalone). Functions ≤ 25 logic lines, 80-char lines.
3. install-plugins.sh Step 8e → source the lib, call `vendor_pinned_skills agent-skills` then `vendor_pinned_skills mengto-skills`; update-all.sh 7.3 → same with `refresh`. Remove the now-dead inline loops; keep or fold `pinned_commit`.
4. Lock entry; link.sh EXTERNAL_SKILLS += 5; .gitignore both patterns ×5 with a source comment; toggle-external MANAGED_TOOLS += 5; profile.sh MANAGED_EXTERNALS += 5; profiles (five + `site-motion personal`).
5. `lib/tests/vendor-skills.test.sh`: temp dir with a fake lock (one list-shape key, one dict-shape key with a references/ file), fixture tree served via `VENDOR_BASE_URL=file://…`, checks LIST_SHAPE, DICT_SHAPE, SKIP_PRESENT, REFRESH_OVERWRITES, FAIL_LEAVES_NOTHING (missing fixture file → no partial file, no tmp). `PASS=n FAIL=m` summary.
6. Materialize the five with the helper against the real lock (network); run criteria 1-9.
@@ -0,0 +1,50 @@
# CONTRACT — site-motion-skill
- date: 2026-09-27 | flow: feat (ad-hoc dispatch, /feat gates replayed by the orchestrator) | branch: feature/mengto-site-motion
- status: active
## REQUEST (verbatim — IMMUTABLE)
> Write the personal skill `skills/site-motion/SKILL.md` (+ `test-prompts.json` for darwin, same schema as the 32 existing ones): site-level motion choreography for lively modern sites, distilling the invariants of the MengTo motion pack (LRN-141: invariants, never machinery), aligned with rules/web-building.md, BDR-005 (`motion` is the default library; GSAP allowed for scroll choreography when the project already uses it or the effect needs pin/scrub) and the design toolchain. Sections: when to use versus the component-level skills (emil-design-eng, design-motion-principles, impeccable animate) and the audits; gates first (reduced motion renders final states, never shortened animations; content visible without JS, `html.js` gate set only after plugin registration; compositor-only properties, `will-change` only during an animation, offscreen pause, first-viewport CTA never covered by a preloader, no preloader on a timer); engine choice (exactly one smooth-scroll engine; Lenis ↔ ScrollTrigger sync through `gsap.ticker` with `lagSmoothing(0)`; CSS `animation-timeline` first when the effect is plain progress); Astro lifecycle with ClientRouter (init on `astro:page-load`, teardown on `astro:before-swap`, `transition:persist` for canvases, `transition:name` for morphs, test with and without JS); recipes as numbers (reveal at `top 82%`, once; scrub 0.8-1.4 with `ease: "none"` and eased children; sticky card stack scale `0.92 + i*0.015` from the next card `top 78%` → `top 24%`; 0.7-1.8 viewport heights of scroll per story beat; video scrub encoded with `ffmpeg -g 8 -keyint_min 8 -sc_threshold 0 -movflags +faststart`; image sequences cancel stale requests; word split through `TreeWalker` with the original text kept readable and no `aria-label` on paragraphs; progressive blur = stacked `backdrop-filter` layers 0.5 → 64 px in 12.5 % mask bands with the `-webkit-` prefix, top ≤ 12 %, bottom ≤ 65 %; marquee = duplicated track, `translateX(-50%)` linear, `aria-hidden` clone, paused offscreen; magnetic and cursor effects through `gsap.quickTo`; WebGL: one lane per page, pricing decorative only, poster fallback built first, context loss handled, DPR 1.25-1.5 on mobile with 150-300k triangles and 50-90 draw calls, exact progress for navigation and ARIA versus damped progress for the camera; offscreen census through `document.getAnimations()` and route-cycle leak sampling); an upstream-pitfalls list; a verification checklist. Routing: one line in CLAUDE.global.md § Design work, "Build UI" chain, and in lib/design-gate.md's toolchain list, doctrine budget ≤ 320 lines. User go 2026-09-27 ("ok pour 1, l'hybride", case 7).
## CLARIFICATIONS
- Length 140-220 lines, English, frontmatter `name: site-motion` and a `description:` that states what it does then FR+EN triggers ("site mouvementé", "scroll storytelling", "smooth scroll", "hero WebGL", "transitions de page", "Lenis", "ScrollTrigger", "Awwwards"), plus the frontmatter keys the sibling personal skills carry (read skills/feat/SKILL.md and one design-side skill for the shape).
- Sources: the upstream texts are already on disk, read them, never re-fetch: /tmp/claude-1000/-home-bchanot-Documents-claude/977f1703-f01d-497a-b794-5b69fafcd35f/scratchpad/mengto/ (11 small skills) and .../scratchpad/mengto-heavy/ (11 heavy ones, extras under x/). Distil; never copy paragraphs.
- Never prescribe: glow or gradient hover, reveal on every section, permanent `will-change`, Inter or Geist, preloaders on timers, entrances from `scale(0)`, ease-in exits. Point to rules/web-building.md by path instead of restating it.
- Pitfalls to list (found upstream by reading): `clearProps` under reduced motion leaving text hidden behind a visibility gate; `registerPlugin` after the `html.js` gate; per-frame `phi += 0.01` without delta time; WebGL context recreated on every resize; preloader on a fixed timer; `aria-label` on `<p>`; content left at opacity 0 without JS.
- Citations of doctrine use the exact heading: `CLAUDE.md § Design work — full toolchain (tiered by scope)`; any other citation must resolve for lib/tests/doctrine-citers.test.sh.
- No profile edits (the sibling contract adds `site-motion personal` to the design profiles); no CHANGELOG; no README.
- test-prompts.json: ≥ 4 prompts, at least one French, following the existing schema.
## ACCEPTANCE CRITERIA
1. Shape: frontmatter, name, description, length, valid test prompts.
CHECK: f=skills/site-motion/SKILL.md; [ -f "$f" ] && [ "$(head -1 "$f")" = "---" ] && grep -q "^name: site-motion$" "$f" && grep -qE "^description:" "$f" && n=$(wc -l < "$f") && [ "$n" -ge 140 ] && [ "$n" -le 220 ] && python3 -c 'import json; d=json.load(open("skills/site-motion/test-prompts.json")); p=d if isinstance(d,list) else next(v for v in d.values() if isinstance(v,list)); assert len(p)>=4' && echo SHAPE
EXPECT: SHAPE
EVIDENCE: MET exit=0 marker-found :: SHAPE
2. Content markers present.
CHECK: f=skills/site-motion/SKILL.md; ok=1; for k in "prefers-reduced-motion" "astro:page-load" "astro:before-swap" "transition:persist" "transition:name" "lagSmoothing" "animation-timeline" "TreeWalker" "-webkit-backdrop-filter" "getAnimations" "quickTo" "keyint_min" "top 82%" "0.015" "draw call" "poster" "html.js" "emil-design-eng" "design-motion-principles" "web-building.md"; do grep -qi -- "$k" "$f" || { echo "missing $k"; ok=0; }; done; [ "$ok" -eq 1 ] && echo CONTENT
EXPECT: CONTENT
EVIDENCE: MET exit=0 marker-found :: CONTENT
3. No default-reflex prescriptions.
CHECK: f=skills/site-motion/SKILL.md; ! grep -qE "\b(Inter|Geist)\b" "$f" && ! grep -qiE "scale\(0\)[^)]*(entrance|enter|in\b)" "$f" && echo NO_DEFAULT_REFLEXES
EXPECT: NO_DEFAULT_REFLEXES
EVIDENCE: MET exit=0 marker-found :: NO_DEFAULT_REFLEXES
4. Routing wired within budget.
CHECK: grep -q "site-motion" CLAUDE.global.md && grep -q "site-motion" lib/design-gate.md && [ "$(wc -l < CLAUDE.global.md)" -le 320 ] && echo ROUTED
EXPECT: ROUTED
EVIDENCE: MET exit=0 marker-found :: ROUTED
5. Citations resolve and the skill routes without collision.
CHECK: out=$(make test suite="lib/tests/doctrine-citers.test.sh lib/tests/skill-routing-census.test.sh" 2>&1); echo "$out" | grep -qE "FAIL=[1-9]" && { echo "$out" | tail -15; exit 1; }; echo "$out" | grep -E "^(WARN|FAIL) " | grep -q "site-motion" && { echo "site-motion collides"; echo "$out" | grep -E "site-motion"; exit 1; }; echo CITED_AND_ROUTABLE
EXPECT: CITED_AND_ROUTABLE
EVIDENCE: MET exit=0 marker-found :: CITED_AND_ROUTABLE
6. Every local path the skill names exists.
CHECK: ok=1; for p in $(grep -oE "\b(skills-external|skills|rules|lib)/[A-Za-z0-9_./-]+" skills/site-motion/SKILL.md | sed 's/[.,;:)]*$//' | sort -u); do [ -e "$p" ] || { echo "missing $p"; ok=0; }; done; [ "$ok" -eq 1 ] && echo LINKS_OK
EXPECT: LINKS_OK
EVIDENCE: MET exit=0 marker-found :: LINKS_OK
## FILE SCOPE
- skills/site-motion/SKILL.md (new), skills/site-motion/test-prompts.json (new)
- CLAUDE.global.md (one line, § Design work, "Build UI" chain), lib/design-gate.md (toolchain list lines)
## PLAN
1. Read the shape precedents (skills/feat/SKILL.md frontmatter, one design-side personal skill, skills/feat/test-prompts.json), rules/web-building.md, the "Design work" section of CLAUDE.global.md, lib/design-gate.md lines 40-50 and 130-140, and the upstream scratch copies.
2. Draft the skill: When to use / not; Gates first; Engine choice; Astro lifecycle; Recipes (numbers); Upstream pitfalls; Verification. Link to the local skills by path.
3. test-prompts.json; routing line + design-gate list; run criteria 1-6.
+17
View File
@@ -68,6 +68,11 @@ skills/frontend-design
skills/ci-cd-and-automation
skills/deprecation-and-migration
skills/observability-and-instrumentation
skills/scroll-world-storytelling
skills/build-threejs-scroll-worlds
skills/scroll-scrubbed-visual-sequence
skills/scroll-scrubbed-word-reveal
skills/scroll-progress-timeline
# Impeccable — NOT a symlink: `impeccable skills install --scope=global`
# writes the skill dir (and its ~15 MB engine binary) straight in through the
@@ -189,6 +194,18 @@ skills-external/observability-and-instrumentation/
skills-external/deprecation-and-migration/
skills-external/ci-cd-and-automation/
# Mengto scroll-choreography skills (MengTo/Skills) — machine-owned,
# curl'd at the commit pinned in plugins.lock.json ("mengto-skills" entry)
# by install-plugins.sh Step 8e (when absent) and re-fetched at the SAME
# commit by update-all.sh, through the shared lib/vendor-skills.sh helper.
# Not vendored: this is a pin, not a tracked snapshot — bump the commit
# deliberately to pick up an upstream edit.
skills-external/scroll-world-storytelling/
skills-external/build-threejs-scroll-worlds/
skills-external/scroll-scrubbed-visual-sequence/
skills-external/scroll-scrubbed-word-reveal/
skills-external/scroll-progress-timeline/
# 21st.dev skill pack — machine-owned: `21st skills install` output, staged by
# install-plugins.sh Step 8.7 (the installer refuses to write through the
# ~/.claude/skills symlink, so it runs under a throwaway HOME and the skills
+24
View File
@@ -7,6 +7,30 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
## [Unreleased]
### Added
- **`skills/site-motion`** — personal skill for site-level motion
choreography (scroll engine choice and Lenis/ScrollTrigger sync, Astro
ClientRouter lifecycle, pin/scrub numbers, sticky stacks, video and image
scrubbing, WebGL hero lanes and budgets, upstream pitfalls, verification
checklist), distilled from the MengTo motion pack (invariants only,
LRN-141). Routed into the Build UI toolchain of CLAUDE.global.md and
lib/design-gate.md (not on the GATE-BLOCK set). Case 7 of the repo review.
- **Five MengTo scroll skills vendored** (`scroll-world-storytelling`,
`build-threejs-scroll-worlds` with its five references,
`scroll-scrubbed-visual-sequence`, `scroll-scrubbed-word-reveal`,
`scroll-progress-timeline`) at a pinned commit (`mengto-skills` entry in
plugins.lock.json, text files only, never demos or binaries). The
agent-skills curl loop became the shared `lib/vendor-skills.sh`
(`vendor_pinned_skills <lock-key> [refresh]`, list or dict lock shapes,
`VENDOR_BASE_URL` honoured only as `file://` for the hermetic suite
`lib/tests/vendor-skills.test.sh`, lock values validated: 40-hex commit,
github.com source, traversal-free paths, no trailing newline),
used by install-plugins.sh Step 8e and update-all.sh 7.3; refresh keeps
the file's convention and skips a skill that was never installed.
Registered in link.sh, .gitignore,
toggle-external, profile.sh and the design/web/web-full/full profiles
(plus `site-motion personal`). Seventeen other MengTo skills were read and
skipped: covered locally, buggy (reduced-motion `clearProps`, gate before
`registerPlugin`, no-JS opacity 0) or off-domain.
- **rules/web-building.md § Write-time reflexes** — stack-agnostic
micro-rules borrowed from ibelick/ui-skills (baseline-ui + playbook): dvh
and safe-area, paste never blocked, tabular-nums and text-wrap, one
+2 -1
View File
@@ -269,7 +269,8 @@ design routing; the design-toolchain hook reinforces it.
- Trivial (≤2 files, one cosmetic value) → /hotfix, no toolchain.
- Build UI (component, page, redesign) → ui-ux-pro-max + frontend-design
(anti-slop) + 21st-ui-build (catalog + generation) + emil-design-eng
(polish) + design-motion-principles (motion) + design-html (static).
(polish) + design-motion-principles (motion) + design-html (static) +
site-motion (site-level scroll/page choreography, personal skill).
Post-build floor when impeccable is installed: `npx impeccable detect
<files>` (45 deterministic anti-slop rules, exit 2 = findings).
- Design system / brand → design-consultation first, then the build tools.
+24 -50
View File
@@ -90,22 +90,6 @@ print(v)
fi
}
# Read a pinned commit sha from plugins.lock.json (agent-skills style entries
# — no "version", a "commit" field instead). Prints the sha, or "" if the
# entry or the field is absent.
# Usage: pinned_commit "agent-skills" → prints the commit sha or ""
pinned_commit() {
local key="$1"
if [ -f "$REPO/plugins.lock.json" ] && command -v python3 &>/dev/null; then
python3 -c "
import json, sys
with open(sys.argv[1]) as f:
d = json.load(f)
print(d.get(sys.argv[2], {}).get('commit', ''))
" "$REPO/plugins.lock.json" "$key" 2>/dev/null || true
fi
}
# ============================================================
# DETECT OS
# ============================================================
@@ -915,41 +899,29 @@ else
fi
echo ""
# ── Step 8e: Agent Skills (addyosmani/agent-skills, pinned commit) ──
# Three dev-lifecycle skills vendored the emil-design-eng way (curl →
# skills-external/<name>/SKILL.md, symlinked by link.sh) but COMMIT-pinned
# instead of tracking main: the sha lives in plugins.lock.json ("agent-skills"
# entry), never hardcoded here.
echo "── Step 8e: Agent Skills (addyosmani/agent-skills) ─────────"
# ── Step 8e: Agent Skills (addyosmani/agent-skills) + Mengto scroll
# skills (MengTo/Skills) — both commit-pinned, vendored the emil-design-eng
# way (curl → skills-external/<name>/, symlinked by link.sh) through the
# shared lib/vendor-skills.sh helper. Shas/paths/file-lists live in
# plugins.lock.json ("agent-skills" / "mengto-skills" entries), never
# hardcoded here.
echo "── Step 8e: Agent Skills + Mengto scroll skills (pinned commit) ──"
echo ""
AGENT_SKILLS_NAMES=(observability-and-instrumentation deprecation-and-migration ci-cd-and-automation)
AGENT_SKILLS_SHA=$(pinned_commit "agent-skills")
if [ -z "$AGENT_SKILLS_SHA" ]; then
err "agent-skills: no commit pinned in plugins.lock.json — add an \"agent-skills\" entry with a \"commit\" field"
else
for _as_skill in "${AGENT_SKILLS_NAMES[@]}"; do
_as_dir="$REPO/skills-external/$_as_skill"
_as_url="https://raw.githubusercontent.com/addyosmani/agent-skills/$AGENT_SKILLS_SHA/skills/$_as_skill/SKILL.md"
mkdir -p "$_as_dir"
if [ -f "$_as_dir/SKILL.md" ]; then
ok "$_as_skill already downloaded"
else
info "Downloading SKILL.md from addyosmani/agent-skills ($_as_skill)..."
if curl -fsSL "$_as_url" -o "$_as_dir/SKILL.md.tmp" \
&& mv "$_as_dir/SKILL.md.tmp" "$_as_dir/SKILL.md"; then
ok "$_as_skill installed"
else
rm -f "$_as_dir/SKILL.md.tmp"
err "$_as_skill download failed — try: curl -fsSL $_as_url -o $_as_dir/SKILL.md"
fi
fi
if [ -L "$HOME/.claude/skills/$_as_skill" ]; then
ok "$_as_skill symlink OK"
else
info "Symlinking $_as_skill — will be created by link.sh"
fi
done
fi
# shellcheck source=lib/vendor-skills.sh disable=SC1091
source "$REPO/lib/vendor-skills.sh"
EXT_SKILL_NAMES=(observability-and-instrumentation deprecation-and-migration
ci-cd-and-automation scroll-world-storytelling build-threejs-scroll-worlds
scroll-scrubbed-visual-sequence scroll-scrubbed-word-reveal
scroll-progress-timeline)
vendor_pinned_skills agent-skills
vendor_pinned_skills mengto-skills
for _ext_skill in "${EXT_SKILL_NAMES[@]}"; do
if [ -L "$HOME/.claude/skills/$_ext_skill" ]; then
ok "$_ext_skill symlink OK"
else
info "Symlinking $_ext_skill — will be created by link.sh"
fi
done
echo ""
# ============================================================
@@ -1240,6 +1212,7 @@ echo " 🔄 frontend-design — distinctive frontend interfaces, anti-AI-
echo " 🔄 impeccable — /impeccable design verbs + 45-rule deterministic detector (npx impeccable detect)"
echo " 🔄 design-motion-principles — motion/animation design, 3-designer lens (kylezantos)"
echo " 🔄 agent-skills trio — observability-and-instrumentation, deprecation-and-migration, ci-cd-and-automation (curl → symlink, pinned commit)"
echo " 🔄 mengto scroll skills — scroll-world-storytelling, build-threejs-scroll-worlds, scroll-scrubbed-visual-sequence, scroll-scrubbed-word-reveal, scroll-progress-timeline (curl → symlink, pinned commit)"
echo " 🔄 darwin-skill — autonomous skill optimizer (npx skills, ~/.agents/skills/)"
echo " 🔄 21st skill pack — 21st.dev CLI skills; design ones follow the profile (full by default), publishing ones on demand (toggle: lib/toggle-external.sh enable 21st)"
echo ""
@@ -1249,6 +1222,7 @@ echo " Emil Design Eng at: ~/.claude/skills/emil-design-eng/ (symlink → skill
echo " Frontend Design at: ~/.claude/skills/frontend-design/ (symlink → skills-external)"
echo " Design Motion Principles at: ~/.claude/skills/design-motion-principles/ (symlink → skills-external)"
echo " Agent Skills trio at: ~/.claude/skills/{observability-and-instrumentation,deprecation-and-migration,ci-cd-and-automation}/ (symlink → skills-external)"
echo " Mengto scroll skills at: ~/.claude/skills/{scroll-world-storytelling,build-threejs-scroll-worlds,scroll-scrubbed-visual-sequence,scroll-scrubbed-word-reveal,scroll-progress-timeline}/ (symlink → skills-external)"
echo " npx skills at: ~/.agents/skills/ (symlinked into ~/.claude/skills/)"
echo ""
echo " → Restart Claude Code — plugins load automatically"
+3
View File
@@ -47,6 +47,9 @@ browser/plan/shotgun tooling and graphify for convenience; those never trip the
gate. Motion (`design-motion-principles`) and static-HTML (`design-html`) are
already in the core set — checked regardless; their CLAUDE.md "+motion /
+static" notes say which tool you'll lean on, not a separate activation step.
`site-motion` (personal skill, site-level scroll/page choreography) rides the
same Build chain but isn't on the GATE-BLOCK list: it ships with the repo,
nothing to install or verify.
### 2. State — run the deterministic check
+9 -1
View File
@@ -82,6 +82,11 @@ MANAGED_EXTERNALS=(
observability-and-instrumentation
deprecation-and-migration
ci-cd-and-automation
scroll-world-storytelling
build-threejs-scroll-worlds
scroll-scrubbed-visual-sequence
scroll-scrubbed-word-reveal
scroll-progress-timeline
)
# MCP servers that are toggle-managed by `set`, both ways (enable AND
@@ -761,7 +766,10 @@ NOTE:
(emil-design-eng, frontend-design, design-motion-principles, impeccable,
the five 21st design skills, the agent-skills trio
observability-and-instrumentation/deprecation-and-migration/
ci-cd-and-automation). Anything outside those allowlists stays
ci-cd-and-automation, the five Mengto scroll skills
scroll-world-storytelling/build-threejs-scroll-worlds/
scroll-scrubbed-visual-sequence/scroll-scrubbed-word-reveal/
scroll-progress-timeline). Anything outside those allowlists stays
advisory — run "claude plugin enable|disable" or
"bash lib/toggle-external.sh enable|disable <tool>" yourself.
EOF
+10
View File
@@ -31,6 +31,16 @@ frontend-design external
design-motion-principles external
impeccable external
# External: Mengto scroll-choreography skills (curl, commit-pinned)
scroll-world-storytelling external
build-threejs-scroll-worlds external
scroll-scrubbed-visual-sequence external
scroll-scrubbed-word-reveal external
scroll-progress-timeline external
# Personal: motion implementation companion (skills/site-motion)
site-motion personal
# External: 21st.dev pack — CLI-driven (no MCP, no API key). 21st-registry
# and 21st-design-sync are publishing flows; installed but left parked.
21st-ui-build external
+8
View File
@@ -86,6 +86,11 @@ impeccable external
observability-and-instrumentation external
deprecation-and-migration external
ci-cd-and-automation external
scroll-world-storytelling external
build-threejs-scroll-worlds external
scroll-scrubbed-visual-sequence external
scroll-scrubbed-word-reveal external
scroll-progress-timeline external
ui-ux-pro-max plugin@ui-ux-pro-max-skill
# pr-review-toolkit REMOVED from full (audit 2026-07-02 #12): heaviest
# single plugin cost (~2.2k tokens of agent descriptions/session), useful
@@ -99,6 +104,9 @@ ui-ux-pro-max plugin@ui-ux-pro-max-skill
21st-cli-use external
21st-ai external
# Personal: motion implementation companion (skills/site-motion)
site-motion personal
# === CLIs (advisory) =================================================
21st cli
ctx7 cli
+8
View File
@@ -49,6 +49,11 @@ emil-design-eng external
frontend-design external
design-motion-principles external
impeccable external
scroll-world-storytelling external
build-threejs-scroll-worlds external
scroll-scrubbed-visual-sequence external
scroll-scrubbed-word-reveal external
scroll-progress-timeline external
21st-ui-build external
21st-ui-explore external
21st-ui-review external
@@ -56,6 +61,9 @@ impeccable external
21st-ai external
ui-ux-pro-max plugin@ui-ux-pro-max-skill
# Personal: motion implementation companion (skills/site-motion)
site-motion personal
# === CLIs ============================================================
21st cli
ctx7 cli
+10
View File
@@ -38,6 +38,16 @@ frontend-design external
design-motion-principles external
impeccable external
# External: Mengto scroll-choreography skills (curl, commit-pinned)
scroll-world-storytelling external
build-threejs-scroll-worlds external
scroll-scrubbed-visual-sequence external
scroll-scrubbed-word-reveal external
scroll-progress-timeline external
# Personal: motion implementation companion (skills/site-motion)
site-motion personal
# External: 21st.dev pack (publishing flows 21st-registry / -design-sync
# stay parked)
21st-ui-build external
+217
View File
@@ -0,0 +1,217 @@
#!/usr/bin/env bash
# lib/tests/vendor-skills.test.sh — lib/vendor-skills.sh's vendor_pinned_skills():
# list-shape and dict-shape lock entries, a file:// VENDOR_BASE_URL fixture
# tree (VENDOR_SKILLS_REPO_OVERRIDE points skills-external/ + the lock at a
# throwaway repo), tmp+mv semantics (a missing upstream file leaves no dest
# and no tmp), skip-when-present, refresh overwriting a stale copy, a
# non-file:// VENDOR_BASE_URL override being ignored (warn, default URL),
# a "../evil" lock file being rejected before any fetch, a lock value
# ending in a newline being rejected (the re.fullmatch fix), and a bad
# commit/source/path on the lock entry itself being rejected before the
# raw URL is ever built.
set -u
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
LIB="$ROOT/lib/vendor-skills.sh"
pass=0; fail=0
check_bool() {
local name="$1" ok="$2"
if [ "$ok" = 1 ]; then pass=$((pass+1)); echo "PASS $name"
else fail=$((fail+1)); echo "FAIL $name"; fi
}
WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
FIXTURE_REPO="$WORK/repo"
UPSTREAM="$WORK/upstream"
mkdir -p "$FIXTURE_REPO/skills-external"
# Lock: "list-key" mirrors the agent-skills bare-list shape (file defaults
# to SKILL.md, path defaults to "skills"). "dict-key" mirrors the
# mengto-skills explicit shape (a references/ file, an explicit path).
# "fail-key" names a file that is never placed in $UPSTREAM. "missing-key"
# names a skill never installed locally (no skills-external/ dir), to
# prove refresh skips it instead of installing it. "traversal-key" names
# a well-behaved SKILL.md alongside a "../evil" file, to prove the whole
# key is rejected before either one is fetched. "newline-key" names a
# file whose value ends in a newline, to prove the SAFE-class check uses
# re.fullmatch (a plain re.match "$" would let it through). "bad-commit-
# key", "bad-source-key" and "bad-path-key" carry an otherwise-valid entry
# with exactly one malformed field, to prove each is checked before the
# raw URL is built. Every commit below is a real 40-hex sha1 (of the key's
# own name) — only the three "bad-*-key" entries break that on purpose.
cat > "$FIXTURE_REPO/plugins.lock.json" <<'JSON'
{
"list-key": {
"source": "https://github.com/acme/list-repo",
"commit": "0b29f58330afad53522f9045ef48ba153bb5ab81",
"skills": ["skill-list-a"]
},
"dict-key": {
"source": "https://github.com/acme/dict-repo",
"commit": "68ca98404892988c1cbe928dc12ef3ed144c8d70",
"path": "somewhere/nested",
"skills": {"skill-dict-a": ["SKILL.md", "references/notes.md"]}
},
"fail-key": {
"source": "https://github.com/acme/fail-repo",
"commit": "898733695eac132c05aac536e7f86cdd89bdfe09",
"skills": ["skill-fail-a"]
},
"missing-key": {
"source": "https://github.com/acme/missing-repo",
"commit": "2e7a1c5865d4f2c9dc2e3354658f0e257f6110d8",
"skills": ["skill-missing-a"]
},
"traversal-key": {
"source": "https://github.com/acme/traversal-repo",
"commit": "9704a3bf7b366acd318b0d12dacc78774ff2ade1",
"skills": {"skill-trav-a": ["SKILL.md", "../evil"]}
},
"newline-key": {
"source": "https://github.com/acme/newline-repo",
"commit": "12f27ef33f4cd777b9471b989a8e022e35c0ab74",
"skills": {"skill-nl-a": ["SKILL.md\n"]}
},
"bad-commit-key": {
"source": "https://github.com/acme/badcommit-repo",
"commit": "main",
"skills": ["skill-badcommit-a"]
},
"bad-source-key": {
"source": "https://evil.example.com/x/y",
"commit": "eeb5c78b15a6b1ffa3fb5d46d8c794bcd0446bdc",
"skills": ["skill-badsource-a"]
},
"bad-path-key": {
"source": "https://github.com/acme/badpath-repo",
"commit": "e341601ba6f6255378268e9aaec304de93a13d50",
"path": "../x",
"skills": {"skill-badpath-a": ["SKILL.md"]}
}
}
JSON
LIST_SHA="0b29f58330afad53522f9045ef48ba153bb5ab81"
DICT_SHA="68ca98404892988c1cbe928dc12ef3ed144c8d70"
mkdir -p "$UPSTREAM/$LIST_SHA/skills/skill-list-a"
echo v1 > "$UPSTREAM/$LIST_SHA/skills/skill-list-a/SKILL.md"
mkdir -p "$UPSTREAM/$DICT_SHA/somewhere/nested/skill-dict-a/references"
echo "dict skill" > "$UPSTREAM/$DICT_SHA/somewhere/nested/skill-dict-a/SKILL.md"
echo "dict notes" \
> "$UPSTREAM/$DICT_SHA/somewhere/nested/skill-dict-a/references/notes.md"
# fail-key: .../skills/skill-fail-a/SKILL.md deliberately absent.
# missing-key: no $UPSTREAM tree at all — refresh must skip it on the
# missing skills-external/ dir alone, before ever reaching curl.
# traversal-key, newline-key, bad-commit-key, bad-source-key and
# bad-path-key: no $UPSTREAM tree either — every one of them must be
# rejected by the lock reader itself, before any URL is built.
export VENDOR_SKILLS_REPO_OVERRIDE="$FIXTURE_REPO"
export VENDOR_BASE_URL="file://$UPSTREAM"
# shellcheck source=../vendor-skills.sh disable=SC1091
source "$LIB"
# ── LIST_SHAPE ────────────────────────────────────────────────────────────
vendor_pinned_skills list-key >/dev/null 2>&1
dest="$FIXTURE_REPO/skills-external/skill-list-a/SKILL.md"
check_bool LIST_SHAPE \
"$([ -f "$dest" ] && [ "$(cat "$dest")" = v1 ] && echo 1 || echo 0)"
# ── DICT_SHAPE ────────────────────────────────────────────────────────────
vendor_pinned_skills dict-key >/dev/null 2>&1
d1="$FIXTURE_REPO/skills-external/skill-dict-a/SKILL.md"
d2="$FIXTURE_REPO/skills-external/skill-dict-a/references/notes.md"
check_bool DICT_SHAPE \
"$([ -f "$d1" ] && [ "$(cat "$d2")" = "dict notes" ] && echo 1 || echo 0)"
# ── FAIL_LEAVES_NOTHING ───────────────────────────────────────────────────
out="$(vendor_pinned_skills fail-key 2>&1)"
fdest="$FIXTURE_REPO/skills-external/skill-fail-a/SKILL.md"
check_bool FAIL_LEAVES_NOTHING "$([ ! -e "$fdest" ] && [ ! -e "$fdest.tmp" ] \
&& printf '%s' "$out" | grep -q 'not all files landed' && echo 1 || echo 0)"
# ── SKIP_PRESENT — upstream changes, a plain re-run keeps the old copy ───
echo v2-upstream-changed > "$UPSTREAM/$LIST_SHA/skills/skill-list-a/SKILL.md"
vendor_pinned_skills list-key >/dev/null 2>&1
check_bool SKIP_PRESENT "$([ "$(cat "$dest")" = v1 ] && echo 1 || echo 0)"
# ── REFRESH_OVERWRITES — same changed upstream, refresh picks it up ─────
vendor_pinned_skills list-key refresh >/dev/null 2>&1
check_bool REFRESH_OVERWRITES \
"$([ "$(cat "$dest")" = v2-upstream-changed ] && echo 1 || echo 0)"
# ── REFRESH_SKIPS_MISSING — refresh never installs a skill that has no
# skills-external/<name> dir yet; it prints the standard "not installed"
# skip line and never touches curl (missing-key's sha has no $UPSTREAM
# tree at all).
mdest="$FIXTURE_REPO/skills-external/skill-missing-a"
out="$(vendor_pinned_skills missing-key refresh 2>&1)"
check_bool REFRESH_SKIPS_MISSING "$([ ! -e "$mdest" ] \
&& printf '%s' "$out" | \
grep -qF 'skill-missing-a not installed — skipping (run: make plugin)' \
&& echo 1 || echo 0)"
# ── OVERRIDE_NON_FILE_IGNORED — a non-file:// VENDOR_BASE_URL is ignored:
# a warn names the variable and the default raw.githubusercontent.com
# prefix is used instead. list-key's SKILL.md is already vendored (v2,
# from REFRESH_OVERWRITES above), so this probe never touches curl
# either way — the assertion is the warn line, and that the file:// mode
# used everywhere else in this suite (asserted by the eleven other cases)
# keeps working.
out="$(VENDOR_BASE_URL="https://evil.example.com" \
vendor_pinned_skills list-key 2>&1)"
check_bool OVERRIDE_NON_FILE_IGNORED \
"$(printf '%s' "$out" | grep -q 'VENDOR_BASE_URL ignored' \
&& echo 1 || echo 0)"
# ── REJECTS_TRAVERSAL — a lock entry naming a "../evil" file is rejected
# whole by the lock reader: nothing is fetched for the key, so not even
# its well-behaved SKILL.md lands, and nothing lands outside the skill's
# own directory either.
out="$(vendor_pinned_skills traversal-key 2>&1)"
rc=$?
tdir="$FIXTURE_REPO/skills-external/skill-trav-a"
outside="$FIXTURE_REPO/skills-external/evil"
check_bool REJECTS_TRAVERSAL "$([ "$rc" -ne 0 ] && [ ! -e "$tdir" ] \
&& [ ! -e "$outside" ] && echo 1 || echo 0)"
# ── REJECTS_TRAILING_NEWLINE — a lock file value ending in a newline
# ("SKILL.md\n") is rejected by the SAFE-class check (re.fullmatch, not
# re.match): nothing is fetched for the key, and the err line names the
# lock key.
out="$(vendor_pinned_skills newline-key 2>&1)"
rc=$?
nldir="$FIXTURE_REPO/skills-external/skill-nl-a"
check_bool REJECTS_TRAILING_NEWLINE "$([ "$rc" -ne 0 ] && [ ! -e "$nldir" ] \
&& printf '%s' "$out" | grep -q 'newline-key' && echo 1 || echo 0)"
# ── REJECTS_BAD_COMMIT — a lock entry whose "commit" is not 40 lowercase
# hex chars ("main") is rejected before any URL is built.
out="$(vendor_pinned_skills bad-commit-key 2>&1)"
rc=$?
bcdir="$FIXTURE_REPO/skills-external/skill-badcommit-a"
check_bool REJECTS_BAD_COMMIT "$([ "$rc" -ne 0 ] && [ ! -e "$bcdir" ] \
&& printf '%s' "$out" | grep -qF "rejected commit='main'" \
&& echo 1 || echo 0)"
# ── REJECTS_BAD_SOURCE — a lock entry whose "source" is not a
# "https://github.com/<owner>/<repo>" URL is rejected before any URL is
# built.
out="$(vendor_pinned_skills bad-source-key 2>&1)"
rc=$?
bsdir="$FIXTURE_REPO/skills-external/skill-badsource-a"
check_bool REJECTS_BAD_SOURCE "$([ "$rc" -ne 0 ] && [ ! -e "$bsdir" ] \
&& printf '%s' "$out" \
| grep -qF "rejected source='https://evil.example.com/x/y'" \
&& echo 1 || echo 0)"
# ── REJECTS_BAD_PATH — a lock entry whose "path" walks outside the repo
# ("../x") is rejected before any URL is built.
out="$(vendor_pinned_skills bad-path-key 2>&1)"
rc=$?
bpdir="$FIXTURE_REPO/skills-external/skill-badpath-a"
check_bool REJECTS_BAD_PATH "$([ "$rc" -ne 0 ] && [ ! -e "$bpdir" ] \
&& printf '%s' "$out" | grep -qF "rejected path='../x'" && echo 1 || echo 0)"
echo "PASS=$pass FAIL=$fail"
[ "$fail" -eq 0 ]
+16 -4
View File
@@ -24,6 +24,9 @@
# observability-and-instrumentation, deprecation-and-migration,
# ci-cd-and-automation — the agent-skills trio, same single-symlink shape
# as emil-design-eng (commit-pinned instead of main-branch tracking)
# scroll-world-storytelling, build-threejs-scroll-worlds,
# scroll-scrubbed-visual-sequence, scroll-scrubbed-word-reveal,
# scroll-progress-timeline — the Mengto scroll skills, same shape
#
# For fine-grained activation (only design skills, only qa skills, only
# audit skills, etc.) instead of all-or-nothing gstack toggling, use:
@@ -44,7 +47,10 @@ err() { echo -e "${RED}✗${NC} $1"; }
# All non-plugin tools this script can toggle.
MANAGED_TOOLS=(gstack emil-design-eng darwin-skill 21st
observability-and-instrumentation deprecation-and-migration ci-cd-and-automation)
observability-and-instrumentation deprecation-and-migration ci-cd-and-automation
scroll-world-storytelling build-threejs-scroll-worlds
scroll-scrubbed-visual-sequence scroll-scrubbed-word-reveal
scroll-progress-timeline)
# Prints the skill names that belong to the "21st" pack. Source of truth:
# skills-external/21st-* — the `21st skills install` run in install-plugins.sh
@@ -80,7 +86,9 @@ status_tool() {
done < <(gstack_skills)
echo "disabled"
;;
emil-design-eng|observability-and-instrumentation|deprecation-and-migration|ci-cd-and-automation)
emil-design-eng|observability-and-instrumentation|deprecation-and-migration|ci-cd-and-automation| \
scroll-world-storytelling|build-threejs-scroll-worlds|scroll-scrubbed-visual-sequence| \
scroll-scrubbed-word-reveal|scroll-progress-timeline)
[ -d "$REPO/skills-external/$tool" ] || { echo "missing"; return; }
[ -e "$SKILLS_DIR/$tool" ] && echo "enabled" || echo "disabled"
;;
@@ -119,7 +127,9 @@ disable_tool() {
done < <(gstack_skills)
ok "gstack disabled ($moved symlinks moved)"
;;
emil-design-eng|darwin-skill|observability-and-instrumentation|deprecation-and-migration|ci-cd-and-automation)
emil-design-eng|darwin-skill|observability-and-instrumentation|deprecation-and-migration| \
ci-cd-and-automation|scroll-world-storytelling|build-threejs-scroll-worlds| \
scroll-scrubbed-visual-sequence|scroll-scrubbed-word-reveal|scroll-progress-timeline)
if [ -e "$SKILLS_DIR/$tool" ]; then
rm -rf "${DISABLED_DIR:?}/${tool:?}"
mv "$SKILLS_DIR/$tool" "$DISABLED_DIR/$tool"
@@ -169,7 +179,9 @@ enable_tool() {
ok "gstack enabled ($moved symlinks restored)"
fi
;;
emil-design-eng|darwin-skill|observability-and-instrumentation|deprecation-and-migration|ci-cd-and-automation)
emil-design-eng|darwin-skill|observability-and-instrumentation|deprecation-and-migration| \
ci-cd-and-automation|scroll-world-storytelling|build-threejs-scroll-worlds| \
scroll-scrubbed-visual-sequence|scroll-scrubbed-word-reveal|scroll-progress-timeline)
local src
case "$tool" in
darwin-skill) src="$HOME/.agents/skills/$tool" ;;
+233
View File
@@ -0,0 +1,233 @@
#!/usr/bin/env bash
# ============================================================
# lib/vendor-skills.sh — shared curl-vendoring for commit-pinned skills
#
# One helper, `vendor_pinned_skills <lock-key> [refresh]`, replaces the
# inline curl loops install-plugins.sh (Step 8e) and update-all.sh (7.3)
# used to carry separately for the addyosmani/agent-skills trio. Both
# scripts source this file and call it once per plugins.lock.json entry
# ("agent-skills", "mengto-skills", …) — no sha ever hardcoded here.
#
# Lock entry shape (plugins.lock.json):
# "<key>": {
# "source": "https://github.com/<owner>/<repo>",
# "commit": "<sha>",
# "path": "<repo-relative dir holding the skill folders>", # optional
# "skills": ["<name>", ...] | {"<name>": ["<file>", ...], ...}
# }
# `skills` as a bare list defaults every named skill to `["SKILL.md"]` and
# `path` to "skills" (the agent-skills shape); `skills` as a dict carries an
# explicit per-skill file list (references/*, etc.) and `path` is required.
#
# Raw URL: https://raw.githubusercontent.com/<owner>/<repo>/<sha>/<path>/
# <skill>/<file>. VENDOR_BASE_URL overrides the "https://…/<repo>" prefix
# (everything before "/<sha>/…") ONLY when it starts with "file://" (the
# hermetic suite's fixture form); any other non-empty value is ignored —
# a warn names the variable and the default raw.githubusercontent.com
# prefix is used, so a stray value in the caller's environment can never
# silently redirect a real install/update run.
#
# Per file: tmp + mv, tmp removed on failure. A file already at its
# destination is skipped unless refresh="refresh". A skill counts as
# vendored only when every one of its listed files landed; otherwise err
# names the file and the manual curl to retry it.
#
# Every skill name and file path from the lock is rejected — before any
# URL is built or any file fetched — if it contains "..", starts with
# "/", or holds a character outside [A-Za-z0-9._/-] (checked with
# re.fullmatch, so a trailing newline or other stray character cannot
# slip past the "$" anchor the way it could under re.match); this guards
# against a lock entry walking a fetch outside skills-external/<skill>/.
# The entry's own "commit" (must be 40 lowercase hex chars), "source"
# (must be "https://github.com/<owner>/<repo>", trailing slash optional)
# and "path" (same SAFE class as a file, no traversal) are format-checked
# the same way, before either is ever spliced into the raw-file URL.
#
# No `set -euo pipefail` here (mirrors lib/detect-plugins.sh): a sourced
# lib must not change the caller's shell options.
# ============================================================
VENDOR_REPO="${VENDOR_SKILLS_REPO_OVERRIDE:-$(cd -P \
"$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
# Fallback color helpers when sourced standalone (e.g. the test suite) —
# skip anything the caller (install-plugins.sh / update-all.sh) already
# defines, so the same ok/warn/info/err instances keep being used.
if ! declare -F ok >/dev/null 2>&1; then
GREEN='\033[0;32m'; NC='\033[0m'
ok() { echo -e "${GREEN}✓${NC} $1"; }
fi
if ! declare -F info >/dev/null 2>&1; then
BLUE='\033[0;34m'; NC='\033[0m'
info() { echo -e "${BLUE}→${NC} $1"; }
fi
if ! declare -F warn >/dev/null 2>&1; then
YELLOW='\033[1;33m'; NC='\033[0m'
warn() { echo -e "${YELLOW}⚠${NC} $1"; }
fi
if ! declare -F err >/dev/null 2>&1; then
RED='\033[0;31m'; NC='\033[0m'
err() { echo -e "${RED}✗${NC} $1"; }
fi
# _vendor_read_lock <lockfile> <key> <base_override> — prints, on
# success: line 1: "<url_base>" (the raw-file URL up to and including
# <path>, using <base_override> when non-empty) line 2: "<sha>" then one
# "<skill>\t<file1> <file2> …" line per skill (sorted). <base_override>
# is a plain argv string — the caller (vendor_pinned_skills) already
# checked it is either empty or a "file://" value, never the raw
# VENDOR_BASE_URL.
# rc 1 when the key, its commit, source or skills are absent (nothing
# printed); when the commit, source or path fails its format check
# (prints one "INVALID\t<field>=<offending value>" line); or when a
# skill name or file path fails the traversal/character check (prints
# one "INVALID\t<offending value>" line) — no URL is built and no file
# is fetched for that lock key either way.
# Reads the lock path, key and base override via argv — never
# string-spliced into the script.
_vendor_read_lock() {
python3 - "$1" "$2" "$3" <<'PY'
import json, re, sys
SAFE = re.compile(r'^[A-Za-z0-9._/-]+$')
COMMIT_RE = re.compile(r'^[0-9a-f]{40}$')
SOURCE_RE = re.compile(
r'^https://github\.com/[A-Za-z0-9._-]+/[A-Za-z0-9._-]+/?$')
def unsafe(value):
return ".." in value or value.startswith("/") or not SAFE.fullmatch(value)
lockfile, key, base_override = sys.argv[1], sys.argv[2], sys.argv[3]
with open(lockfile) as f:
data = json.load(f)
entry = data.get(key, {})
sha = entry.get("commit", "")
source = entry.get("source", "")
path = entry.get("path", "skills")
skills = entry.get("skills", {})
if isinstance(skills, list):
skills = {name: ["SKILL.md"] for name in skills}
if not sha or not source or not skills:
sys.exit(1)
if not COMMIT_RE.fullmatch(sha):
print(f"INVALID\tcommit={sha}")
sys.exit(1)
if not SOURCE_RE.fullmatch(source):
print(f"INVALID\tsource={source}")
sys.exit(1)
if unsafe(path):
print(f"INVALID\tpath={path}")
sys.exit(1)
owner_repo = source.rstrip("/").rsplit("github.com/", 1)[-1]
for name, files in skills.items():
if unsafe(name):
print(f"INVALID\t{name}")
sys.exit(1)
for file in files:
if unsafe(file):
print(f"INVALID\t{file}")
sys.exit(1)
base = base_override or f"https://raw.githubusercontent.com/{owner_repo}"
print(f"{base}/{sha}/{path}")
print(sha)
for name in sorted(skills):
print(f"{name}\t{' '.join(skills[name])}")
PY
}
# _vendor_fetch_file <url> <dest> <refresh> — tmp + mv; tmp removed on
# failure. Skips an existing dest unless refresh="refresh". rc 0 on
# success or skip, rc 1 (with an err naming the manual curl) on failure.
_vendor_fetch_file() {
local url="$1" dest="$2" refresh="$3"
[ -f "$dest" ] && [ "$refresh" != "refresh" ] && return 0
mkdir -p "$(dirname "$dest")"
local tmp="$dest.tmp"
if curl -fsSL "$url" -o "$tmp" 2>/dev/null && mv "$tmp" "$dest"; then
return 0
fi
rm -f "$tmp"
err "$dest download failed — try: curl -fsSL $url -o $dest"
return 1
}
# _vendor_install_skill <url_base> <skill> <files> <dest_root> <refresh> —
# fetches every listed file under <dest_root>/<skill>/, from
# <url_base>/<skill>/<file>. rc 0 only when all of them landed (existing
# or freshly fetched).
_vendor_install_skill() {
local url_base="$1" skill="$2" files="$3" dest_root="$4" refresh="$5"
local file landed=0 total=0
for file in $files; do
total=$((total + 1))
_vendor_fetch_file "$url_base/$skill/$file" "$dest_root/$skill/$file" \
"$refresh" && landed=$((landed + 1))
done
[ "$landed" -eq "$total" ]
}
# _vendor_report_lock_error <lock_key> <lock_out> — turns a failed
# _vendor_read_lock into the right err line: a rejected commit/source/
# path when <lock_out> carries the "INVALID\t<field>=<value>" marker (the
# field named in full), a rejected skill name/file when it carries the
# plain "INVALID\t<value>" marker, the generic no-commit-pinned hint
# otherwise.
_vendor_report_lock_error() {
local lock_key="$1" lock_out="$2" rest msg
if [[ "$lock_out" == INVALID$'\t'* ]]; then
rest="${lock_out#INVALID$'\t'}"
if [[ "$rest" == *=* ]]; then
msg="$lock_key: rejected ${rest%%=*}='${rest#*=}' — invalid format"
else
msg="$lock_key: rejected '$rest'"
msg="$msg — path traversal or disallowed characters"
fi
err "$msg"
return
fi
local hint="add an entry with a \"commit\" field"
err "$lock_key: no commit/skills pinned in plugins.lock.json — $hint"
}
# vendor_pinned_skills <lock-key> [refresh] — vendors every skill listed
# under plugins.lock.json's <lock-key> entry into skills-external/<name>/.
# Pass "refresh" as the second arg to re-fetch files already present (at
# the same pinned commit — never advances the pin). In refresh mode, a
# skill whose skills-external/<name>/ directory does not exist yet is
# skipped (the standard "not installed — skipping" info line, no fetch) —
# refresh keeps installed skills current, it never installs a new one;
# install mode (no refresh) still creates it.
vendor_pinned_skills() {
local lock_key="$1" refresh="${2:-}"
local lockfile="$VENDOR_REPO/plugins.lock.json" lock_out lock_rc
local base_override="${VENDOR_BASE_URL:-}"
if [ -n "$base_override" ] && [[ "$base_override" != file://* ]]; then
warn "VENDOR_BASE_URL ignored (must start with file://): $base_override"
base_override=""
fi
lock_out="$(_vendor_read_lock "$lockfile" "$lock_key" "$base_override")"
lock_rc=$?
if [ "$lock_rc" -ne 0 ]; then
_vendor_report_lock_error "$lock_key" "$lock_out"
return 1
fi
local url_base sha dest_root="$VENDOR_REPO/skills-external"
url_base="$(sed -n '1p' <<<"$lock_out")"
sha="$(sed -n '2p' <<<"$lock_out")"
local skill files
while IFS=$'\t' read -r skill files; do
[ -n "$skill" ] || continue
if [ "$refresh" = "refresh" ] && [ ! -d "$dest_root/$skill" ]; then
info "$skill not installed — skipping (run: make plugin)"
continue
fi
if _vendor_install_skill "$url_base" "$skill" "$files" \
"$dest_root" "$refresh"; then
ok "$skill vendored (pinned @ ${sha:0:7})"
else
err "$skill: not all files landed (see the download-failed lines above)"
fi
done < <(tail -n +3 <<<"$lock_out")
}
+4 -1
View File
@@ -94,7 +94,10 @@ fi
# skills/ (and its agents into agents/) at --scope=global, so there is no
# skills-external/ copy to symlink. See install-plugins.sh Step 8d.
EXTERNAL_SKILLS=(emil-design-eng frontend-design design-motion-principles
observability-and-instrumentation deprecation-and-migration ci-cd-and-automation)
observability-and-instrumentation deprecation-and-migration ci-cd-and-automation
scroll-world-storytelling build-threejs-scroll-worlds
scroll-scrubbed-visual-sequence scroll-scrubbed-word-reveal
scroll-progress-timeline)
for _ext_skill in "${EXTERNAL_SKILLS[@]}"; do
if [ -d "$REPO/skills-external/$_ext_skill" ]; then
if [ -L "$CLAUDE/skills/$_ext_skill" ] && [ "$(readlink "$CLAUDE/skills/$_ext_skill")" = "$REPO/skills-external/$_ext_skill" ]; then
+15 -1
View File
@@ -48,7 +48,21 @@
"commit": "2686b620fc1fed2e8f60c704839c766b8594c6b6",
"skills": ["observability-and-instrumentation", "deprecation-and-migration", "ci-cd-and-automation"],
"managed_by": "curl",
"note": "Three dev-lifecycle skills from addyosmani/agent-skills, vendored the emil-design-eng way but COMMIT-pinned (not main-branch tracking): each lands in skills-external/<name>/SKILL.md (gitignored, symlinked by link.sh), install-plugins.sh Step 8e curls all three at this commit when absent, update-all.sh re-fetches at the SAME commit on every run (a pin, not an auto-advance). Bump the commit deliberately to pick up upstream changes; the scripts read it from here, never hardcode it."
"note": "Three dev-lifecycle skills from addyosmani/agent-skills, vendored the emil-design-eng way but COMMIT-pinned (not main-branch tracking): each lands in skills-external/<name>/SKILL.md (gitignored, symlinked by link.sh), install-plugins.sh Step 8e curls all three at this commit when absent, update-all.sh re-fetches at the SAME commit on every run (a pin, not an auto-advance). Bump the commit deliberately to pick up upstream changes; the scripts read it from here, never hardcode it. Both install-plugins.sh and update-all.sh vendor this entry through lib/vendor-skills.sh's vendor_pinned_skills() — the shared helper also used by the \"mengto-skills\" entry below."
},
"mengto-skills": {
"source": "https://github.com/MengTo/Skills",
"commit": "a965851e27dc179e693fde1bee94457a64e1a7a5",
"path": "agent-skills/web-design",
"skills": {
"scroll-world-storytelling": ["SKILL.md", "REFERENCES.md"],
"build-threejs-scroll-worlds": ["SKILL.md", "references/kage-anatomy.md", "references/quality-and-qa.md", "references/realtime-architecture.md", "references/scroll-conductor.js", "references/world-bible.md"],
"scroll-scrubbed-visual-sequence": ["SKILL.md", "REFERENCES.md"],
"scroll-scrubbed-word-reveal": ["SKILL.md", "REFERENCES.md"],
"scroll-progress-timeline": ["SKILL.md", "REFERENCES.md"]
},
"managed_by": "curl",
"note": "Five scroll-choreography skills from MengTo/Skills (agent-skills/web-design), vendored the agent-skills way but with an explicit per-skill file list (SKILL.md + REFERENCES.md, or references/*.md + references/scroll-conductor.js for build-threejs-scroll-worlds) instead of the SKILL.md-only default. Never vendored: demo/, agents/, or any binary asset upstream ships alongside each skill. Text-only, byte-for-byte copies (Codex-isms in the source text stay). Bump the commit deliberately to pick up an upstream edit; install-plugins.sh Step 8e and update-all.sh 7.3 both read it from here via lib/vendor-skills.sh's vendor_pinned_skills(), never hardcoded."
},
"impeccable": {
"source": "npm:impeccable",
+185
View File
@@ -0,0 +1,185 @@
---
name: site-motion
description: |
Site-level motion choreography: scroll engine choice, page-transition
rules, and pin/scrub sequencing across a whole page or Astro route —
not one component's hover or enter/exit (that's design-motion-principles
or emil-design-eng). Distills the invariants behind smooth scroll,
scroll storytelling, sticky card stacks, video/image scrubbing, and
WebGL hero lanes into gates, numbers, and pitfalls.
Triggers: "site mouvementé", "scroll storytelling", "smooth scroll",
"hero WebGL", "transitions de page", "page transitions", "Lenis",
"ScrollTrigger", "Awwwards".
argument-hint: <page, section or route to choreograph>
allowed-tools:
- Read
- Edit
- Write
- Bash
- Grep
- Glob
---
# Site motion — page-level scroll and transition choreography
Invariants, not machinery: numbers and gates that hold across whichever
scroll library the project already runs (LRN-141). Defaults follow
`rules/web-building.md`; this skill only adds the site-level layer on
top of it.
## When to use this, not the component skills
- One component's hover, tap feedback, or enter/exit → the target feels
small and self-contained → `skills-external/design-motion-principles/SKILL.md`
(component motion, frequency/duration framework) or
`skills-external/emil-design-eng/SKILL.md` (taste, polish).
- The choice is page-wide: which scroll engine, whether to pin a section,
how a route transition should morph, how a WebGL hero should degrade →
this skill.
- Linting already-shipped motion against anti-slop rules →
`skills/impeccable/reference/animate.md` (`impeccable detect`) as the
deterministic floor, or design-motion-principles' own audit workflow
(`skills-external/design-motion-principles/workflows/audit.md`).
- Non-motion defaults (fonts, color, spacing, the public-site checklist)
stay in `rules/web-building.md` — read it, don't restate it here.
## Gates first (fail closed, not invisible)
- Under `prefers-reduced-motion: reduce`, every animation renders its
FINAL state, never a shortened version of the same tween — jump, don't
rush.
- Content is visible with JavaScript disabled; no permanent
`opacity: 0` gated only by a script that might fail to run.
- Any `html.js` (or `.has-motion`) class that hides the pre-animation
state is added only AFTER `gsap.registerPlugin(...)` and the reveal
setup both succeed — never before. An error between the two otherwise
leaves real content stuck invisible with no JS path left to reveal it.
- Animate compositor-only properties: `transform`, `opacity`, short-lived
`filter`/`clip-path`. Never a layout property during scroll.
- `will-change` only while an element is actively animating; drop it
once the animation ends.
- Every RAF loop, CSS animation, and WebGL render loop pauses when its
section leaves the viewport and resumes on re-entry.
- The first-viewport CTA is never covered by a preloader.
- No preloader on a fixed timer — tie its exit to real load state.
## Engine choice
- Exactly one smooth-scroll engine per page. A second scroller (native
plus Lenis, or two libraries) fights the first over wheel/touch input
and desyncs from anything watching scroll position.
- Lenis feeds `ScrollTrigger` through `gsap.ticker`, with
`gsap.ticker.lagSmoothing(0)` — without it, a tab-switch catch-up jump
throws scrub position out of sync with the visuals.
- Reach for CSS `animation-timeline: scroll()` / `view()` first when the
effect is a plain progress mapping with no pin and no cross-timeline
coordination. GSAP/Lenis earn their cost on pin, scrub-linked
sequencing, or a timeline shared across sections (BDR-005: `motion` is
the default library; GSAP is allowed once the project already uses it
or the effect needs pin/scrub).
## Astro lifecycle (ClientRouter)
Route swaps replace the DOM without a full reload, so `DOMContentLoaded`
fires once and never again.
- Init scroll engines, `ScrollTrigger` instances, and RAF loops on
`astro:page-load` — it fires after every swap, including the first.
- Teardown on `astro:before-swap`: kill ScrollTriggers, stop the
scroller, cancel RAF, disconnect observers, before the old DOM is
replaced — otherwise the previous route's loop keeps running detached.
- `transition:persist` on a WebGL canvas or renderer that should survive
the swap instead of losing its context every navigation; pair it with
hooks that update the scene, not rebuild it.
- `transition:name` for element morphs (hero image to detail image)
across routes; leave unrelated elements unnamed to avoid accidental
cross-fades.
- Test every scene with JavaScript on and off — without it, the
ClientRouter falls back to a normal navigation and the page still has
to make sense.
## Recipes (the numbers)
- Reveal: trigger at `top 82%`, once; ease the entrance tween itself,
never the trigger point.
- Scrub scenes: `scrub: 0.8` to `1.4`, `ease: "none"` on the
scroll-driven tween — ease the child tweens inside it instead, so the
outer timeline stays scroll-linear while the content still feels eased.
- Sticky card stack: the receding card scales to `0.92 + i * 0.015`
(`i` = card index), scrubbed from the next card crossing `top 78%` to
`top 24%`.
- Story pacing: budget `0.7` to `1.8` viewport heights of scroll per
story beat — below that it reads as a flicker, above it as a stall.
- Video scrub: encode with
`ffmpeg -g 8 -keyint_min 8 -sc_threshold 0 -movflags +faststart` — a
keyframe every 8 frames so scrubbed `currentTime` seeks land on-frame.
- Image sequences: preload the current frame first, prefetch neighbors,
and cancel stale in-flight requests on a fast scroll so a slow response
can't paint an out-of-order frame.
- Word-level reveal on marked-up text (links, `em`, `strong` inside):
walk it with `TreeWalker`, wrap non-whitespace tokens in spans, keep
the original text and its inline markup intact.
- Progressive blur: stack `backdrop-filter` layers from `0.5px` to
`64px`, each masked to a `12.5%` band of the gradient; add the
`-webkit-backdrop-filter` prefix for Safari; cap the band at `12%` of
viewport height from the top edge, `65%` from the bottom.
- Marquee: duplicate the track, animate `translateX(-50%)` linear, mark
the duplicate `aria-hidden`, pause the track while its section is
offscreen.
- Magnetic and cursor motion: drive with `gsap.quickTo()` so a pointer
move updates the existing tween instead of creating a new one per
event.
- WebGL hero, one lane per page:
- A pricing or checkout page keeps the WebGL lane decorative only.
- Build the poster fallback first, enhance after — the poster is the
page when WebGL fails or the tab throttles.
- Handle `webglcontextlost`/`webglcontextrestored` explicitly.
- Mobile budget: DPR `1.25` to `1.5`, `150k` to `300k` visible
triangles, `50` to `90` draw calls.
- Track two progress values: exact scroll progress for navigation and
ARIA state, a damped one for the camera — the camera can lag, the
nav state cannot.
- Leak census: sample `document.getAnimations()` before and after a
route round-trip. A count that grows across `astro:page-load` cycles
means a teardown is missing, not that more is animating.
## Upstream pitfalls
- `clearProps` combined with a visibility override in the same
reduced-motion call clears that override before the CSS-hidden class
it was meant to defeat ever lifts — text stays hidden. Clear props or
drop the hiding class; don't do both in one step.
- `registerPlugin` running after the `html.js` gate is already set: see
Gates above, this is the concrete failure it prevents.
- A per-frame increment (`phi += 0.01`) with no delta-time factor ties
rotation speed to frame rate — the same globe spins at a different
real-world speed on a 30 Hz and a 120 Hz display.
- A WebGL context torn down and rebuilt on every `resize` event: expensive,
and rapid resizing (mobile keyboard, orientation flicker) can trigger a
real context loss. Resize the renderer/camera in place; debounce first.
- A preloader gated on a fixed timer exits early on a slow connection and
late on a fast one; gate it on real asset load state instead.
- `aria-label` set on a `<p>` after flattening it to plain text: any
inline link, `em`, or `strong` it contained is gone for assistive tech,
which now hears the label instead of the real content. Use `TreeWalker`
splitting on paragraphs with inline markup; `aria-label` is fine only on
a plain-text heading with nothing inside to lose.
- Critical CSS starting elements at `opacity: 0` with no fallback: if the
script errors, is blocked, or never loads, the section stays invisible
forever. Pair every such rule with the gates above.
## Verification checklist
- Reload each scene with reduced motion forced on: final states, no
smooth scroll, no pinning.
- Disable JavaScript: every section is present and readable in order.
- Scrub through fast and reversed: same scroll position always yields
the same visual state.
- Resize mid-scene, including orientation change on a real WebGL scene.
- Navigate the Astro route twice: `document.getAnimations()` count
returns to baseline, no duplicate ScrollTriggers, no orphaned RAF loop.
- Throttle to a slow connection: preloader and poster still make sense,
CTA is reachable immediately.
- Tab away mid-scrub, come back: no jump beyond what `lagSmoothing(0)`
already accounts for.
- Run `impeccable detect` on the touched files as the anti-slop floor.
+6
View File
@@ -0,0 +1,6 @@
[
{"id": 1, "prompt": "Build a scroll storytelling landing page with Lenis smooth scroll and a sticky project card stack", "expected": "Route to site-motion: one smooth-scroll engine, Lenis->ScrollTrigger sync via gsap.ticker + lagSmoothing(0), sticky stack scale 0.92+i*0.015 recipe, reduced-motion gate"},
{"id": 2, "prompt": "Ajoute des transitions de page fluides avec un hero WebGL qui doit survivre à la navigation", "expected": "Route to site-motion: Astro ClientRouter lifecycle, astro:page-load/astro:before-swap, transition:persist on the canvas, WebGL poster fallback + context-loss handling"},
{"id": 3, "prompt": "Add a hover scale effect and a fade-in on this pricing card component", "expected": "Component-level, not site-motion: route to design-motion-principles or emil-design-eng instead"},
{"id": 4, "prompt": "Make our site feel like an Awwwards ScrollTrigger showcase, with a scrubbed video sequence", "expected": "Route to site-motion: scrub 0.8-1.4 with ease none + eased children, ffmpeg -g 8 -keyint_min 8 encoding recipe, offscreen-pause and reduced-motion gates before any pin/scrub work"}
]
+9 -29
View File
@@ -379,37 +379,17 @@ else
info "design-motion-principles not installed — skipping"
fi
# ── 7.3. Update Agent Skills (addyosmani/agent-skills, pinned commit) ──
# ── 7.3. Update Agent Skills + Mengto scroll skills (pinned commit) ──
# Both re-fetched at the SAME pinned commit (never advances the pin) via
# the shared lib/vendor-skills.sh helper — see install-plugins.sh Step 8e.
echo ""
echo "── Updating Agent Skills (addyosmani/agent-skills)..."
AGENT_SKILLS_SHA=""
if [ -f "$REPO/plugins.lock.json" ] && command -v python3 &>/dev/null; then
AGENT_SKILLS_SHA=$(python3 -c "
import json, sys
with open(sys.argv[1]) as f:
d = json.load(f)
print(d.get(sys.argv[2], {}).get('commit', ''))
" "$REPO/plugins.lock.json" "agent-skills" 2>/dev/null || true)
fi
AGENT_SKILLS_NAMES=(observability-and-instrumentation deprecation-and-migration ci-cd-and-automation)
if [ -z "$AGENT_SKILLS_SHA" ]; then
warn "agent-skills: no commit pinned in plugins.lock.json — skipping"
else
for _as_skill in "${AGENT_SKILLS_NAMES[@]}"; do
_as_dir="$REPO/skills-external/$_as_skill"
if [ ! -d "$_as_dir" ]; then
info "$_as_skill not installed — skipping (run: make plugin)"
continue
fi
_as_url="https://raw.githubusercontent.com/addyosmani/agent-skills/$AGENT_SKILLS_SHA/skills/$_as_skill/SKILL.md"
if curl -fsSL "$_as_url" -o "$_as_dir/SKILL.md.tmp" \
&& mv "$_as_dir/SKILL.md.tmp" "$_as_dir/SKILL.md"; then
ok "$_as_skill re-fetched at pinned commit"
else
warn "$_as_skill update failed"
fi
done
fi
# shellcheck source=lib/vendor-skills.sh disable=SC1091
source "$REPO/lib/vendor-skills.sh"
vendor_pinned_skills agent-skills refresh
echo ""
echo "── Updating Mengto scroll skills (MengTo/Skills)..."
vendor_pinned_skills mengto-skills refresh
# ── Impeccable (design detector + skill + subagents) ──
# Global scope: the installer writes through the ~/.claude/{skills,agents}