@ericrisco/rsc 1.1.0 → 1.1.2

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 CHANGED
@@ -142,7 +142,7 @@ $ rsc
142
142
  264 skills · one CLI · zero bloat
143
143
 
144
144
  What do you want to do? ↑↓ move · enter select
145
- ❯ Base install — the essentials (orient + suggest + bro + harness + init)
145
+ ❯ Base install — the essentials (8 skills)
146
146
  Base + Spec-Driven Development — specify → plan → implement → ship
147
147
  Pick skills by hand, by area
148
148
  ```
@@ -185,7 +185,7 @@ rsc # plain-language wizard (recommended) — p
185
185
  rsc add fastapi postgresdb # install specific skills, by name
186
186
  rsc add youtube-api remotion-video # …grow a channel, edit with Remotion
187
187
  rsc add fastapi --target claude,codex # install into several assistants at once
188
- rsc install --profile minimal # the floor: orient + suggest + bro + harness + init
188
+ rsc install --profile minimal # the base: orient + suggest + bro + unslop + show-me + eli5 + harness + init
189
189
  rsc install --profile core # floor + the full SDD workflow
190
190
  rsc install --profile full # everything (all 264)
191
191
  rsc install --profile full --without go
@@ -259,7 +259,7 @@ just asks in plain language.
259
259
  ### 🧭 Core & control plane
260
260
  The front door and the workspace brain.
261
261
 
262
- [init](skills/init/) · [harness](skills/harness/) · [orient](skills/orient/) · [suggest](skills/suggest/) · [bro](skills/bro/) · [author-skill](skills/author-skill/) · [sdd-init](skills/sdd-init/)
262
+ [init](skills/init/) · [harness](skills/harness/) · [orient](skills/orient/) · [suggest](skills/suggest/) · [bro](skills/bro/) · [unslop](skills/unslop/) · [author-skill](skills/author-skill/) · [sdd-init](skills/sdd-init/)
263
263
 
264
264
  > **harness** is the Karpathy *chaos→knowledge* engine — a `01-TOOLS/` layer (one
265
265
  > folder per provider, each with a working `test_connection`) and a `02-DOCS/`
@@ -382,7 +382,7 @@ Three engines + engine-agnostic disciplines. Every engine skill pins the current
382
382
 
383
383
  ### 🧠 Knowledge & meta
384
384
 
385
- [knowledge-ops](skills/knowledge-ops/) · [codebase-onboarding](skills/codebase-onboarding/) · [research-ops](skills/research-ops/) · [decision-records](skills/decision-records/) · [continuous-learning](skills/continuous-learning/) · [skill-scout](skills/skill-scout/) · [context-budget](skills/context-budget/) · [roast-me](skills/roast-me/) · [fable-operator](skills/fable-operator/)
385
+ [knowledge-ops](skills/knowledge-ops/) · [codebase-onboarding](skills/codebase-onboarding/) · [research-ops](skills/research-ops/) · [decision-records](skills/decision-records/) · [continuous-learning](skills/continuous-learning/) · [skill-scout](skills/skill-scout/) · [context-budget](skills/context-budget/) · [roast-me](skills/roast-me/) · [show-me](skills/show-me/) · [eli5](skills/eli5/) · [fable-operator](skills/fable-operator/)
386
386
 
387
387
  ---
388
388
 
package/manifest.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "1.1.0",
2
+ "version": "1.1.2",
3
3
  "counts": {
4
- "skills": 278
4
+ "skills": 281
5
5
  },
6
6
  "skills": [
7
7
  {
@@ -565,6 +565,7 @@
565
565
  "no-jargon"
566
566
  ],
567
567
  "recommends": [
568
+ "unslop",
568
569
  "brand-voice",
569
570
  "technical-writing",
570
571
  "translation-l10n"
@@ -1711,6 +1712,29 @@
1711
1712
  ],
1712
1713
  "profiles": []
1713
1714
  },
1715
+ {
1716
+ "id": "eli5",
1717
+ "description": "Use when a topic must be explained from zero to someone who knows nothing about it: one page, big pictures, very few words, everyday analogies, no jargon and nothing assumed. NOT the smallest in-conversation visual for someone already following (that is `show-me`), NOT a course with exercises (that is `course-builder`).",
1718
+ "tags": [
1719
+ "eli5",
1720
+ "explain",
1721
+ "beginner",
1722
+ "simple",
1723
+ "analogy",
1724
+ "no-jargon",
1725
+ "explicame"
1726
+ ],
1727
+ "recommends": [
1728
+ "show-me",
1729
+ "technical-writing",
1730
+ "course-builder"
1731
+ ],
1732
+ "profiles": [
1733
+ "minimal",
1734
+ "core",
1735
+ "full"
1736
+ ]
1737
+ },
1714
1738
  {
1715
1739
  "id": "elixir",
1716
1740
  "description": "Use when writing or refactoring Elixir/OTP on the BEAM — GenServers, supervision trees and restart strategies, pattern matching, mix projects and releases — or when processes misbehave (restart loops, mailbox growth, call timeouts). NOT a Phoenix web app, LiveView, Ecto or channels (that is `phoenix`).",
@@ -4687,6 +4711,31 @@
4687
4711
  ],
4688
4712
  "profiles": []
4689
4713
  },
4714
+ {
4715
+ "id": "show-me",
4716
+ "description": "Use when the user needs to see how something works instead of reading about it: picks the smallest visual that carries the point — pseudocode, call tree, file tree, component tree, Mermaid, a shaped diff, or one HTML page. NOT visual identity (that is `design-loop`), NOT slides (that is `presentations`), NOT a from-zero explainer (that is `eli5`).",
4717
+ "tags": [
4718
+ "show-me",
4719
+ "explain",
4720
+ "diagram",
4721
+ "visual",
4722
+ "mermaid",
4723
+ "diff",
4724
+ "call-tree",
4725
+ "ensename"
4726
+ ],
4727
+ "recommends": [
4728
+ "eli5",
4729
+ "presentations",
4730
+ "technical-writing",
4731
+ "codebase-onboarding"
4732
+ ],
4733
+ "profiles": [
4734
+ "minimal",
4735
+ "core",
4736
+ "full"
4737
+ ]
4738
+ },
4690
4739
  {
4691
4740
  "id": "simplify-code",
4692
4741
  "description": "Use when correct working code is unnecessarily hard to read, change or verify and needs behaviour-preserving simplification in a bounded scope. Characterizes the observable contract, removes accidental complexity one concept at a time and proves equivalence. NOT fixing a failure (`debug`), NOT read-only diff judgment (`review`/`code-review`).",
@@ -5362,6 +5411,29 @@
5362
5411
  "full"
5363
5412
  ]
5364
5413
  },
5414
+ {
5415
+ "id": "unslop",
5416
+ "description": "Use when a written text must be audited against a named catalogue of AI tells before it ships: puffery, `not just X but Y`, the rule of three, em dashes, filler, hedging, abstract metaphor nouns. Names each hit and fixes it. NOT the register rewrite in the user's own voice (that is `bro`), NOT a reusable voice guide (that is `brand-voice`).",
5417
+ "tags": [
5418
+ "unslop",
5419
+ "ai-tells",
5420
+ "editing",
5421
+ "audit",
5422
+ "slop",
5423
+ "publish-check",
5424
+ "revisar-texto"
5425
+ ],
5426
+ "recommends": [
5427
+ "bro",
5428
+ "brand-voice",
5429
+ "technical-writing"
5430
+ ],
5431
+ "profiles": [
5432
+ "minimal",
5433
+ "core",
5434
+ "full"
5435
+ ]
5436
+ },
5365
5437
  {
5366
5438
  "id": "unsloth",
5367
5439
  "description": "Use when fine-tuning an open-weight LLM fast on ONE GPU with low VRAM — Unsloth's fast model loaders with 4-bit QLoRA and the trl trainer, response-only loss masking so the prompt is not trained on, GRPO reasoning fine-tunes, and export to merged 16-bit, GGUF or the Hub. NOT whether, why or which method to fine-tune (that is `finetuning`), NOT running the exported GGUF locally (that is `ollama`), NOT serving-engine flags and throughput (that is `vllm`), NOT building the JSONL dataset (that is `training-data`).",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ericrisco/rsc",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Eric Risco's agent-skills catalog as a granular, self-recommending CLI installer.",
5
5
  "type": "module",
6
6
  "bin": {
package/scripts/doctor.js CHANGED
@@ -8,6 +8,7 @@ import { listBackups } from './lib/backups.js';
8
8
  import { SDD_GATE_TEXT } from '../targets/hook-once.mjs';
9
9
  import { isEnabled, checkSello, readSello, countFindings, readEffectiveConfig, validateRiskConfig } from '../targets/sello.mjs';
10
10
  import { designIdentity } from './lib/design-identity.js';
11
+ import { startingPointSummary } from './lib/starting-point.js';
11
12
  import { countGaps } from './lib/capabilities.js';
12
13
 
13
14
  // The sello's health, surfaced where the user already looks (spec: non-blocking
@@ -62,6 +63,10 @@ export function doctor({ target, home, cwd }) {
62
63
  // stops without one; until lib/design-identity.js nothing checked it (P2). Reported here,
63
64
  // never nagged about and never blocking: a missing identity is low risk (P7).
64
65
  designIdentity: designIdentity(root),
66
+ // The other half of the same question: an identity is what this project settled on, a starting
67
+ // point is what it has to settle FROM. Three skills promise to propose one and none could look;
68
+ // this is the look. One field, not a section — a report grows for every user who runs it (P5).
69
+ designStartingPoint: startingPointSummary(root),
65
70
  // An inert gate must never read as an armed one (the same rule the sello follows):
66
71
  // the gitmoji guard has a per-project kill switch, so say which state it is in.
67
72
  gitmojiGuard: existsSync(join(root, '.rsc', '.no-gitmoji')) ? 'opted-out' : 'armed',
@@ -4,7 +4,7 @@
4
4
  // catalog grows deliberately, never silently.
5
5
 
6
6
  export const DOMAINS = [
7
- { title: 'Core & control plane', ids: ['init', 'harness', 'orient', 'suggest', 'bro', 'author-skill', 'sdd-init'] },
7
+ { title: 'Core & control plane', ids: ['init', 'harness', 'orient', 'suggest', 'bro', 'unslop', 'author-skill', 'sdd-init'] },
8
8
  { title: 'Spec-Driven Development', ids: ['sdd', 'constitution', 'idea-refinement', 'specify', 'clarify', 'plan', 'tasks', 'analyze', 'decision-challenge', 'implement', 'source-grounded-development', 'verify', 'review', 'simplify-code', 'ship', 'debug', 'worktrees', 'parallel'] },
9
9
  { title: 'Run a business', ids: ['finance-ops', 'invoicing', 'bookkeeping', 'pricing', 'sales-pipeline', 'lead-gen', 'cold-outreach', 'proposals', 'contracts', 'customer-support', 'client-onboarding', 'retention', 'hiring', 'people-ops', 'inventory', 'logistics-ops', 'procurement', 'meeting-notes', 'sop-builder', 'project-ops'] },
10
10
  { title: 'Raise & model money', ids: ['pitch-deck', 'investor-materials', 'financial-model', 'fundraising', 'unit-economics', 'grants'] },
@@ -26,7 +26,7 @@ export const DOMAINS = [
26
26
  { title: 'Ship & operate — quality & security', ids: ['code-review', 'security-scan', 'secure-coding', 'testing-py', 'testing-web', 'testing-go', 'e2e-testing', 'accessibility', 'performance', 'error-handling', 'observability'] },
27
27
  { title: 'Motion & interface craft', ids: ['design-eng', 'animate', 'animate-expo', 'review-animations', 'improve-animations', 'find-animation-opportunities', 'animation-vocabulary', 'apple-design', 'prototype'] },
28
28
  { title: 'Design & content craft', ids: ['design-loop', 'design', 'design-dna', 'presentations', 'course-storytelling', 'course-builder', 'technical-writing', 'translation-l10n'] },
29
- { title: 'Knowledge & meta', ids: ['knowledge-ops', 'codebase-onboarding', 'research-ops', 'decision-records', 'continuous-learning', 'skill-scout', 'context-budget', 'roast-me', 'fable-operator'] },
29
+ { title: 'Knowledge & meta', ids: ['knowledge-ops', 'codebase-onboarding', 'research-ops', 'decision-records', 'continuous-learning', 'skill-scout', 'context-budget', 'roast-me', 'show-me', 'eli5', 'fable-operator'] },
30
30
  ];
31
31
 
32
32
  export function allDomainIds() {
@@ -0,0 +1,314 @@
1
+ // What does this project already own to start from, and does its colour hold up?
2
+ //
3
+ // The design area declares three times that it will propose a starting point when the user has
4
+ // none — `design-loop` phase 1 on `skip`, the closing line of `design`'s brand grounding, and
5
+ // `design-dna`'s REUSE mode — and none of the three had anything behind it. An unbacked promise to
6
+ // propose falls back to the model's prior, which is the AI-template median the area exists to
7
+ // escape. Spec: 02-DOCS/wiki/sdd/specs/design-starting-point.md
8
+ //
9
+ // This file is the half that can be an algorithm (P1): listing what is installed, and refusing to
10
+ // propose a colour that fails contrast. The half that needs judgement — which reference is the right
11
+ // bar for what you are building — is prose, in design/references/starting-point.md.
12
+ //
13
+ // Read-only, always. `02-DOCS` is untracked and has no undo (P9), and a discovery pass has no
14
+ // business writing anywhere.
15
+ import { existsSync, lstatSync, readFileSync, readdirSync, statSync } from 'node:fs';
16
+ import { join } from 'node:path';
17
+ import { homedir } from 'node:os';
18
+ import { designIdentity, BRAND_DIR } from './design-identity.js';
19
+
20
+ // WCAG 2.2 AA. Body text, and the relaxed floor for large text and UI components.
21
+ export const TEXT_MIN = 4.5;
22
+ export const UI_MIN = 3.0;
23
+
24
+ // Which palette roles are held to which floor. `design-dna`'s schema fixes the role vocabulary
25
+ // (ground, ink, accent, secondary, hairline); anything outside it is skipped rather than guessed at,
26
+ // because inventing a threshold for an unknown role is how a gate starts approving things.
27
+ const TEXT_ROLES = new Set(['ink', 'secondary']);
28
+ const UI_ROLES = new Set(['accent']);
29
+ const GROUND_ROLE = 'ground';
30
+
31
+ // A record is only reusable if it carries the fields a style is actually rebuilt from. Half a record
32
+ // cannot be reused, so it is reported as unreadable instead of offered as a candidate.
33
+ const REQUIRED = ['palette'];
34
+ const DIMENSIONS = ['palette', 'type', 'space', 'surface', 'motion', 'signatures', 'voice'];
35
+
36
+ /** Where `design-dna` emits, and where rsc installs, in both scopes. */
37
+ export function defaultRoots({ home = homedir(), cwd = process.cwd() } = {}) {
38
+ const roots = [];
39
+ for (const base of home === cwd ? [cwd] : [cwd, home]) {
40
+ roots.push(join(base, '.claude', 'skills'));
41
+ roots.push(join(base, '.rsc', 'skills'));
42
+ }
43
+ return roots;
44
+ }
45
+
46
+ function parseHex(hex) {
47
+ if (typeof hex !== 'string') return null;
48
+ const m = /^#([0-9a-fA-F]{6})$/.exec(hex.trim());
49
+ if (!m) return null;
50
+ const n = parseInt(m[1], 16);
51
+ return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
52
+ }
53
+
54
+ // WCAG relative luminance. The sRGB transfer function, not a shortcut: an approximation here is a
55
+ // wrong ratio everywhere downstream, and it would pass a test written against this file's own output.
56
+ function luminance([r, g, b]) {
57
+ const lin = [r, g, b].map((c) => {
58
+ const s = c / 255;
59
+ return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
60
+ });
61
+ return 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2];
62
+ }
63
+
64
+ /**
65
+ * WCAG 2.x contrast ratio between two colours, 1.0 … 21.0. Symmetric.
66
+ * @param {string} a sRGB hex, `#rrggbb`
67
+ * @param {string} b sRGB hex, `#rrggbb`
68
+ */
69
+ export function contrastRatio(a, b) {
70
+ const ca = parseHex(a);
71
+ const cb = parseHex(b);
72
+ if (!ca || !cb) throw new TypeError(`not an sRGB hex colour: ${!ca ? a : b}`);
73
+ const la = luminance(ca);
74
+ const lb = luminance(cb);
75
+ const [hi, lo] = la >= lb ? [la, lb] : [lb, la];
76
+ return (hi + 0.05) / (lo + 0.05);
77
+ }
78
+
79
+ /**
80
+ * Does this palette's text hold up against its own ground?
81
+ *
82
+ * Runs before a measured colour is written as a proposed dimension: a value that fails is not
83
+ * proposed at all, rather than proposed and flagged. Three outcomes are kept apart on purpose —
84
+ * measured-and-passing, measured-and-failing, and **not measured**. Collapsing the third into the
85
+ * first is the vacuous pass this area has already paid for twice.
86
+ *
87
+ * @param {Array<{role: string, hex: string, name?: string}>} colors — `palette.colors` from a dna.json
88
+ * @returns {{ok: boolean, pairs: Array<object>, failures: Array<object>, skipped: Array<object>,
89
+ * unparsed: string[], reason: string}}
90
+ */
91
+ export function checkPairings(colors) {
92
+ const list = Array.isArray(colors) ? colors : [];
93
+ const pairs = [];
94
+ const skipped = [];
95
+ const unparsed = [];
96
+
97
+ const ground = list.find((c) => c && c.role === GROUND_ROLE);
98
+ const groundHex = ground ? parseHex(ground.hex) : null;
99
+ if (!ground) {
100
+ return { ok: false, pairs, failures: [], skipped, unparsed, reason: 'no ground role in the palette — nothing to measure against' };
101
+ }
102
+ if (!groundHex) {
103
+ unparsed.push(String(ground.hex));
104
+ return { ok: false, pairs, failures: [], skipped, unparsed, reason: `ground colour is not an sRGB hex: ${ground.hex}` };
105
+ }
106
+
107
+ for (const c of list) {
108
+ if (!c || c.role === GROUND_ROLE) continue;
109
+ const isText = TEXT_ROLES.has(c.role);
110
+ const isUi = UI_ROLES.has(c.role);
111
+ if (!isText && !isUi) {
112
+ // A hairline is not text. Saying so is not the same as saying it passed.
113
+ skipped.push({ role: c.role, name: c.name, reason: 'role carries no text, so no text floor applies' });
114
+ continue;
115
+ }
116
+ if (!parseHex(c.hex)) {
117
+ unparsed.push(String(c.hex));
118
+ continue;
119
+ }
120
+ const min = isText ? TEXT_MIN : UI_MIN;
121
+ const ratio = Math.round(contrastRatio(c.hex, ground.hex) * 100) / 100;
122
+ pairs.push({ role: c.role, name: c.name, fg: c.hex, bg: ground.hex, ratio, min, ok: ratio >= min });
123
+ }
124
+
125
+ const failures = pairs.filter((p) => !p.ok);
126
+ // Not measuring anything is not a pass. An empty failure list only means green when at least one
127
+ // pair was actually evaluated and nothing was left unparsed.
128
+ const ok = failures.length === 0 && unparsed.length === 0 && pairs.length > 0;
129
+ const reason = unparsed.length
130
+ ? `${unparsed.length} colour(s) could not be parsed, so they were not measured`
131
+ : pairs.length === 0
132
+ ? 'no text or UI role to measure against the ground'
133
+ : failures.length
134
+ ? `${failures.length} pair(s) below their floor`
135
+ : `${pairs.length} pair(s) measured, all above their floor`;
136
+ return { ok, pairs, failures, skipped, unparsed, reason };
137
+ }
138
+
139
+ function readRecord(dir, slug) {
140
+ const file = join(dir, 'dna.json');
141
+ let raw;
142
+ try {
143
+ raw = readFileSync(file, 'utf8');
144
+ } catch (e) {
145
+ return { error: `cannot read dna.json: ${e.code || e.message}` };
146
+ }
147
+ let dna;
148
+ try {
149
+ dna = JSON.parse(raw);
150
+ } catch (e) {
151
+ return { error: `dna.json is not valid JSON: ${e.message}` };
152
+ }
153
+ const missing = REQUIRED.filter((k) => !dna || !dna[k]);
154
+ if (missing.length) return { error: `record is missing ${missing.join(', ')} — half a record cannot be reused` };
155
+
156
+ return {
157
+ record: {
158
+ slug: (dna.meta && dna.meta.slug) || slug,
159
+ name: (dna.meta && dna.meta.name) || slug,
160
+ path: file,
161
+ covers: DIMENSIONS.filter((d) => dna[d]),
162
+ palette: (dna.palette && dna.palette.colors) || [],
163
+ },
164
+ };
165
+ }
166
+
167
+ // What the harness's own identity articles mention. A record the project actually used outranks one
168
+ // that merely happens to be installed — but the others stay on the list, because choosing for the
169
+ // user is exactly what the spec forbids.
170
+ function citedSlugs(harnessRoot) {
171
+ const dir = join(harnessRoot, BRAND_DIR);
172
+ let text = '';
173
+ try {
174
+ for (const f of readdirSync(dir)) {
175
+ if (!f.endsWith('.md')) continue;
176
+ try { text += readFileSync(join(dir, f), 'utf8').toLowerCase(); } catch { /* one unreadable article is not the answer */ }
177
+ }
178
+ } catch { /* no identity here; designIdentity reports that half */ }
179
+ return text;
180
+ }
181
+
182
+ /**
183
+ * What this project already owns to start a design from.
184
+ *
185
+ * @param {string[]} roots — skill directories to scan. Defaults to both scopes, both layouts.
186
+ * @param {string} harnessRoot — where `02-DOCS/` lives, for the identity half and the ordering.
187
+ * @returns {{state: 'owned'|'none'|'inconclusive', identity: object, records: object[],
188
+ * unreadable: Array<{path: string, reason: string}>, reason: string, fix?: string}}
189
+ * `owned` — an identity or at least one legible record exists.
190
+ * `none` — looked everywhere, found nothing. Carries the way out (P6).
191
+ * `inconclusive` — something could not be read. Never reported as `none`, never as green.
192
+ */
193
+ export function ownedStartingPoints(roots = defaultRoots(), harnessRoot = process.cwd()) {
194
+ const identity = designIdentity(harnessRoot);
195
+ const records = [];
196
+ const unreadable = [];
197
+ let blind = 0;
198
+
199
+ for (const root of roots) {
200
+ // existsSync follows symlinks, so a dangling one answers false and would be filed as "nothing
201
+ // installed here". That is unreadability, not absence — the exact conflation designIdentity was
202
+ // fixed for. lstat sees the link itself and tells them apart.
203
+ if (!existsSync(root)) {
204
+ let dangling = false;
205
+ try { dangling = lstatSync(root).isSymbolicLink(); } catch { /* genuinely absent */ }
206
+ if (dangling) {
207
+ blind += 1;
208
+ unreadable.push({ path: root, reason: 'skills root is a symlink that does not resolve' });
209
+ }
210
+ continue;
211
+ }
212
+ let entries;
213
+ try {
214
+ if (!statSync(root).isDirectory()) {
215
+ blind += 1;
216
+ unreadable.push({ path: root, reason: 'skills root exists but is not a directory' });
217
+ continue;
218
+ }
219
+ entries = readdirSync(root, { withFileTypes: true });
220
+ } catch (e) {
221
+ blind += 1;
222
+ unreadable.push({ path: root, reason: `cannot read skills root: ${e.code || e.message}` });
223
+ continue;
224
+ }
225
+ for (const entry of entries) {
226
+ // Dirents, not statSync: a symlinked entry could otherwise report on a directory outside the
227
+ // root we were asked to scan.
228
+ if (!entry.isDirectory()) continue;
229
+ const dir = join(root, entry.name);
230
+ if (!existsSync(join(dir, 'dna.json'))) continue;
231
+ const { record, error } = readRecord(dir, entry.name);
232
+ if (error) { unreadable.push({ path: join(dir, 'dna.json'), reason: error }); continue; }
233
+ if (!records.some((r) => r.slug === record.slug)) records.push(record);
234
+ }
235
+ }
236
+
237
+ const cited = citedSlugs(harnessRoot);
238
+ records.sort((a, b) => {
239
+ const ac = cited.includes(a.slug.toLowerCase()) ? 0 : 1;
240
+ const bc = cited.includes(b.slug.toLowerCase()) ? 0 : 1;
241
+ return ac - bc || a.slug.localeCompare(b.slug);
242
+ });
243
+
244
+ if (records.length || identity.state === 'present') {
245
+ return {
246
+ state: 'owned',
247
+ identity,
248
+ records,
249
+ unreadable,
250
+ reason: [
251
+ identity.state === 'present' ? `identity present (${identity.articles.length} article(s))` : 'no harness identity',
252
+ `${records.length} style record(s)`,
253
+ unreadable.length ? `${unreadable.length} unreadable` : null,
254
+ ].filter(Boolean).join(', '),
255
+ };
256
+ }
257
+
258
+ // Something was there and we could not use it. Saying "nothing to propose" would be a finding we
259
+ // did not earn, and saying nothing at all would hide it.
260
+ if (unreadable.length || blind || identity.state === 'inconclusive') {
261
+ return {
262
+ state: 'inconclusive',
263
+ identity,
264
+ records,
265
+ unreadable,
266
+ reason: unreadable.length
267
+ ? `could not read ${unreadable.length} candidate(s): ${unreadable[0].reason}`
268
+ : `identity could not be read: ${identity.reason}`,
269
+ fix: 'name what could not be read, and ask for another route to it — never propose past a blind spot',
270
+ };
271
+ }
272
+
273
+ return {
274
+ state: 'none',
275
+ identity,
276
+ records: [],
277
+ unreadable,
278
+ reason: 'no harness identity and no style record installed in any scope',
279
+ fix: 'propose at most three openable references as candidate bars (design/references/starting-point.md), then run `design-loop`',
280
+ };
281
+ }
282
+
283
+ /**
284
+ * The compact projection `doctor` reports: one object, not a section (P5 — every byte of a report is
285
+ * paid by every user who runs it). Contrast is folded in per record because a record whose own
286
+ * palette fails is not a starting point anyone should be offered.
287
+ *
288
+ * @returns {{state: string, records: string[], contrast: string, unreadable: number, reason: string}}
289
+ */
290
+ export function startingPointSummary(root = process.cwd(), roots = defaultRoots({ cwd: root })) {
291
+ const r = ownedStartingPoints(roots, root);
292
+ let failing = 0;
293
+ let unmeasured = 0;
294
+ for (const rec of r.records) {
295
+ const c = checkPairings(rec.palette);
296
+ if (c.failures.length) failing += 1;
297
+ else if (!c.ok) unmeasured += 1;
298
+ }
299
+ const contrast = r.records.length === 0
300
+ ? 'no record to measure'
301
+ : failing
302
+ ? `${failing} record(s) with a pair below its floor`
303
+ : unmeasured
304
+ ? `${unmeasured} record(s) could not be measured`
305
+ : 'all records above their floors';
306
+ return {
307
+ state: r.state,
308
+ records: r.records.map((x) => x.slug),
309
+ contrast,
310
+ unreadable: r.unreadable.length,
311
+ reason: r.reason,
312
+ ...(r.fix ? { fix: r.fix } : {}),
313
+ };
314
+ }
package/scripts/rsc.js CHANGED
@@ -161,17 +161,21 @@ async function wizard() {
161
161
  const m = loadManifest();
162
162
  await banner(m.counts.skills);
163
163
  say(' the skill catalog for your assistant (Claude Code · Codex · Cursor · Gemini · Antigravity)\n');
164
+ // The base set is READ from the manifest, never enumerated here: a hand-kept list in a menu
165
+ // label is parallel accounting (P3), and it silently starts lying the day a skill's profiles
166
+ // change. The exact ids are printed before the install confirmation either way.
167
+ const baseIds = skillsForProfile(m, 'minimal');
164
168
  // Navigable loop: esc / "← Back" / "no" all return here instead of quitting.
165
169
  for (;;) {
166
170
  const choice = await select('What do you want to do?', [
167
- { key: 'base', label: 'Base install — the essentials (orient + suggest + bro + harness + init)' },
171
+ { key: 'base', label: `Base install — the essentials (${baseIds.length} skills)` },
168
172
  { key: 'sdd', label: 'Base + Spec-Driven Development — the specify → plan → implement → ship flow' },
169
173
  { key: 'manual', label: 'Pick skills by hand, by area' },
170
174
  ]);
171
175
  if (choice === null) { say('\nOK — nothing installed. Anytime: npx @ericrisco/rsc'); return; }
172
176
 
173
177
  let ids;
174
- if (choice === 'base') ids = skillsForProfile(m, 'minimal');
178
+ if (choice === 'base') ids = baseIds;
175
179
  else if (choice === 'sdd') ids = skillsForProfile(m, 'core');
176
180
  else if (choice === 'manual') {
177
181
  const picked = await manualSelect();
@@ -2,7 +2,7 @@
2
2
  name: bro
3
3
  description: "Use whenever the user asks to make an answer or draft sound human, natural, plain-spoken or less AI-written — including a terse bro after the last response. Rewrites or drafts in the same language while preserving meaning, facts and channel. NOT a reusable brand voice (that is `brand-voice`), NOT translation (that is `translation-l10n`)."
4
4
  tags: [bro, human-writing, humano, plain-language, natural-language, rewrite, no-jargon]
5
- recommends: [brand-voice, technical-writing, translation-l10n]
5
+ recommends: [unslop, brand-voice, technical-writing, translation-l10n]
6
6
  profiles: [minimal, core, full]
7
7
  origin: risco
8
8
  ---
@@ -57,6 +57,14 @@ plain language instead of deleting precision.
57
57
  | Promise the result is undetectable as AI | No rewrite can honestly guarantee that | Promise clarity and naturalness, not detector outcomes |
58
58
  | Announce and justify every edit | The preamble becomes the same friction the user asked to remove | Lead with the finished words |
59
59
 
60
+ ## Where this ends
61
+
62
+ `unslop` owns the other half of this job: the **named** audit of a text that already exists, tell by
63
+ catalogued tell (puffery, `not just X but Y`, em dashes, bold-label lists, hedging), run before the
64
+ text ships. This skill owns the **register**: which language, which relationship, which channel, and
65
+ the cadence that fits them. A bare "bro" is always this skill. "Check this for AI tells before I
66
+ publish it" is `unslop`. When a text needs both, audit first, register last.
67
+
60
68
  ## Output boundary
61
69
 
62
70
  Treat the rewritten text as an artifact: keep harness commentary outside it. If the task also
@@ -46,6 +46,10 @@ should_not_trigger:
46
46
  route_to: "simplify-code"
47
47
  why: "The requested source-code simplification is owned by simplify-code; 'rewrite' does not make it a prose task."
48
48
 
49
+ - prompt: "Revisa este README antes de publicarlo y dime qué frases suenan a IA, una por una."
50
+ route_to: "unslop"
51
+ why: "A named, per-phrase audit of an existing text before shipping is unslop's contract; bro rewrites the register instead of enumerating tells."
52
+
49
53
  capability:
50
54
  - scenario: "The previous assistant answer is a long Spanish status update full of headings, repeated summaries, corporate jargon and fake enthusiasm. Rewrite it for a client while preserving a June 30 deadline, a EUR 2,400 price, one unresolved risk, two URLs and the original uncertainty."
51
55
  must_include:
@@ -35,6 +35,8 @@ Follow the harness 02-DOCS convention (brand study = wiki articles under `02-DOC
35
35
 
36
36
  The completeness checklist spans visual identity (OKLCH color system, type pairing & scale, logo, imagery/illustration mood, density, radius/shadow/motion personality), reference/inspiration sites the user loves, layout preferences, dark-mode stance, accessibility & performance constraints, and brand voice/positioning (so copy and design agree). Full checklist + exact question script → `references/brand-grounding.md`.
37
37
 
38
+ **When a dimension has no answer** — the non-technical default, and the reason this gate used to be impassable: do not invent one and do not wave the STOP through. `references/starting-point.md` is how a starting point gets proposed instead: what the project already owns is checked first, the value comes from a reference you opened and measured, and it is written marked `propuesto` — which does **not** count as complete until the user confirms it.
39
+
38
40
  The order is: **brand grounding → trend research → build.**
39
41
 
40
42
  ## Pick a direction first
@@ -66,7 +68,7 @@ Then map the project type to composition, density, and motion budget. Density an
66
68
  Trends churn quarterly and your built-in aesthetic prior is the median of every AI template ever scraped. Never prescribe from stale memory; counter it with a loop:
67
69
 
68
70
  1. Define 2–3 reference archetypes from the DIRECTION BRIEF (e.g. "Linear-grade dev tool, dark, type-led").
69
- 2. WebSearch award galleries and tier-1 product sites: `awwwards.com`, `godly.website`, `land-book.com`, `mobbin.com`, Refactoring UI, and the tier-1 sites (Linear, Stripe, Vercel, Cursor, Resend).
71
+ 2. Browse the dated registry in `references/inspiration-sources.md` — whole pages to beat, SaaS and conversion surfaces, real product flows (Mobbin, Page Flows), single patterns, and the design systems that are specification rather than inspiration — plus the tier-1 sites (Linear, Stripe, Vercel, Cursor, Resend). Every entry there carries a verification date; `godly.website` now redirects to `recent.design`, which is exactly why the list is dated and not inline.
70
72
  3. WebFetch 3–5 exemplars, prompting each for type, color, layout, motion, and copy voice — concrete details, not adjectives.
71
73
  4. Extract a pattern table from what they share and where they differ.
72
74
  5. Synthesize a one-paragraph DESIGN DIRECTION with citations (which URL contributed what).
@@ -96,7 +96,7 @@ Ask **one batch at a time**. Send the batch, wait for the answer, persist what y
96
96
  extend rather than replace?
97
97
  ```
98
98
 
99
- If the user can't answer a question, that dimension stays incomplete — note the gap, keep the STOP in place for that dimension, and offer to propose a researched default (from the research-first protocol / `trends-2026.md`) they can confirm, rather than silently defaulting to your generic prior.
99
+ If the user can't answer a question, that dimension stays incomplete — note the gap, keep the STOP in place for that dimension, and offer to propose a researched default they can confirm, rather than silently defaulting to your generic prior. **How to actually propose one: `starting-point.md`.** It is the difference between offering something and improvising something: what the project already owns is checked first, the value comes from a reference you opened and measured, and it is written marked `propuesto`. A proposed-and-unconfirmed dimension does **not** count as complete against the checklist above — the STOP stays up until the user confirms it.
100
100
 
101
101
  ## Persistence format
102
102
 
@@ -39,3 +39,11 @@ Everything in `../SKILL.md` that is not styling: the brand gate, the accessibili
39
39
  the Core Web Vitals budget, the 5-second value-prop test, and the tell registry in
40
40
  `../scripts/verify.sh`. An official design system settles which components you use. It does
41
41
  not settle whether the page says anything.
42
+
43
+ ## Where to browse for component ideas
44
+
45
+ When a surface needs an idea rather than a primitive, `../../pick-ui-library/references/component-galleries.md`
46
+ names where to go — Aceternity for marketing effects, 21st.dev for breadth, React Bits for tunable
47
+ animation — and what each is good for. Take the technique and rebuild it in this project's tokens;
48
+ check the brand study for what it refuses first, because a component that contradicts a stated ban
49
+ makes the page look less designed, not more.