@ericrisco/rsc 1.1.5 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +72 -14
  2. package/manifest.json +888 -231
  3. package/package.json +3 -2
  4. package/schema/ecc-originality.json +16 -0
  5. package/schema/ecc-originality.u64 +0 -0
  6. package/scripts/build-manifest.js +43 -3
  7. package/scripts/doctor.js +47 -1
  8. package/scripts/install-apply.js +148 -31
  9. package/scripts/lib/capabilities.js +21 -0
  10. package/scripts/lib/domains.js +3 -3
  11. package/scripts/lib/manifest-file.js +4 -1
  12. package/scripts/originality-check.js +137 -0
  13. package/scripts/rsc.js +122 -14
  14. package/skills/design/SKILL.md +3 -3
  15. package/skills/design/references/design-systems.md +1 -1
  16. package/skills/design/references/inspiration-sources.md +2 -2
  17. package/skills/design/references/motion-and-interaction.md +2 -2
  18. package/skills/design-dna/evals/cases.yaml +1 -1
  19. package/skills/design-loop/SKILL.md +3 -3
  20. package/skills/design-loop/evals/cases.yaml +3 -3
  21. package/skills/expo/SKILL.md +1 -1
  22. package/skills/motion-craft/SKILL.md +125 -0
  23. package/skills/motion-craft/evals/cases.yaml +38 -0
  24. package/skills/motion-craft/scripts/verify.sh +75 -0
  25. package/skills/react/SKILL.md +2 -2
  26. package/skills/react-native/SKILL.md +1 -1
  27. package/skills/swift-ios/SKILL.md +2 -2
  28. package/skills/ui-engineering/SKILL.md +114 -0
  29. package/skills/ui-engineering/evals/cases.yaml +38 -0
  30. package/skills/variant-explorer/SKILL.md +93 -0
  31. package/skills/variant-explorer/evals/cases.yaml +38 -0
  32. package/targets/agent-catalog.js +220 -0
  33. package/targets/agent-sources.json +48 -0
  34. package/targets/agents.js +61 -13
  35. package/targets/commands.js +212 -0
  36. package/targets/memory.js +276 -0
  37. package/targets/opencode-memory-plugin.js +65 -0
  38. package/targets/session-memory-adapter.mjs +117 -0
  39. package/targets/session-memory-core.mjs +477 -0
  40. package/targets/session-memory.mjs +27 -0
  41. package/skills/animate/SKILL.md +0 -851
  42. package/skills/animate/evals/cases.yaml +0 -36
  43. package/skills/animate/references/recipes.md +0 -324
  44. package/skills/animate-expo/SKILL.md +0 -281
  45. package/skills/animate-expo/evals/cases.yaml +0 -36
  46. package/skills/animate-expo/references/recipes.md +0 -385
  47. package/skills/animation-vocabulary/SKILL.md +0 -189
  48. package/skills/animation-vocabulary/evals/cases.yaml +0 -31
  49. package/skills/apple-design/SKILL.md +0 -301
  50. package/skills/apple-design/evals/cases.yaml +0 -33
  51. package/skills/ask-sonner/SKILL.md +0 -104
  52. package/skills/ask-sonner/evals/cases.yaml +0 -33
  53. package/skills/ask-sonner/references/api.md +0 -64
  54. package/skills/design-eng/SKILL.md +0 -88
  55. package/skills/design-eng/evals/cases.yaml +0 -36
  56. package/skills/find-animation-opportunities/SKILL.md +0 -150
  57. package/skills/find-animation-opportunities/evals/cases.yaml +0 -31
  58. package/skills/improve-animations/SKILL.md +0 -126
  59. package/skills/improve-animations/evals/cases.yaml +0 -31
  60. package/skills/improve-animations/references/audit.md +0 -115
  61. package/skills/improve-animations/references/plan-template.md +0 -73
  62. package/skills/pick-ui-library/SKILL.md +0 -101
  63. package/skills/pick-ui-library/evals/cases.yaml +0 -33
  64. package/skills/pick-ui-library/references/component-galleries.md +0 -59
  65. package/skills/prototype/SKILL.md +0 -113
  66. package/skills/prototype/evals/cases.yaml +0 -31
  67. package/skills/prototype/references/picker.md +0 -197
  68. package/skills/review-animations/SKILL.md +0 -188
  69. package/skills/review-animations/evals/cases.yaml +0 -34
  70. package/skills/review-animations/references/standards.md +0 -187
  71. package/skills/review-animations/scripts/verify.sh +0 -164
  72. package/skills/write-swift/SKILL.md +0 -413
  73. package/skills/write-swift/evals/cases.yaml +0 -36
