Merge feature/mengto-site-motion into develop
This commit is contained in:
@@ -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-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-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-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).
|
- **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.
|
- **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]].
|
- **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.
|
||||||
|
|||||||
@@ -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-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-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-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.
|
- **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.
|
- **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.
|
- **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.
|
||||||
|
|||||||
@@ -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 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.
|
- 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.
|
- 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.
|
||||||
|
|||||||
@@ -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-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-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-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
|
## 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`.
|
- **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]]).
|
- **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]]).
|
||||||
|
|||||||
@@ -1,5 +1,30 @@
|
|||||||
# TODO
|
# 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)
|
## 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
|
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
|
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
@@ -68,6 +68,11 @@ skills/frontend-design
|
|||||||
skills/ci-cd-and-automation
|
skills/ci-cd-and-automation
|
||||||
skills/deprecation-and-migration
|
skills/deprecation-and-migration
|
||||||
skills/observability-and-instrumentation
|
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`
|
# Impeccable — NOT a symlink: `impeccable skills install --scope=global`
|
||||||
# writes the skill dir (and its ~15 MB engine binary) straight in through the
|
# 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/deprecation-and-migration/
|
||||||
skills-external/ci-cd-and-automation/
|
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
|
# 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
|
# 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
|
# ~/.claude/skills symlink, so it runs under a throwaway HOME and the skills
|
||||||
|
|||||||
@@ -7,6 +7,30 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### 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
|
- **rules/web-building.md § Write-time reflexes** — stack-agnostic
|
||||||
micro-rules borrowed from ibelick/ui-skills (baseline-ui + playbook): dvh
|
micro-rules borrowed from ibelick/ui-skills (baseline-ui + playbook): dvh
|
||||||
and safe-area, paste never blocked, tabular-nums and text-wrap, one
|
and safe-area, paste never blocked, tabular-nums and text-wrap, one
|
||||||
|
|||||||
+2
-1
@@ -269,7 +269,8 @@ design routing; the design-toolchain hook reinforces it.
|
|||||||
- Trivial (≤2 files, one cosmetic value) → /hotfix, no toolchain.
|
- Trivial (≤2 files, one cosmetic value) → /hotfix, no toolchain.
|
||||||
- Build UI (component, page, redesign) → ui-ux-pro-max + frontend-design
|
- Build UI (component, page, redesign) → ui-ux-pro-max + frontend-design
|
||||||
(anti-slop) + 21st-ui-build (catalog + generation) + emil-design-eng
|
(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
|
Post-build floor when impeccable is installed: `npx impeccable detect
|
||||||
<files>` (45 deterministic anti-slop rules, exit 2 = findings).
|
<files>` (45 deterministic anti-slop rules, exit 2 = findings).
|
||||||
- Design system / brand → design-consultation first, then the build tools.
|
- Design system / brand → design-consultation first, then the build tools.
|
||||||
|
|||||||
+21
-47
@@ -90,22 +90,6 @@ print(v)
|
|||||||
fi
|
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
|
# DETECT OS
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -915,41 +899,29 @@ else
|
|||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# ── Step 8e: Agent Skills (addyosmani/agent-skills, pinned commit) ──
|
# ── Step 8e: Agent Skills (addyosmani/agent-skills) + Mengto scroll
|
||||||
# Three dev-lifecycle skills vendored the emil-design-eng way (curl →
|
# skills (MengTo/Skills) — both commit-pinned, vendored the emil-design-eng
|
||||||
# skills-external/<name>/SKILL.md, symlinked by link.sh) but COMMIT-pinned
|
# way (curl → skills-external/<name>/, symlinked by link.sh) through the
|
||||||
# instead of tracking main: the sha lives in plugins.lock.json ("agent-skills"
|
# shared lib/vendor-skills.sh helper. Shas/paths/file-lists live in
|
||||||
# entry), never hardcoded here.
|
# plugins.lock.json ("agent-skills" / "mengto-skills" entries), never
|
||||||
echo "── Step 8e: Agent Skills (addyosmani/agent-skills) ─────────"
|
# hardcoded here.
|
||||||
|
echo "── Step 8e: Agent Skills + Mengto scroll skills (pinned commit) ──"
|
||||||
echo ""
|
echo ""
|
||||||
AGENT_SKILLS_NAMES=(observability-and-instrumentation deprecation-and-migration ci-cd-and-automation)
|
# shellcheck source=lib/vendor-skills.sh disable=SC1091
|
||||||
AGENT_SKILLS_SHA=$(pinned_commit "agent-skills")
|
source "$REPO/lib/vendor-skills.sh"
|
||||||
if [ -z "$AGENT_SKILLS_SHA" ]; then
|
EXT_SKILL_NAMES=(observability-and-instrumentation deprecation-and-migration
|
||||||
err "agent-skills: no commit pinned in plugins.lock.json — add an \"agent-skills\" entry with a \"commit\" field"
|
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
|
else
|
||||||
for _as_skill in "${AGENT_SKILLS_NAMES[@]}"; do
|
info "Symlinking $_ext_skill — will be created by link.sh"
|
||||||
_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
|
fi
|
||||||
done
|
done
|
||||||
fi
|
|
||||||
echo ""
|
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 " 🔄 impeccable — /impeccable design verbs + 45-rule deterministic detector (npx impeccable detect)"
|
||||||
echo " 🔄 design-motion-principles — motion/animation design, 3-designer lens (kylezantos)"
|
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 " 🔄 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 " 🔄 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 " 🔄 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 ""
|
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 " Frontend Design at: ~/.claude/skills/frontend-design/ (symlink → skills-external)"
|
||||||
echo " Design Motion Principles at: ~/.claude/skills/design-motion-principles/ (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 " 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 " npx skills at: ~/.agents/skills/ (symlinked into ~/.claude/skills/)"
|
||||||
echo ""
|
echo ""
|
||||||
echo " → Restart Claude Code — plugins load automatically"
|
echo " → Restart Claude Code — plugins load automatically"
|
||||||
|
|||||||
@@ -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
|
gate. Motion (`design-motion-principles`) and static-HTML (`design-html`) are
|
||||||
already in the core set — checked regardless; their CLAUDE.md "+motion /
|
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.
|
+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
|
### 2. State — run the deterministic check
|
||||||
|
|
||||||
|
|||||||
+9
-1
@@ -82,6 +82,11 @@ MANAGED_EXTERNALS=(
|
|||||||
observability-and-instrumentation
|
observability-and-instrumentation
|
||||||
deprecation-and-migration
|
deprecation-and-migration
|
||||||
ci-cd-and-automation
|
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
|
# 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,
|
(emil-design-eng, frontend-design, design-motion-principles, impeccable,
|
||||||
the five 21st design skills, the agent-skills trio
|
the five 21st design skills, the agent-skills trio
|
||||||
observability-and-instrumentation/deprecation-and-migration/
|
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
|
advisory — run "claude plugin enable|disable" or
|
||||||
"bash lib/toggle-external.sh enable|disable <tool>" yourself.
|
"bash lib/toggle-external.sh enable|disable <tool>" yourself.
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
@@ -31,6 +31,16 @@ frontend-design external
|
|||||||
design-motion-principles external
|
design-motion-principles external
|
||||||
impeccable 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
|
# External: 21st.dev pack — CLI-driven (no MCP, no API key). 21st-registry
|
||||||
# and 21st-design-sync are publishing flows; installed but left parked.
|
# and 21st-design-sync are publishing flows; installed but left parked.
|
||||||
21st-ui-build external
|
21st-ui-build external
|
||||||
|
|||||||
@@ -86,6 +86,11 @@ impeccable external
|
|||||||
observability-and-instrumentation external
|
observability-and-instrumentation external
|
||||||
deprecation-and-migration external
|
deprecation-and-migration external
|
||||||
ci-cd-and-automation 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
|
ui-ux-pro-max plugin@ui-ux-pro-max-skill
|
||||||
# pr-review-toolkit REMOVED from full (audit 2026-07-02 #12): heaviest
|
# pr-review-toolkit REMOVED from full (audit 2026-07-02 #12): heaviest
|
||||||
# single plugin cost (~2.2k tokens of agent descriptions/session), useful
|
# 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-cli-use external
|
||||||
21st-ai external
|
21st-ai external
|
||||||
|
|
||||||
|
# Personal: motion implementation companion (skills/site-motion)
|
||||||
|
site-motion personal
|
||||||
|
|
||||||
# === CLIs (advisory) =================================================
|
# === CLIs (advisory) =================================================
|
||||||
21st cli
|
21st cli
|
||||||
ctx7 cli
|
ctx7 cli
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ emil-design-eng external
|
|||||||
frontend-design external
|
frontend-design external
|
||||||
design-motion-principles external
|
design-motion-principles external
|
||||||
impeccable 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-build external
|
||||||
21st-ui-explore external
|
21st-ui-explore external
|
||||||
21st-ui-review external
|
21st-ui-review external
|
||||||
@@ -56,6 +61,9 @@ impeccable external
|
|||||||
21st-ai external
|
21st-ai external
|
||||||
ui-ux-pro-max plugin@ui-ux-pro-max-skill
|
ui-ux-pro-max plugin@ui-ux-pro-max-skill
|
||||||
|
|
||||||
|
# Personal: motion implementation companion (skills/site-motion)
|
||||||
|
site-motion personal
|
||||||
|
|
||||||
# === CLIs ============================================================
|
# === CLIs ============================================================
|
||||||
21st cli
|
21st cli
|
||||||
ctx7 cli
|
ctx7 cli
|
||||||
|
|||||||
@@ -38,6 +38,16 @@ frontend-design external
|
|||||||
design-motion-principles external
|
design-motion-principles external
|
||||||
impeccable 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
|
# External: 21st.dev pack (publishing flows 21st-registry / -design-sync
|
||||||
# stay parked)
|
# stay parked)
|
||||||
21st-ui-build external
|
21st-ui-build external
|
||||||
|
|||||||
Executable
+217
@@ -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
@@ -24,6 +24,9 @@
|
|||||||
# observability-and-instrumentation, deprecation-and-migration,
|
# observability-and-instrumentation, deprecation-and-migration,
|
||||||
# ci-cd-and-automation — the agent-skills trio, same single-symlink shape
|
# ci-cd-and-automation — the agent-skills trio, same single-symlink shape
|
||||||
# as emil-design-eng (commit-pinned instead of main-branch tracking)
|
# 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
|
# For fine-grained activation (only design skills, only qa skills, only
|
||||||
# audit skills, etc.) instead of all-or-nothing gstack toggling, use:
|
# 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.
|
# All non-plugin tools this script can toggle.
|
||||||
MANAGED_TOOLS=(gstack emil-design-eng darwin-skill 21st
|
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:
|
# 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
|
# skills-external/21st-* — the `21st skills install` run in install-plugins.sh
|
||||||
@@ -80,7 +86,9 @@ status_tool() {
|
|||||||
done < <(gstack_skills)
|
done < <(gstack_skills)
|
||||||
echo "disabled"
|
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; }
|
[ -d "$REPO/skills-external/$tool" ] || { echo "missing"; return; }
|
||||||
[ -e "$SKILLS_DIR/$tool" ] && echo "enabled" || echo "disabled"
|
[ -e "$SKILLS_DIR/$tool" ] && echo "enabled" || echo "disabled"
|
||||||
;;
|
;;
|
||||||
@@ -119,7 +127,9 @@ disable_tool() {
|
|||||||
done < <(gstack_skills)
|
done < <(gstack_skills)
|
||||||
ok "gstack disabled ($moved symlinks moved)"
|
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
|
if [ -e "$SKILLS_DIR/$tool" ]; then
|
||||||
rm -rf "${DISABLED_DIR:?}/${tool:?}"
|
rm -rf "${DISABLED_DIR:?}/${tool:?}"
|
||||||
mv "$SKILLS_DIR/$tool" "$DISABLED_DIR/$tool"
|
mv "$SKILLS_DIR/$tool" "$DISABLED_DIR/$tool"
|
||||||
@@ -169,7 +179,9 @@ enable_tool() {
|
|||||||
ok "gstack enabled ($moved symlinks restored)"
|
ok "gstack enabled ($moved symlinks restored)"
|
||||||
fi
|
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
|
local src
|
||||||
case "$tool" in
|
case "$tool" in
|
||||||
darwin-skill) src="$HOME/.agents/skills/$tool" ;;
|
darwin-skill) src="$HOME/.agents/skills/$tool" ;;
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
@@ -94,7 +94,10 @@ fi
|
|||||||
# skills/ (and its agents into agents/) at --scope=global, so there is no
|
# skills/ (and its agents into agents/) at --scope=global, so there is no
|
||||||
# skills-external/ copy to symlink. See install-plugins.sh Step 8d.
|
# skills-external/ copy to symlink. See install-plugins.sh Step 8d.
|
||||||
EXTERNAL_SKILLS=(emil-design-eng frontend-design design-motion-principles
|
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
|
for _ext_skill in "${EXTERNAL_SKILLS[@]}"; do
|
||||||
if [ -d "$REPO/skills-external/$_ext_skill" ]; then
|
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
|
if [ -L "$CLAUDE/skills/$_ext_skill" ] && [ "$(readlink "$CLAUDE/skills/$_ext_skill")" = "$REPO/skills-external/$_ext_skill" ]; then
|
||||||
|
|||||||
+15
-1
@@ -48,7 +48,21 @@
|
|||||||
"commit": "2686b620fc1fed2e8f60c704839c766b8594c6b6",
|
"commit": "2686b620fc1fed2e8f60c704839c766b8594c6b6",
|
||||||
"skills": ["observability-and-instrumentation", "deprecation-and-migration", "ci-cd-and-automation"],
|
"skills": ["observability-and-instrumentation", "deprecation-and-migration", "ci-cd-and-automation"],
|
||||||
"managed_by": "curl",
|
"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": {
|
"impeccable": {
|
||||||
"source": "npm:impeccable",
|
"source": "npm:impeccable",
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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
@@ -379,37 +379,17 @@ else
|
|||||||
info "design-motion-principles not installed — skipping"
|
info "design-motion-principles not installed — skipping"
|
||||||
fi
|
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 ""
|
||||||
echo "── Updating Agent Skills (addyosmani/agent-skills)..."
|
echo "── Updating Agent Skills (addyosmani/agent-skills)..."
|
||||||
AGENT_SKILLS_SHA=""
|
# shellcheck source=lib/vendor-skills.sh disable=SC1091
|
||||||
if [ -f "$REPO/plugins.lock.json" ] && command -v python3 &>/dev/null; then
|
source "$REPO/lib/vendor-skills.sh"
|
||||||
AGENT_SKILLS_SHA=$(python3 -c "
|
vendor_pinned_skills agent-skills refresh
|
||||||
import json, sys
|
echo ""
|
||||||
with open(sys.argv[1]) as f:
|
echo "── Updating Mengto scroll skills (MengTo/Skills)..."
|
||||||
d = json.load(f)
|
vendor_pinned_skills mengto-skills refresh
|
||||||
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
|
|
||||||
|
|
||||||
# ── Impeccable (design detector + skill + subagents) ──
|
# ── Impeccable (design detector + skill + subagents) ──
|
||||||
# Global scope: the installer writes through the ~/.claude/{skills,agents}
|
# Global scope: the installer writes through the ~/.claude/{skills,agents}
|
||||||
|
|||||||
Reference in New Issue
Block a user