@ericrisco/rsc 1.1.4 → 1.1.6
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 +81 -11
- package/manifest.json +56 -231
- package/package.json +1 -1
- package/scripts/doctor.js +68 -1
- package/scripts/install-apply.js +91 -16
- package/scripts/lib/divergence.js +33 -0
- package/scripts/lib/domains.js +3 -3
- package/scripts/lib/manifest-file.js +53 -0
- package/scripts/lib/repair.js +156 -0
- package/scripts/lib/ui.js +5 -0
- package/scripts/rsc.js +84 -11
- 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/suggest/SKILL.md +25 -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/_md-block.js +17 -4
- package/targets/claude.js +17 -7
- package/targets/index.js +66 -19
- 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,38 @@
|
|
|
1
|
+
skill: variant-explorer
|
|
2
|
+
|
|
3
|
+
should_trigger:
|
|
4
|
+
- prompt: "Show me a few versions of this onboarding and I'll pick one."
|
|
5
|
+
why: "The canonical phrasing: options to choose from, not an answer."
|
|
6
|
+
- prompt: "Give me three directions for how this could work before we commit."
|
|
7
|
+
why: "Explicit divergence ahead of an expensive decision."
|
|
8
|
+
- prompt: "Enséñame varias opciones de cómo montar el checkout, que ya elijo yo."
|
|
9
|
+
why: "Same request in Spanish; the trigger is the ask for options."
|
|
10
|
+
- prompt: "I can't decide between a wizard and a single page. Can I see both?"
|
|
11
|
+
why: "Two named approaches where seeing them is what settles it."
|
|
12
|
+
- prompt: "What are some different ways we could structure this dashboard?"
|
|
13
|
+
why: "Open-ended exploration of structural decisions, which is exactly divergence."
|
|
14
|
+
- prompt: "Before we build it, I want to compare a couple of approaches side by side."
|
|
15
|
+
why: "Comparability is the property this skill guarantees."
|
|
16
|
+
|
|
17
|
+
should_not_trigger:
|
|
18
|
+
- prompt: "Review this checkout screen and grade it."
|
|
19
|
+
route_to: "design-loop"
|
|
20
|
+
why: "Judging one finished thing is critique, the opposite of divergence."
|
|
21
|
+
- prompt: "We need to agree what this feature is for before anyone builds it."
|
|
22
|
+
route_to: "specify"
|
|
23
|
+
why: "A missing WHAT/WHY is a spec problem; options would be premature."
|
|
24
|
+
- prompt: "Build the pricing page we agreed on last week."
|
|
25
|
+
route_to: "design"
|
|
26
|
+
why: "The decision is already made; manufacturing options would waste the time this skill is meant to save."
|
|
27
|
+
- prompt: "Which colour should the primary button be?"
|
|
28
|
+
route_to: "design"
|
|
29
|
+
why: "Cheap and reversible, and a detail rather than a decision — the explicit non-case here."
|
|
30
|
+
- prompt: "Run an A/B test to see which headline converts better."
|
|
31
|
+
route_to: "ab-testing"
|
|
32
|
+
why: "Measuring a winner with users is experimentation, not showing a human candidates to choose from."
|
|
33
|
+
|
|
34
|
+
capability:
|
|
35
|
+
- prompt: "Give me three options for this signup flow. Make sure they are actually different, not the same thing restyled."
|
|
36
|
+
why: "Goes straight at the failure mode this skill exists to prevent, and demands the decision behind each be nameable."
|
|
37
|
+
- prompt: "Here are three variants I made. Are these real alternatives or the same idea three times?"
|
|
38
|
+
why: "Applies the one rule as a test on someone else's set, which is where it is hardest and most useful."
|
package/targets/_md-block.js
CHANGED
|
@@ -9,6 +9,9 @@ import { linkOrCopy } from './index.js';
|
|
|
9
9
|
// multiple assistants sharing one file never duplicate it.
|
|
10
10
|
const MARK_START = '<!-- rsc-suggest:start -->';
|
|
11
11
|
const MARK_END = '<!-- rsc-suggest:end -->';
|
|
12
|
+
// The exact separator wireHook writes before an appended block, so unwireHook can take
|
|
13
|
+
// back precisely what it gave and leave the rest of the file untouched.
|
|
14
|
+
const SEAM = '\n\n';
|
|
12
15
|
|
|
13
16
|
export function writeSkill(id, fromDir, toPath) {
|
|
14
17
|
return linkOrCopy(fromDir, toPath);
|
|
@@ -21,7 +24,9 @@ export function wireHook(paths, sourceMd) {
|
|
|
21
24
|
if (doc.includes(MARK_START)) {
|
|
22
25
|
doc = doc.replace(new RegExp(`${MARK_START}[\\s\\S]*?${MARK_END}`), block);
|
|
23
26
|
} else {
|
|
24
|
-
|
|
27
|
+
// Append with a fixed seam, and record nothing else. unwireHook removes exactly this
|
|
28
|
+
// seam and this block, which is what lets the file come back byte-identical.
|
|
29
|
+
doc += `${SEAM}${block}\n`;
|
|
25
30
|
}
|
|
26
31
|
mkdirSync(dirname(paths.hookTarget), { recursive: true });
|
|
27
32
|
writeFileSync(paths.hookTarget, doc);
|
|
@@ -34,9 +39,17 @@ export function unwireHook(paths) {
|
|
|
34
39
|
if (!existsSync(paths.hookTarget)) return [];
|
|
35
40
|
const doc = readFileSync(paths.hookTarget, 'utf8');
|
|
36
41
|
if (!doc.includes(MARK_START)) return [];
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
// Remove the block and the seam that introduced it — nothing more.
|
|
43
|
+
//
|
|
44
|
+
// This used to end with a document-wide `\n{3,}` -> `\n\n` pass, which tidied blank
|
|
45
|
+
// lines the author had put hundreds of lines away from anything of ours. In #249 that
|
|
46
|
+
// file was the project's hand-written constitution: it had already received 95 lines it
|
|
47
|
+
// never asked for, and giving it back reformatted is the second half of the same damage.
|
|
48
|
+
// A slightly larger gap is cosmetic; rewriting someone's document is not.
|
|
49
|
+
const cleaned = doc.replace(
|
|
50
|
+
new RegExp(`${SEAM.replace(/\n/g, '\\n')}?${MARK_START}[\\s\\S]*?${MARK_END}\\n?`),
|
|
51
|
+
'',
|
|
52
|
+
);
|
|
40
53
|
writeFileSync(paths.hookTarget, cleaned);
|
|
41
54
|
return [paths.hookTarget];
|
|
42
55
|
}
|
package/targets/claude.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, rmSync } from 'node:fs';
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
2
|
+
import { dirname, join, relative, sep } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { linkOrCopy } from './index.js';
|
|
5
5
|
|
|
@@ -70,6 +70,15 @@ export function unwireHook(paths) {
|
|
|
70
70
|
// entry (the old `cat …/suggest/SKILL.md` form, or a previous `.sh`/`.mjs` script
|
|
71
71
|
// form) is dropped before we add the current one — idempotent, migrating legacy and
|
|
72
72
|
// bash-era hooks in place. Other (non-rsc) SessionStart hooks are preserved.
|
|
73
|
+
// Commands name the project through ${CLAUDE_PROJECT_DIR}, never through the absolute
|
|
74
|
+
// path of the folder it happened to be installed in. Baking that path in is what made a
|
|
75
|
+
// harness unshareable: a committed settings.json was broken in every other checkout, and
|
|
76
|
+
// even renaming your own folder broke it. The variable is resolved by the client at run
|
|
77
|
+
// time, so the same bytes work on every machine — which is the precondition for the
|
|
78
|
+
// manifest being worth committing at all.
|
|
79
|
+
const P = '${CLAUDE_PROJECT_DIR}';
|
|
80
|
+
const at = (...seg) => `${P}/${seg.join('/')}`;
|
|
81
|
+
|
|
73
82
|
export function wireHook(paths) {
|
|
74
83
|
const scriptDest = join(paths.projectRoot, '.rsc', 'session-start.mjs');
|
|
75
84
|
mkdirSync(dirname(scriptDest), { recursive: true });
|
|
@@ -79,7 +88,8 @@ export function wireHook(paths) {
|
|
|
79
88
|
copyFileSync(join(HERE, 'session-start.mjs'), scriptDest);
|
|
80
89
|
|
|
81
90
|
const suggestMd = `${paths.skillDir('suggest')}/SKILL.md`;
|
|
82
|
-
const
|
|
91
|
+
const suggestRel = at(relative(paths.projectRoot, paths.skillDir('suggest')).split(sep).join('/'), 'SKILL.md');
|
|
92
|
+
const cmd = `node "${at('.rsc', 'session-start.mjs')}" "${suggestRel}" "${P}"`;
|
|
83
93
|
|
|
84
94
|
const file = paths.hookTarget;
|
|
85
95
|
const settings = existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : {};
|
|
@@ -99,7 +109,7 @@ export function wireHook(paths) {
|
|
|
99
109
|
// any prior rsc worklog-checkpoint entry (.sh or .mjs) dropped first.
|
|
100
110
|
const wlDest = join(paths.projectRoot, '.rsc', 'worklog-checkpoint.mjs');
|
|
101
111
|
copyFileSync(join(HERE, 'worklog-checkpoint.mjs'), wlDest);
|
|
102
|
-
const wlCmd = `node "${
|
|
112
|
+
const wlCmd = `node "${at('.rsc', 'worklog-checkpoint.mjs')}" "${P}"`;
|
|
103
113
|
for (const event of ['PreCompact', 'SessionEnd']) {
|
|
104
114
|
settings.hooks[event] ||= [];
|
|
105
115
|
settings.hooks[event] = settings.hooks[event].filter(
|
|
@@ -118,7 +128,7 @@ export function wireHook(paths) {
|
|
|
118
128
|
// sello.mjs is ship-guard's sibling import (hooks are materialized file-by-file,
|
|
119
129
|
// so the deterministic sello core must land next to the guard that loads it).
|
|
120
130
|
copyFileSync(join(HERE, 'sello.mjs'), join(paths.projectRoot, '.rsc', 'sello.mjs'));
|
|
121
|
-
const sgCmd = `node "${
|
|
131
|
+
const sgCmd = `node "${at('.rsc', 'ship-guard.mjs')}" "${P}"`;
|
|
122
132
|
settings.hooks.PreToolUse ||= [];
|
|
123
133
|
settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
|
|
124
134
|
(e) => !hookWiringOf(e).includes('.rsc/ship-guard.'),
|
|
@@ -132,7 +142,7 @@ export function wireHook(paths) {
|
|
|
132
142
|
// node-run (Windows-safe), idempotent, fail-open, opt-out via .rsc/.no-danger-guard.
|
|
133
143
|
const dgDest = join(paths.projectRoot, '.rsc', 'danger-guard.mjs');
|
|
134
144
|
copyFileSync(join(HERE, 'danger-guard.mjs'), dgDest);
|
|
135
|
-
const dgCmd = `node "${
|
|
145
|
+
const dgCmd = `node "${at('.rsc', 'danger-guard.mjs')}" "${P}"`;
|
|
136
146
|
settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
|
|
137
147
|
(e) => !hookWiringOf(e).includes('.rsc/danger-guard.'),
|
|
138
148
|
);
|
|
@@ -147,7 +157,7 @@ export function wireHook(paths) {
|
|
|
147
157
|
// .rsc/.no-gitmoji.
|
|
148
158
|
const gmDest = join(paths.projectRoot, '.rsc', 'gitmoji-guard.mjs');
|
|
149
159
|
copyFileSync(join(HERE, 'gitmoji-guard.mjs'), gmDest);
|
|
150
|
-
const gmCmd = `node "${
|
|
160
|
+
const gmCmd = `node "${at('.rsc', 'gitmoji-guard.mjs')}" "${P}"`;
|
|
151
161
|
settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
|
|
152
162
|
(e) => !hookWiringOf(e).includes('.rsc/gitmoji-guard.'),
|
|
153
163
|
);
|
|
@@ -163,7 +173,7 @@ export function wireHook(paths) {
|
|
|
163
173
|
// (non-rsc) UserPromptSubmit hooks are preserved.
|
|
164
174
|
const fgDest = join(paths.projectRoot, '.rsc', 'userprompt-gate.mjs');
|
|
165
175
|
copyFileSync(join(HERE, 'userprompt-gate.mjs'), fgDest);
|
|
166
|
-
const fgCmd = `node "${
|
|
176
|
+
const fgCmd = `node "${at('.rsc', 'userprompt-gate.mjs')}" "${P}"`;
|
|
167
177
|
settings.hooks.UserPromptSubmit ||= [];
|
|
168
178
|
settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter(
|
|
169
179
|
(e) => !hookWiringOf(e).includes('.rsc/userprompt-gate.'),
|
package/targets/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { existsSync, lstatSync, mkdirSync, rmSync, symlinkSync, cpSync } from 'node:fs';
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, rmSync, symlinkSync, cpSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join, dirname, relative } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import * as claudeAdapter from './claude.js';
|
|
5
5
|
import * as cursorAdapter from './cursor.js';
|
|
6
6
|
import * as mdAdapter from './_md-block.js';
|
|
7
|
+
import { readManifest } from '../scripts/lib/manifest-file.js';
|
|
7
8
|
|
|
8
9
|
// Project-local single source of truth. Real skill files live here exactly once;
|
|
9
10
|
// every assistant gets a lightweight pointer (symlink) back to it — no duplication.
|
|
@@ -83,28 +84,74 @@ export const TARGETS = [
|
|
|
83
84
|
{ id: 'aider', label: 'Aider', hint: 'CONVENTIONS.md' },
|
|
84
85
|
];
|
|
85
86
|
|
|
86
|
-
//
|
|
87
|
-
//
|
|
87
|
+
// Which assistants are actually installed here, read from the state file each one
|
|
88
|
+
// writes. This is evidence: only an install of ours writes it. Everything below
|
|
89
|
+
// (detectTarget) is inference from files a human — or our own `harness` skill —
|
|
90
|
+
// may have written for unrelated reasons. Order follows SPEC so the ambiguity
|
|
91
|
+
// message is deterministic. A missing, empty or unreadable state means "not
|
|
92
|
+
// installed", never an error: a half-written file must not break resolution.
|
|
93
|
+
export function installedTargets(cwd = process.cwd()) {
|
|
94
|
+
return TARGET_IDS.filter((id) => {
|
|
95
|
+
try {
|
|
96
|
+
const state = JSON.parse(readFileSync(targetPaths(id, undefined, cwd).stateFile, 'utf8'));
|
|
97
|
+
return Object.keys(state.skills || {}).length > 0;
|
|
98
|
+
} catch { return false; }
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Inference, used only when there is no evidence. Unique config dirs win.
|
|
103
|
+
//
|
|
104
|
+
// Two rules here are scar tissue from issue #249. Claude Code had NO signal at
|
|
105
|
+
// all — it was merely the final fallthrough — so any repo with a root AGENTS.md
|
|
106
|
+
// resolved to codex even with .claude/ full of skills. And AGENTS.md is the
|
|
107
|
+
// weakest signal on purpose: our own `harness` skill writes one into every repo
|
|
108
|
+
// it equips, so treating it as a strong hint made the harness poison its own
|
|
109
|
+
// detection the moment it ran once.
|
|
88
110
|
export function detectTarget(cwd = process.cwd()) {
|
|
89
|
-
const
|
|
90
|
-
if (
|
|
91
|
-
if (
|
|
92
|
-
if (
|
|
93
|
-
if (
|
|
94
|
-
if (
|
|
95
|
-
if (
|
|
96
|
-
if (
|
|
97
|
-
if (
|
|
98
|
-
if (
|
|
99
|
-
if (
|
|
100
|
-
if (
|
|
101
|
-
if (
|
|
102
|
-
if (
|
|
103
|
-
if (
|
|
104
|
-
if (
|
|
111
|
+
const has = (p) => existsSync(join(cwd, p));
|
|
112
|
+
if (has('.cursor')) return 'cursor';
|
|
113
|
+
if (has('.windsurf')) return 'windsurf';
|
|
114
|
+
if (has('.clinerules')) return 'cline';
|
|
115
|
+
if (has('.roo')) return 'roo';
|
|
116
|
+
if (has('.continue')) return 'continue';
|
|
117
|
+
if (has('.junie')) return 'junie';
|
|
118
|
+
if (has('.kiro')) return 'kiro';
|
|
119
|
+
if (has('.zed')) return 'zed';
|
|
120
|
+
if (has('.opencode')) return 'opencode';
|
|
121
|
+
if (has('.amp')) return 'amp';
|
|
122
|
+
if (has('.jules')) return 'jules';
|
|
123
|
+
if (has('.antigravity')) return 'antigravity';
|
|
124
|
+
if (has(join('.github', 'copilot-instructions.md'))) return 'copilot';
|
|
125
|
+
if (has('.claude') || has('CLAUDE.md')) return 'claude';
|
|
126
|
+
if (has('.codex')) return 'codex';
|
|
127
|
+
if (has('.gemini') || has('GEMINI.md')) return 'gemini';
|
|
128
|
+
if (has('AGENTS.md')) return 'codex'; // weakest: our own harness writes this
|
|
105
129
|
return 'claude';
|
|
106
130
|
}
|
|
107
131
|
|
|
132
|
+
// The single point of truth for "which assistant are we acting on". Every command
|
|
133
|
+
// goes through here — including doctor, which used to resolve on its own and so
|
|
134
|
+
// could report a different target than the one just installed into.
|
|
135
|
+
//
|
|
136
|
+
// Ambiguity is a distinct outcome, not a value the caller might mistake for an id:
|
|
137
|
+
// returning a sentinel string would let a forgetful caller pass it to targetPaths()
|
|
138
|
+
// and die with `unknown target`. `ids` empty + `ambiguous` set cannot be used by
|
|
139
|
+
// accident.
|
|
140
|
+
export function resolveTargets({ cwd = process.cwd(), flagValue } = {}) {
|
|
141
|
+
if (typeof flagValue === 'string' && flagValue.trim()) {
|
|
142
|
+
const ids = flagValue.split(',').map((s) => s.trim()).filter(Boolean);
|
|
143
|
+
return { ids, ambiguous: null, source: 'flag' };
|
|
144
|
+
}
|
|
145
|
+
// The committed manifest is what the team decided, so it outranks what happens to be
|
|
146
|
+
// on this machine. Two targets here is not ambiguity — it is the team saying "both".
|
|
147
|
+
const declared = readManifest(cwd)?.targets || [];
|
|
148
|
+
if (declared.length) return { ids: declared, ambiguous: null, source: 'manifest' };
|
|
149
|
+
const found = installedTargets(cwd);
|
|
150
|
+
if (found.length === 1) return { ids: found, ambiguous: null, source: 'evidence' };
|
|
151
|
+
if (found.length > 1) return { ids: [], ambiguous: found, source: 'evidence' };
|
|
152
|
+
return { ids: [detectTarget(cwd)], ambiguous: null, source: 'heuristic' };
|
|
153
|
+
}
|
|
154
|
+
|
|
108
155
|
export function targetPaths(target, home = homedir(), cwd = process.cwd()) {
|
|
109
156
|
const s = SPEC[target];
|
|
110
157
|
if (!s) throw new Error(`unknown target ${target}`);
|