@@ -0,0 +1,137 @@
1
+ import {
2
+ existsSync, readdirSync, readFileSync, statSync, writeFileSync,
3
+ } from 'node:fs';
4
+ import { createHash } from 'node:crypto';
5
+ import { dirname, join } from 'node:path';
6
+ import { execFileSync } from 'node:child_process';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { stackAgents } from '../targets/agent-catalog.js';
9
+ import { allAgentNames } from '../targets/agents.js';
10
+ import { resolveCommands } from '../targets/commands.js';
11
+
12
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
13
+ const META_PATH = join(ROOT, 'schema', 'ecc-originality.json');
14
+ const DATA_PATH = join(ROOT, 'schema', 'ecc-originality.u64');
15
+ const NGRAM_SIZE = 8;
16
+ const EXEMPTION_CATEGORIES = new Set(['name', 'error', 'tool-command', 'language-term']);
17
+
18
+ export function normalizeTokens(text) {
19
+ return String(text || '').normalize('NFKC').toLowerCase().match(/[\p{L}\p{N}_+.#/-]+/gu) || [];
20
+ }
21
+
22
+ function fingerprint(tokens, offset, size = NGRAM_SIZE) {
23
+ return createHash('sha256').update(tokens.slice(offset, offset + size).join(' ')).digest('hex').slice(0, 16);
24
+ }
25
+
26
+ function fingerprintsOf(text, size = NGRAM_SIZE) {
27
+ const tokens = normalizeTokens(text);
28
+ const out = [];
29
+ for (let offset = 0; offset <= tokens.length - size; offset += 1) {
30
+ out.push({ fingerprint: fingerprint(tokens, offset, size), offset });
31
+ }
32
+ return out;
33
+ }
34
+
35
+ function walk(directory) {
36
+ if (!existsSync(directory)) throw new Error(`originality: corpus directory missing: ${directory}`);
37
+ return readdirSync(directory).sort().flatMap((name) => {
38
+ const path = join(directory, name);
39
+ return statSync(path).isDirectory() ? walk(path) : [path];
40
+ });
41
+ }
42
+
43
+ function catalogDocuments() {
44
+ const skillIds = readdirSync(join(ROOT, 'skills')).filter((id) => {
45
+ try { return statSync(join(ROOT, 'skills', id, 'SKILL.md')).isFile(); } catch { return false; }
46
+ });
47
+ const commands = resolveCommands({
48
+ target: 'cursor', skills: skillIds, agents: allAgentNames(), memoryMode: 'full',
49
+ });
50
+ return [
51
+ ...stackAgents().map((agent) => ({ id: `agent:${agent.name}`, text: `${agent.desc}\n${agent.body}` })),
52
+ ...commands.map((command) => ({ id: `command:${command.name}`, text: `${command.description}\n${command.body}` })),
53
+ ];
54
+ }
55
+
56
+ export function fingerprintCorpus(metaPath = META_PATH, dataPath = DATA_PATH) {
57
+ const metadata = JSON.parse(readFileSync(metaPath, 'utf8'));
58
+ if (metadata.ngramSize !== NGRAM_SIZE) throw new Error(`originality: unsupported n-gram size ${metadata.ngramSize}`);
59
+ for (const exemption of metadata.exemptions || []) {
60
+ if (!EXEMPTION_CATEGORIES.has(exemption.category) || !/^[0-9a-f]{16}$/.test(exemption.fingerprint || '')) {
61
+ throw new Error('originality: every exemption needs a valid fingerprint and closed category');
62
+ }
63
+ }
64
+ const bytes = readFileSync(dataPath);
65
+ if (bytes.length % 8 !== 0 || bytes.length / 8 !== metadata.fingerprintCount) {
66
+ throw new Error('originality: fingerprint corpus length does not match its receipt');
67
+ }
68
+ const digest = createHash('sha256').update(bytes).digest('hex');
69
+ if (digest !== metadata.dataSha256) throw new Error('originality: fingerprint corpus failed its integrity receipt');
70
+ const fingerprints = new Set();
71
+ for (let offset = 0; offset < bytes.length; offset += 8) fingerprints.add(bytes.subarray(offset, offset + 8).toString('hex'));
72
+ return { metadata, fingerprints };
73
+ }
74
+
75
+ export function checkCatalogOriginality({ corpus = fingerprintCorpus(), documents = catalogDocuments() } = {}) {
76
+ const exempt = new Set((corpus.metadata.exemptions || []).map((entry) => entry.fingerprint));
77
+ const matches = [];
78
+ for (const document of documents) {
79
+ for (const candidate of fingerprintsOf(document.text, corpus.metadata.ngramSize)) {
80
+ if (corpus.fingerprints.has(candidate.fingerprint) && !exempt.has(candidate.fingerprint)) {
81
+ matches.push({ document: document.id, tokenOffset: candidate.offset, fingerprint: candidate.fingerprint });
82
+ }
83
+ }
84
+ }
85
+ return {
86
+ ngramSize: corpus.metadata.ngramSize,
87
+ corpusRevision: corpus.metadata.revision,
88
+ documents: documents.length,
89
+ exemptions: exempt.size,
90
+ matches,
91
+ };
92
+ }
93
+
94
+ function refresh(corpusRoot) {
95
+ const files = ['agents', 'commands'].flatMap((directory) => walk(join(corpusRoot, directory)));
96
+ const hashes = new Set();
97
+ for (const path of files) {
98
+ for (const candidate of fingerprintsOf(readFileSync(path, 'utf8'))) hashes.add(candidate.fingerprint);
99
+ }
100
+ const sorted = [...hashes].sort();
101
+ const bytes = Buffer.alloc(sorted.length * 8);
102
+ sorted.forEach((hash, index) => Buffer.from(hash, 'hex').copy(bytes, index * 8));
103
+ writeFileSync(DATA_PATH, bytes);
104
+ const revision = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: corpusRoot, encoding: 'utf8' }).trim();
105
+ const metadata = {
106
+ schemaVersion: 1,
107
+ source: 'https://github.com/affaan-m/everything-claude-code',
108
+ revision,
109
+ generatedAt: new Date().toISOString().slice(0, 10),
110
+ included: ['agents/**', 'commands/**'],
111
+ ngramSize: NGRAM_SIZE,
112
+ normalization: 'NFKC lowercase; Unicode letters/numbers plus _+.#/-; whitespace joined',
113
+ fingerprintFormat: 'first 64 bits of SHA-256, sorted big-endian; collisions fail closed',
114
+ fingerprintCount: sorted.length,
115
+ dataSha256: createHash('sha256').update(bytes).digest('hex'),
116
+ exemptions: [],
117
+ };
118
+ writeFileSync(META_PATH, `${JSON.stringify(metadata, null, 2)}\n`);
119
+ return metadata;
120
+ }
121
+
122
+ function main() {
123
+ if (process.argv[2] === '--refresh') {
124
+ if (!process.argv[3]) throw new Error('Usage: node scripts/originality-check.js --refresh <ecc-checkout>');
125
+ const metadata = refresh(process.argv[3]);
126
+ console.log(`originality corpus refreshed (${metadata.fingerprintCount} fingerprints at ${metadata.revision})`);
127
+ return;
128
+ }
129
+ const result = checkCatalogOriginality();
130
+ if (result.matches.length) {
131
+ for (const match of result.matches) console.error(`${match.document}: shared ${result.ngramSize}-token fingerprint ${match.fingerprint} at token ${match.tokenOffset}`);
132
+ process.exit(1);
133
+ }
134
+ console.log(`originality OK (${result.documents} documents vs ${fingerprintCorpus().metadata.fingerprintCount} corpus fingerprints; ${result.exemptions} exemptions)`);
135
+ }
136
+
137
+ if (import.meta.url === `file://${process.argv[1]}`) main();
package/scripts/rsc.js CHANGED
@@ -4,7 +4,8 @@ import { detectTarget, resolveTargets, TARGETS } from '../targets/index.js';
4
4
  import { detectRepo } from './detect-repo.js';
