@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.
- package/README.md +72 -14
- package/manifest.json +888 -231
- package/package.json +3 -2
- package/schema/ecc-originality.json +16 -0
- package/schema/ecc-originality.u64 +0 -0
- package/scripts/build-manifest.js +43 -3
- package/scripts/doctor.js +47 -1
- package/scripts/install-apply.js +148 -31
- package/scripts/lib/capabilities.js +21 -0
- package/scripts/lib/domains.js +3 -3
- package/scripts/lib/manifest-file.js +4 -1
- package/scripts/originality-check.js +137 -0
- package/scripts/rsc.js +122 -14
- package/skills/design/SKILL.md +3 -3
- package/skills/design/references/design-systems.md +1 -1
- package/skills/design/references/inspiration-sources.md +2 -2
- package/skills/design/references/motion-and-interaction.md +2 -2
- package/skills/design-dna/evals/cases.yaml +1 -1
- package/skills/design-loop/SKILL.md +3 -3
- package/skills/design-loop/evals/cases.yaml +3 -3
- package/skills/expo/SKILL.md +1 -1
- package/skills/motion-craft/SKILL.md +125 -0
- package/skills/motion-craft/evals/cases.yaml +38 -0
- package/skills/motion-craft/scripts/verify.sh +75 -0
- package/skills/react/SKILL.md +2 -2
- package/skills/react-native/SKILL.md +1 -1
- package/skills/swift-ios/SKILL.md +2 -2
- package/skills/ui-engineering/SKILL.md +114 -0
- package/skills/ui-engineering/evals/cases.yaml +38 -0
- package/skills/variant-explorer/SKILL.md +93 -0
- package/skills/variant-explorer/evals/cases.yaml +38 -0
- package/targets/agent-catalog.js +220 -0
- package/targets/agent-sources.json +48 -0
- package/targets/agents.js +61 -13
- package/targets/commands.js +212 -0
- package/targets/memory.js +276 -0
- package/targets/opencode-memory-plugin.js +65 -0
- package/targets/session-memory-adapter.mjs +117 -0
- package/targets/session-memory-core.mjs +477 -0
- package/targets/session-memory.mjs +27 -0
- package/skills/animate/SKILL.md +0 -851
- package/skills/animate/evals/cases.yaml +0 -36
- package/skills/animate/references/recipes.md +0 -324
- package/skills/animate-expo/SKILL.md +0 -281
- package/skills/animate-expo/evals/cases.yaml +0 -36
- package/skills/animate-expo/references/recipes.md +0 -385
- package/skills/animation-vocabulary/SKILL.md +0 -189
- package/skills/animation-vocabulary/evals/cases.yaml +0 -31
- package/skills/apple-design/SKILL.md +0 -301
- package/skills/apple-design/evals/cases.yaml +0 -33
- package/skills/ask-sonner/SKILL.md +0 -104
- package/skills/ask-sonner/evals/cases.yaml +0 -33
- package/skills/ask-sonner/references/api.md +0 -64
- package/skills/design-eng/SKILL.md +0 -88
- package/skills/design-eng/evals/cases.yaml +0 -36
- package/skills/find-animation-opportunities/SKILL.md +0 -150
- package/skills/find-animation-opportunities/evals/cases.yaml +0 -31
- package/skills/improve-animations/SKILL.md +0 -126
- package/skills/improve-animations/evals/cases.yaml +0 -31
- package/skills/improve-animations/references/audit.md +0 -115
- package/skills/improve-animations/references/plan-template.md +0 -73
- package/skills/pick-ui-library/SKILL.md +0 -101
- package/skills/pick-ui-library/evals/cases.yaml +0 -33
- package/skills/pick-ui-library/references/component-galleries.md +0 -59
- package/skills/prototype/SKILL.md +0 -113
- package/skills/prototype/evals/cases.yaml +0 -31
- package/skills/prototype/references/picker.md +0 -197
- package/skills/review-animations/SKILL.md +0 -188
- package/skills/review-animations/evals/cases.yaml +0 -34
- package/skills/review-animations/references/standards.md +0 -187
- package/skills/review-animations/scripts/verify.sh +0 -164
- package/skills/write-swift/SKILL.md +0 -413
- 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
|
-
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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
|
|
627
|
-
|
|
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;
|
package/skills/design/SKILL.md
CHANGED
|
@@ -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
|
-
`../
|
|
15
|
-
|
|
16
|
-
several of those,
|
|
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,
|
|
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 `../../
|
|
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
|
-
|
|
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
|
-
`../../
|
|
34
|
-
|
|
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: "
|
|
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
|
-
|
|
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
|
|
156
|
-
- **"Show me a few versions and I'll pick"** → `../
|
|
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: "
|
|
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: "
|
|
31
|
-
why: "One diff, motion-specific, no reference and no build-then-judge rounds.
|
|
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."
|
package/skills/expo/SKILL.md
CHANGED
|
@@ -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
|
-
`../
|
|
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."
|