5
5
  import { rank } from './consult.js';
6
6
  import { expandRecommends, toOutcomes, hasOutcome } from './lib/recommend.js';
7
- import { applyInstall, listInstalled, uninstall, syncInstalled, purge, collisions } from './install-apply.js';
7
+ import { applyInstall, listInstalled, listInstalledAgents, listInstalledCommands, uninstall, syncInstalled, purge, collisions } from './install-apply.js';
8
+ import { stackAgentNames } from '../targets/agents.js';
8
9
  import { doctor } from './doctor.js';
9
10
  import { say, select, pickFrom, banner, confirm, isInteractive } from './lib/ui.js';
10
11
  import { refreshRegistry, registryStatus } from './lib/registry.js';
@@ -20,6 +21,54 @@ const cmd = argv[0];
20
21
 
21
22
  const targetLabel = (id) => TARGETS.find((t) => t.id === id)?.label || id;
22
23
 
24
+ function requestedIds(start = 1) {
25
+ const valueFlags = new Set(['--target', '--profile', '--without']);
26
+ const ids = [];
27
+ for (let i = start; i < argv.length; i++) {
28
+ if (valueFlags.has(argv[i])) { i++; continue; }
29
+ if (argv[i].startsWith('--')) continue;
30
+ ids.push(argv[i]);
31
+ }
32
+ return ids;
33
+ }
34
+
35
+ function editDistance(a, b) {
36
+ const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
37
+ for (let i = 1; i <= a.length; i++) {
38
+ let diagonal = prev[0];
39
+ prev[0] = i;
40
+ for (let j = 1; j <= b.length; j++) {
41
+ const above = prev[j];
42
+ prev[j] = Math.min(prev[j] + 1, prev[j - 1] + 1, diagonal + (a[i - 1] === b[j - 1] ? 0 : 1));
43
+ diagonal = above;
44
+ }
45
+ }
46
+ return prev[b.length];
47
+ }
48
+
49
+ function classifyRequested(ids) {
50
+ const skills = new Set(loadManifest().skills.map((skill) => skill.id));
51
+ const agents = new Set(stackAgentNames());
52
+ const out = { skills: [], agents: [], unknown: [] };
53
+ for (const id of ids) {
54
+ if (skills.has(id)) out.skills.push(id);
55
+ else if (agents.has(id)) out.agents.push(id);
56
+ else out.unknown.push(id);
57
+ }
58
+ return out;
59
+ }
60
+
61
+ function reportUnknown(ids) {
62
+ if (!ids.length) return false;
63
+ const candidates = [...loadManifest().skills.map((skill) => skill.id), ...stackAgentNames()];
64
+ for (const id of ids) {
65
+ const close = [...candidates].sort((a, b) => editDistance(id, a) - editDistance(id, b) || a.localeCompare(b)).slice(0, 3);
66
+ console.error(`rsc: unknown skill or agent '${id}'. Closest: ${close.join(', ')}`);
67
+ }
68
+ process.exitCode = 1;
69
+ return true;
70
+ }
71
+
23
72
  // Commands that act ON one assistant. Only these stop when two are installed and no
24
73
  // flag says which: `purge` sweeps every target by design, and the catalog/consult
25
74
  // side barely touches one, so blocking them would be a regression, not a safeguard.
@@ -113,6 +162,8 @@ function printNextSteps(targets, ids) {
113
162
  say('\n Add something by hand anytime: npx @ericrisco/rsc add <skill>');
114
163
  say(' Browse the catalog / get picks: npx @ericrisco/rsc consult "whatever you need"');
115
164
  say('────────────────────────────────────────────────────────');
165
+ if (targets.includes('codex')) say(' Codex: review and trust the project lifecycle hook once with `/hooks`; until then memory is reported as requiring trust.');
166
+ if (targets.includes('cursor')) say(' Cursor: startup memory is assisted because its sessionStart hook is fire-and-forget; the installed always-on rule performs the read-before-action fallback.');
116
167
  printAgentHandoff();
117
168
  }
118
169
 
@@ -268,15 +319,12 @@ async function main() {
268
319
  case undefined:
269
320
  return wizard(f ? targets : null);
270
321
  case 'add': {
271
- // Positional args = skill ids; skip flags and any flag value (the token after a --flag).
272
- const requested = [];
273
- for (let i = 1; i < argv.length; i++) {
274
- if (argv[i].startsWith('--')) { i++; continue; }
275
- requested.push(argv[i]);
276
- }
277
- const ids = withDefaultSkillFloor(requested);
322
+ const requested = requestedIds();
323
+ const selected = classifyRequested(requested);
324
+ if (reportUnknown(selected.unknown)) return;
325
+ const ids = withDefaultSkillFloor(selected.skills);
278
326
  if (!argv.includes('--force') && !(await guardCollisions(targets, ids))) return;
279
- for (const t of targets) await applyInstall({ skillIds: ids, target: t });
327
+ for (const t of targets) await applyInstall({ skillIds: ids, agentIds: selected.agents, target: t });
280
328
  say(`✅ Installed for ${targets.join(', ')}: ${requested.join(', ')}`);
281
329
  return void say(' ↻ Reload/restart your assistant so the new skill activates.');
282
330
  }
@@ -328,14 +376,66 @@ async function main() {
328
376
  else say('\n(no harness wiki here — printed above only; run `harness` to keep a written record)');
329
377
  return;
330
378
  }
331
- case 'list':
332
- return void say(listInstalled({ target }).join('\n') || '(nothing installed)');
379
+ case 'list': {
380
+ const skills = listInstalled({ target }).map((id) => `skill\t${id}`);
381
+ const agents = listInstalledAgents({ target }).map((agent) => `agent\t${agent.id}\t${agent.source}${agent.skills.length ? `:${agent.skills.join(',')}` : ''}`);
382
+ const commands = listInstalledCommands({ target }).map((command) => `command\t${command.id}\t${command.path}`);
383
+ return void say([...skills, ...agents, ...commands].join('\n') || '(nothing installed)');
384
+ }
333
385
  case 'doctor': {
334
386
  const report = doctor({ target });
335
387
  if (argv.includes('--json')) return void say(JSON.stringify(report, null, 2));
336
388
  printContextBudget(report.contextBudget);
337
389
  return void say(JSON.stringify({ ...report, contextBudget: undefined }, null, 2));
338
390
  }
391
+ case 'memory': {
392
+ const sub = argv[1] || 'status';
393
+ const root = process.cwd();
394
+ if (sub === 'on' || sub === 'off') {
395
+ const { readManifest, writeManifest } = await import('./lib/manifest-file.js');
396
+ const current = readManifest(root) || { version: 1, targets: targets || [], skills: [], agents: [], ownSkills: [], optOuts: [] };
397
+ writeManifest(root, { ...current, memory: sub === 'off' ? false : { enabled: true } });
398
+ for (const t of targets) await syncInstalled({ target: t, cwd: root });
399
+ say(`rsc memory ${sub}: ${sub === 'on' ? 'enabled' : 'disabled'} for this project (${targets.join(', ') || 'declaration only'}).`);
400
+ return;
401
+ }
402
+ const M = await import('../targets/session-memory-core.mjs');
403
+ if (sub === 'status') {
404
+ const reports = targets.map((t) => doctor({ target: t, cwd: root }).memory);
405
+ return void say(JSON.stringify(reports.length === 1 ? reports[0] : reports, null, 2));
406
+ }
407
+ if (sub === 'resume') {
408
+ const result = M.resume({ cwd: root });
409
+ return void say(argv.includes('--json') ? JSON.stringify(result, null, 2) : (result.context || '(no local continuation for this branch/worktree)'));
410
+ }
411
+ if (sub === 'save') {
412
+ const result = M.capture({
413
+ cwd: root,
414
+ sessionId: typeof flag('session') === 'string' ? flag('session') : `manual-${Date.now()}`,
415
+ target: target || 'manual',
416
+ event: 'save',
417
+ force: true,
418
+ });
419
+ return void say(JSON.stringify(result, null, 2));
420
+ }
421
+ if (sub === 'learn') {
422
+ const value = (name) => { const v = flag(name); return typeof v === 'string' && !v.startsWith('--') ? v : undefined; };
423
+ const result = M.learn({
424
+ cwd: root,
425
+ text: value('text'),
426
+ evidence: value('evidence'),
427
+ scope: value('scope') || 'project',
428
+ confidence: value('confidence'),
429
+ approved: argv.includes('--approve'),
430
+ });
431
+ if (!result.saved) process.exitCode = 1;
432
+ return void say(JSON.stringify(result, null, 2));
433
+ }
434
+ if (sub === 'metrics') return void say(JSON.stringify(M.metricsSummary({ cwd: root }), null, 2));
435
+ say('Use: npx @ericrisco/rsc memory on|off|status|save [--session id]|resume [--json]|learn --text "…" --evidence "…" --scope project|global --confidence 0..1 --approve|metrics');
436
+ process.exitCode = 1;
437
+ return;
438
+ }
339
439
  case 'sync': {
340
440
  const dry = argv.includes('--dry-run');
341
441
  for (const t of targets) {
@@ -446,6 +546,13 @@ async function main() {
446
546
  } else {
447
547
  say(`# ${caps.target} has no file-based agents — the agent option does not apply here`);
448
548
  }
549
+ if (caps.commandsSupported) {
550
+ for (const command of caps.commands) say(`command\t${command.id}\tproject\t${command.path}`);
551
+ if (!caps.commands.length) say('# no managed command files found (this target supports them)');
552
+ } else {
553
+ say(`# ${caps.target} has no supported project command surface`);
554
+ }
555
+ say(`memory\t${caps.memory.mode}\t${caps.memory.status}\t${caps.memory.reason}`);
449
556
  }
450
557
  if (!full) say('# catalog shown as ids; add --full for descriptions, or use `catalog --available` to match by meaning');
451
558
  return;
@@ -623,8 +730,9 @@ async function main() {
623
730
  const dry = argv.includes('--dry-run');
624
731
  // `uninstall --all` is an alias for a full purge.
625
732
  if (argv.includes('--all')) return void (await runPurge(dry, argv.includes('--with-docs')));
626
- const ids = argv.slice(1).filter((a) => !a.startsWith('--'));
627
- const removed = await uninstall({ skillIds: ids, target, dryRun: dry });
733
+ const selected = classifyRequested(requestedIds());
734
+ if (reportUnknown(selected.unknown)) return;
735
+ const removed = await uninstall({ skillIds: selected.skills, agentIds: selected.agents, target, dryRun: dry });
628
736
  return void say((dry ? 'Would remove:\n' : 'Removed:\n') + (removed.join('\n') || '(nothing)'));
629
737
  }
630
738
  case 'repair': {
@@ -652,7 +760,7 @@ async function main() {
652
760
  return void (await runPurge(argv.includes('--dry-run'), argv.includes('--with-docs')));
653
761
  default:
654
762
  say(`rsc: unknown command '${cmd}'.`);
655
- say('Use: npx @ericrisco/rsc | add <id...> | install --profile <p> | consult "<text>" | list | capabilities [--full|gap-log] | audit | registry refresh | doctor | sync | sello <on|off|status|…> | backups | restore <id|latest> | upgrade | repair | uninstall <id> | purge');
763
+ say('Use: npx @ericrisco/rsc | add <id...> | install --profile <p> | consult "<text>" | list | capabilities [--full|gap-log] | audit | registry refresh | doctor | sync | memory <on|off|status|save|resume|learn|metrics> | sello <on|off|status|…> | backups | restore <id|latest> | upgrade | repair | uninstall <id> | purge');
656
764
  say('Any command takes --target <claude|codex|cursor|copilot|gemini|…> (comma-separate for several)');
657
765
  say(' → without it, rsc uses the assistant already installed here; if two are, it asks instead of guessing.');
658
766
  process.exitCode = 1;
@@ -11,9 +11,9 @@ origin: risco
11
11
 
12
12
  **Hand-off — the visual system vs the craft of motion.** This skill owns the visual system and page
13
13
  composition, and sets motion *intent + budget* only. The **implementation** of an animation is
14
- `../animate/SKILL.md`'s; reviewing motion is `../review-animations/SKILL.md`'s; gesture physics and
15
- translucent materials are `../apple-design/SKILL.md`'s. When a frontend request could belong to
16
- several of those, `../design-eng/SKILL.md` is the umbrella that routes it. The motion reference in
14
+ `../motion-craft/SKILL.md`'s; how a piece of interface should be BUILT — boundaries, state, the
15
+ loading and empty and error states — is `../ui-engineering/SKILL.md`'s. When a request could belong to
16
+ several of those, start from the one that owns the decision, not the one that owns the file. The motion reference in
17
17
  this skill defers its mechanics to them rather than restating them.
18
18
 
19
19
 
@@ -42,7 +42,7 @@ not settle whether the page says anything.
42
42
 
43
43
  ## Where to browse for component ideas
44
44
 
45
- When a surface needs an idea rather than a primitive, `../../pick-ui-library/references/component-galleries.md`
45
+ When a surface needs an idea rather than a primitive, a gallery of shipped products
46
46
  names where to go — Aceternity for marketing effects, 21st.dev for breadth, React Bits for tunable
47
47
  animation — and what each is good for. Take the technique and rebuild it in this project's tokens;
48
48
  check the brand study for what it refuses first, because a component that contradicts a stated ban
@@ -59,7 +59,7 @@ these are for when a piece is "the footer" or "the empty state".
59
59
  | [Footer.design](https://www.footer.design) | Footers only. Narrow on purpose, and the narrowness is why it is good. |
60
60
  | [Collect UI](https://collectui.com) | Daily-UI-style collections by component and pattern. |
61
61
  | [UI Garage](https://uigarage.net) | Patterns tagged by element and by industry. |
62
- | [UI Movement](https://uimovement.com) | Motion and interaction specifically — pair with the frequency gate in `../../animate/SKILL.md` before copying anything. |
62
+ | [UI Movement](https://uimovement.com) | Motion and interaction specifically — pair with the frequency gate in `../../motion-craft/SKILL.md` before copying anything. |
63
63
  | [Dark.design](https://dark.design) · [Dark Mode Design](https://darkmodedesign.com) | Dark interfaces done properly, which is a different problem from inverting a light one. |
64
64
  | [Scrnshts](https://scrnshts.club) | App Store screenshot design — a real discipline of its own. |
65
65
 
@@ -91,6 +91,6 @@ component", these outrank any gallery.
91
91
  - **[Bestfolios](https://www.bestfolios.com) returned 502 on 2026-08-24** and is excluded until it
92
92
  comes back. Do not cite it.
93
93
  - **Component ideas**, as opposed to whole pages, live in
94
- `../../pick-ui-library/references/component-galleries.md` — Aceternity, 21st.dev, React Bits.
94
+ Component galleries — Aceternity, 21st.dev, React Bits.
95
95
  - **Identity outranks all of this.** Check `02-DOCS/wiki/brand/` for what the project refuses before
96
96
  bringing an idea back. A gallery cannot know what your brand vetoed.
@@ -30,8 +30,8 @@ Need one that is not here? Take it from [easing.dev](https://easing.dev/), do no
30
30
 
31
31
  The choice, in decision order: entering or exiting → `ease-out`; moving on screen → `ease-in-out`;
32
32
  hover or color → `ease`; constant motion (marquee, progress) → `linear`; unsure → `ease-out`.
33
- `../../animate/SKILL.md` owns the full build sequence and the per-element duration table, and
34
- `../../review-animations/scripts/verify.sh` flags an `ease-in` that slips through.
33
+ `../../motion-craft/SKILL.md` owns the duration ranges, the easing families and the never-animate list, and
34
+ its diagnosis table maps the complaint — sluggish, cheap, janky — back to the cause.
35
35
 
36
36
  - **Enter vs exit asymmetry:** entrances are slower and softer (`--dur-normal`, `--ease-out`); exits are quicker and quieter (~150ms) so dismissals feel responsive.
37
37
  - **Press feedback:** `scale(0.97)` on `:active` gives a tactile button without distracting movement.
@@ -30,7 +30,7 @@ should_not_trigger:
30
30
  route_to: "nextjs"
31
31
  why: "The build. The identity stays in the record; the framework work is the sibling's."
32
32
  - prompt: "Give me three different directions for this poster and I'll choose."
33
- route_to: "prototype"
33
+ route_to: "variant-explorer"
34
34
  why: "Exploration among variants. One skill per style: merging two candidate identities into one spec produces the average of two good designs, which is a bad design."
35
35
 
36
36
  capability:
@@ -40,7 +40,7 @@ and proposing from memory is how this promise used to resolve to the AI-template
40
40
 
41
41
  **Where to go looking.** For a whole page to beat, and for the flows and single patterns behind it:
42
42
  `../design/references/inspiration-sources.md`. For a component idea rather than a whole reference:
43
- `../pick-ui-library/references/component-galleries.md`. Browse for the technique, never to clone a
43
+ a gallery of real products. Browse for the technique, never to clone a
44
44
  competitor's exact grid — and come back with **a specific page you can open**, because a gallery
45
45
  is not a bar.
46
46
 
@@ -152,8 +152,8 @@ how the look drifts back to generic by the fifth piece.
152
152
  this loop invokes; it stays reachable directly when there is no bar to beat and no loop to run.
153
153
  - **Codifying the winner into a permanent identity** → `../design-dna/SKILL.md`.
154
154
  - **The words** → `../marketing/SKILL.md`. **The build** → `../nextjs/SKILL.md`.
155
- - **Motion craft, and routing among the frontend craft siblings** → `../design-eng/SKILL.md`.
156
- - **"Show me a few versions and I'll pick"** → `../prototype/SKILL.md`. That is selection among
155
+ - **Motion craft** → `../motion-craft/SKILL.md`. **How the interface should be built** → `../ui-engineering/SKILL.md`.
156
+ - **"Show me a few versions and I'll pick"** → `../variant-explorer/SKILL.md`. That is selection among
157
157
  variants; this is convergence against a bar.
158
158
  - **SDD gate.** This skill runs a method; it does not outrank the chain. If the request is a new
159
159
  feature or a non-trivial behaviour change in a real app, `../specify/SKILL.md` runs first, then
@@ -24,11 +24,11 @@ should_not_trigger:
24
24
  route_to: "design"
25
25
  why: "Visual system work with no reference to beat and nothing to judge. `design` stays directly reachable exactly for this."
26
26
  - prompt: "Show me three versions of this button and I'll pick the one I like."
27
- route_to: "prototype"
27
+ route_to: "variant-explorer"
28
28
  why: "Selection among variants by human preference, not convergence against a measured bar. The user is the judge and there is no reference."
29
29
  - prompt: "Review the motion in this diff and tell me what's off."
30
- route_to: "review-animations"
31
- why: "One diff, motion-specific, no reference and no build-then-judge rounds. Reviewing is not looping."
30
+ route_to: "motion-craft"
31
+ why: "One diff, motion-specific, no reference and no build-then-judge rounds. The motion craft skill owns the complaint-to-cause mapping; reviewing is not looping."
32
32
  - prompt: "Write the hero headline and value prop for our developer-tool site."
33
33
  route_to: "marketing"
34
34
  why: "The words, not the bar. `voice` constrains copy in this pipe; it does not write it."
@@ -11,7 +11,7 @@ origin: risco
11
11
  **Hand-off — the app vs its motion.** This skill owns the Expo platform: router, config plugins, EAS,
12
12
  native modules, store builds. **Animation and gestures** — Reanimated worklets, the UI runtime,
13
13
  sheets, screen transitions, press feedback, haptics, motion that stutters on a real device — are
14
- `../animate-expo/SKILL.md`'s.
14
+ `../motion-craft/SKILL.md`'s.
15
15
 
16
16
 
17
17
  ## What this skill owns
@@ -0,0 +1,125 @@
1
+ ---
2
+ name: motion-craft
3
+ description: "Use when an interface moves and the movement has to feel right — durations and easing, what must never animate, fixing motion that reads sluggish, cheap or janky, and reduced-motion. Web and native. NOT the visual system (that is `design`), NOT the graded critique (that is `design-loop`)."
4
+ tags: [motion, animation, interface, craft, frontend]
5
+ recommends: [design]
6
+ profiles: [full]
7
+ origin: risco
8
+ ---
9
+
10
+ # Motion craft — movement that reads as intent
11
+
12
+ Motion is not decoration laid over an interface. It is the interface **explaining what just
13
+ happened**: what appeared, what it came from, what is still loading, what the user just did. When
14
+ motion feels wrong the complaint is almost never "the curve is off" — it is "this feels cheap", or
15
+ "slow", or "I don't know what happened". Those are the symptoms this skill exists to convert into
16
+ decisions.
17
+
18
+ ## The one rule
19
+
20
+ **Every animation answers a question the user is already asking.** If you cannot name the question,
21
+ delete the animation — it is costing time and attention and returning nothing.
22
+
23
+ | The user is asking | The motion that answers it |
24
+ | --- | --- |
25
+ | "Where did this come from?" | It enters from its origin, not from nowhere |
26
+ | "Is it working?" | Immediate feedback, under 100ms, even before the result |
27
+ | "What did I just do?" | The changed thing moves; nothing else does |
28
+ | "Where did it go?" | It exits toward where it can be found again |
29
+ | nothing | **no animation** |
30
+
31
+ That table is the whole method. Everything below is how to honour it without shipping something
32
+ that janks.
33
+
34
+ ## Duration: the only numbers worth memorising
35
+
36
+ Movement is read as *slow* long before it is read as *long*. The usable range is narrower than most
37
+ people expect, and the mistake is almost always **too slow**, not too fast.
38
+
39
+ | What moves | Range | Why |
40
+ | --- | --- | --- |
41
+ | State on an element already on screen (hover, toggle, press) | 100–150ms | It must feel like a response, not an event |
42
+ | Something entering or leaving in place (menu, tooltip, toast) | 150–250ms | Long enough to be seen, short enough not to be waited for |
43
+ | A region rearranging (list reflow, panel) | 250–350ms | Bigger travel needs more time to stay legible |
44
+ | A full view transition | 300–400ms | The ceiling. Past 400ms it is a wait, not a transition |
45
+
46
+ Two rules that come from those numbers, not from taste:
47
+
48
+ - **Exits are faster than entrances**, usually by about a third. The user has already decided; making
49
+ them watch the decision leave is the single most common way an interface feels sluggish.
50
+ - **Distance and duration scale together, sub-linearly.** Doubling the travel does not double the
51
+ time. If a value grows past 400ms because the element crosses the screen, the layout is the
52
+ problem, not the timing.
53
+
54
+ ## Easing: three curves cover almost everything
55
+
56
+ - **Entering, or responding to input** — start fast, settle slow (`ease-out` family). The user's
57
+ attention is already there; arriving quickly and settling reads as responsive.
58
+ - **Leaving** — start slow, accelerate away (`ease-in` family). It matches how the eye lets go.
59
+ - **Moving between two on-screen positions** — accelerate and decelerate (`ease-in-out` family).
60
+
61
+ **Linear is for one thing only: continuous, non-positional change** — a progress bar filling, a
62
+ spinner rotating, a colour crossfade. Linear on anything that moves in space reads as mechanical,
63
+ because nothing physical starts at full speed.
64
+
65
+ Springs are worth reaching for when the motion should feel *physical* — dragging, dismissing,
66
+ anything the user's finger or pointer is still in contact with. They are the wrong tool for
67
+ appearing and disappearing, where a bounce reads as indecision.
68
+
69
+ ## What must never animate
70
+
71
+ This is where most jank comes from, and it is not a matter of degree.
72
+
73
+ - **Never animate layout.** Width, height, top, left, margin, padding — each frame forces the engine
74
+ to recompute geometry. Animate transform and opacity, which do not.
75
+ - **Never `transition: all`.** It is the shortest path to animating layout by accident. List the
76
+ properties.
77
+ - **Never animate what the user is reading.** Text that moves while being read is illegible.
78
+ - **Never block input on an animation.** The interface accepts the next action immediately, even
79
+ mid-motion. An animation that must finish before the user may act has become a modal dialog.
80
+ - **Never animate more than one thing per event.** If three things move when one changed, the user
81
+ cannot tell which one mattered.
82
+
83
+ ## Reduced motion is not an edge case
84
+
85
+ A real share of people have vestibular conditions for which large movement is nauseating, and they
86
+ have told their operating system so. Honouring it is not optional and it is not a fallback to
87
+ nothing:
88
+
89
+ - **Keep** the feedback: opacity changes, colour, instant state.
90
+ - **Remove** the travel: no sliding across the screen, no scaling, no parallax, no auto-playing loops.
91
+ - The interface must still say what happened — it just says it without moving.
92
+
93
+ An implementation with no reduced-motion branch is unfinished, the same way an interface with no
94
+ error state is unfinished.
95
+
96
+ ## Diagnosing motion that feels wrong
97
+
98
+ The complaint names the symptom; this maps it to a cause.
99
+
100
+ | Complaint | Usually | Fix |
101
+ | --- | --- | --- |
102
+ | "Sluggish" | Duration too long, or the exit matches the entrance | Cut to the range above; make exits faster |
103
+ | "Cheap" / "amateur" | Linear easing on positional movement, or bounce on entrances | Match curve to direction |
104
+ | "Janky" / "stutters" | Animating layout properties | Move to transform and opacity |
105
+ | "Distracting" | More than one thing moving per event | Animate only what changed |
106
+ | "I don't know what happened" | Motion with no origin or destination | Move it from where it came from |
107
+ | "Nice, but I'd turn it off" | No reduced-motion branch, or motion that repeats | Honour the setting; never loop what is not loading |
108
+
109
+ ## Anti-patterns → STOP
110
+
111
+ | If you're about to… | Reality / Fix |
112
+ | --- | --- |
113
+ | Add motion because the screen feels static | Static is not a defect. Motion with no question to answer is noise with a frame cost. |
114
+ | Copy a duration from a design you liked | You copied the number without its distance and its context. Take the range, verify against the feel. |
115
+ | Animate a list so every item cascades in | Stagger past ~4 items becomes a queue the user waits through. Cap it, or drop it. |
116
+ | Use a spring for a menu appearing | A bounce on an entrance reads as indecision. Springs are for what the pointer is still touching. |
117
+ | Ship motion without checking reduced-motion | It is not an edge case. Unchecked means unfinished. |
118
+ | Reach here for the visual system | Type, colour, spacing and composition are `../design/SKILL.md`. This skill only moves what that one placed. |
119
+
120
+ ## See Also
121
+
122
+ - `../design/SKILL.md` — the visual system this motion serves.
123
+ - `../design-loop/SKILL.md` — the graded critique that judges a finished screen, motion included.
124
+ - `../design-dna/SKILL.md` — turning a look, motion rules included, into a reusable identity.
125
+ - `../accessibility/SKILL.md` — the wider ground reduced-motion sits in.
@@ -0,0 +1,38 @@
1
+ skill: motion-craft
2
+
3
+ should_trigger:
4
+ - prompt: "The menu animation feels sluggish, can you make it feel snappier?"
5
+ why: "The canonical case: a complaint about feel, which this skill converts into a duration and an easing decision."
6
+ - prompt: "What duration and easing should this dropdown use?"
7
+ why: "Directly the numbers this skill owns, for an element entering in place."
8
+ - prompt: "My list animation stutters when it reflows."
9
+ why: "Jank is the symptom of animating layout properties, which is the one hard rule here."
10
+ - prompt: "Añade una animación al abrir el panel, pero que no quede cutre."
11
+ why: "Same ask in Spanish; the trigger is the intent, not the language."
12
+ - prompt: "Does this respect prefers-reduced-motion? I don't think we handled it."
13
+ why: "Reduced-motion is treated here as part of finishing the work, not as an edge case."
14
+ - prompt: "Everything on the page moves at once when I click one button."
15
+ why: "More than one thing moving per event is a named diagnosis in this skill."
16
+
17
+ should_not_trigger:
18
+ - prompt: "Design a landing page for my analytics SaaS — hero, pricing, the works."
19
+ route_to: "design"
20
+ why: "Type, colour, spacing and composition are the visual system, which this skill only moves once it exists."
21
+ - prompt: "Review this checkout screen and tell me how good it actually is, with a grade."
22
+ route_to: "design-loop"
23
+ why: "Graded critique of a finished screen is the loop's job; this skill builds motion, it does not judge screens."
24
+ - prompt: "This design came out great — turn it into a style I can reuse everywhere."
25
+ route_to: "design-dna"
26
+ why: "Codifying a look into a permanent identity is capture, not motion craft."
27
+ - prompt: "Make the site keyboard navigable and fix the contrast failures."
28
+ route_to: "accessibility"
29
+ why: "Accessibility beyond reduced-motion is its own ground; only the motion half lives here."
30
+ - prompt: "Our React app re-renders too much and the profiler shows wasted renders."
31
+ route_to: "react"
32
+ why: "A rendering performance problem is framework work, even when the symptom looks like stutter."
33
+
34
+ capability:
35
+ - prompt: "This modal slides in over 600ms with a linear curve and it feels cheap. Fix it and say why."
36
+ why: "Exercises the two most common defects at once — too slow, and linear on positional movement — and demands the reasoning, not just new numbers."
37
+ - prompt: "We animate width and height on the sidebar and it janks on mobile. What do we do instead?"
38
+ why: "The layout-vs-compositor rule is the load-bearing piece of technical content here."