@ericrisco/rsc 1.0.10 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +15 -13
  2. package/manifest.json +133 -2
  3. package/package.json +3 -3
  4. package/scripts/consult.js +75 -42
  5. package/scripts/lib/default-skill-floor.js +11 -0
  6. package/scripts/lib/domains.js +3 -3
  7. package/scripts/lib/recommend.js +1 -0
  8. package/scripts/lib/text-rank.js +112 -0
  9. package/scripts/lib/ui.js +3 -3
  10. package/scripts/routing-audit.js +163 -0
  11. package/scripts/rsc.js +11 -10
  12. package/skills/author-skill/references/rsc-conventions.md +1 -1
  13. package/skills/bro/SKILL.md +68 -0
  14. package/skills/bro/evals/README.md +22 -0
  15. package/skills/bro/evals/cases.yaml +58 -0
  16. package/skills/decision-challenge/SKILL.md +113 -0
  17. package/skills/decision-challenge/evals/README.md +3 -0
  18. package/skills/decision-challenge/evals/cases.yaml +42 -0
  19. package/skills/deprecation/SKILL.md +123 -0
  20. package/skills/deprecation/evals/README.md +3 -0
  21. package/skills/deprecation/evals/cases.yaml +42 -0
  22. package/skills/idea-refinement/SKILL.md +121 -0
  23. package/skills/idea-refinement/evals/README.md +3 -0
  24. package/skills/idea-refinement/evals/cases.yaml +42 -0
  25. package/skills/observability/SKILL.md +6 -0
  26. package/skills/observability/evals/cases.yaml +2 -0
  27. package/skills/performance/SKILL.md +13 -0
  28. package/skills/performance/evals/cases.yaml +2 -0
  29. package/skills/simplify-code/SKILL.md +101 -0
  30. package/skills/simplify-code/evals/README.md +3 -0
  31. package/skills/simplify-code/evals/cases.yaml +42 -0
  32. package/skills/source-grounded-development/SKILL.md +108 -0
  33. package/skills/source-grounded-development/evals/README.md +3 -0
  34. package/skills/source-grounded-development/evals/cases.yaml +42 -0
  35. package/skills/suggest/evals/cases.yaml +1 -1
package/README.md CHANGED
@@ -12,7 +12,7 @@
12
12
  ██ ██ ███████ ███████
13
13
  ```
14
14
 
15
- # `rsc` — 256 agent skills, one CLI, zero bloat
15
+ # `rsc` — 264 agent skills, one CLI, zero bloat
16
16
 
17
17
  **A self-recommending skill catalog for 17 coding assistants** — Claude Code,
18
18
  Codex, GitHub Copilot, Cursor, Gemini, Windsurf, Cline, Antigravity, Zed and more.
@@ -21,7 +21,7 @@ skills that fit — one at a time — into every assistant you pick, and keeps t
21
21
  equipped as you work.
22
22
 
23
23
  From *"document my company"* to *"ship a FastAPI service"* to *"grow my YouTube
24
- channel"* — **232 skills across 21 domains**, every one researched against live
24
+ channel"* — **264 skills across 21 domains**, every one researched against live
25
25
  2025-2026 sources and **adversarially scored ≥ 8.5/10** before it shipped.
26
26
 
27
27
  ```bash
@@ -43,7 +43,7 @@ npx @ericrisco/rsc@latest
43
43
  ```
44
44
 
45
45
  That launches a plain-language wizard: it asks **which assistants** to target and **what the
46
- user wants**, reads the repo, then installs the floor (`orient` + `rsc-suggest` + `harness` +
46
+ user wants**, reads the repo, then installs the floor (`orient` + `rsc-suggest` + `bro` + `harness` +
47
47
  `init`) plus only the skills that fit — one at a time. No global install and no API key needed.
48
48
 
49
49
  - **Choose assistants non-interactively:** `npx @ericrisco/rsc@latest --target claude` (comma-separate for several).
@@ -109,7 +109,7 @@ cd ~/rsc-skills && npm install && npm link
109
109
  The first run asks **which assistants** you want — Claude Code, Codex, Copilot,
110
110
  Cursor, Gemini, Windsurf, Cline and 11 more (pick any combination) — and installs
111
111
  the **floor**:
112
- `orient` + `rsc-suggest` (always-on) + `harness` + `init`. In Claude Code it
112
+ `orient` + `rsc-suggest` (always-on) + `bro` (on request) + `harness` + `init`. In Claude Code it
113
113
  also wires a `SessionStart` hook (so your assistant proposes new skills on its
114
114
  own) and a `UserPromptSubmit` hook that re-asserts the **SDD new-feature gate**
115
115
  on every turn — so a feature request, in any language, routes through `specify`
@@ -129,10 +129,10 @@ $ rsc
129
129
  ██████╗ ███████╗ ██████╗ ← animated gradient wordmark
130
130
  ██╔══██╗██╔════╝██╔════╝
131
131
  ██████╔╝███████╗██║
132
- 232 skills · one CLI · zero bloat
132
+ 264 skills · one CLI · zero bloat
133
133
 
134
134
  What do you want to do? ↑↓ move · enter select
135
- ❯ Base install — the essentials (orient + suggest + harness + init)
135
+ ❯ Base install — the essentials (orient + suggest + bro + harness + init)
136
136
  Base + Spec-Driven Development — specify → plan → implement → ship
137
137
  Pick skills by hand, by area
138
138
  ```
@@ -175,9 +175,9 @@ rsc # plain-language wizard (recommended) — p
175
175
  rsc add fastapi postgresdb # install specific skills, by name
176
176
  rsc add youtube-api remotion-video # …grow a channel, edit with Remotion
177
177
  rsc add fastapi --target claude,codex # install into several assistants at once
178
- rsc install --profile minimal # the floor: orient + suggest + harness + init
178
+ rsc install --profile minimal # the floor: orient + suggest + bro + harness + init
179
179
  rsc install --profile core # floor + the full SDD workflow
180
- rsc install --profile full # everything (all 231)
180
+ rsc install --profile full # everything (all 264)
181
181
  rsc install --profile full --without go
182
182
  rsc consult "I want to launch a SaaS" # recommend only, no install
183
183
  rsc registry refresh # write .rsc/skill-registry.{json,md}
@@ -228,7 +228,7 @@ rsc restore <snapshot-id> # restore it
228
228
  Two faces, one catalog (`manifest.json`):
229
229
 
230
230
  - **In the terminal** — `rsc` / `rsc consult` rank the catalog against your words
231
- (an FTS index over each skill's description + tags), merge that with what they
231
+ (multilingual TF-IDF blended with exact tag/id weights and intent synonyms), merge that with what they
232
232
  detect in your repo, and expand via each skill's `recommends`.
233
233
  - **In the chat** — `rsc-suggest` is a tiny always-on skill. When a task would
234
234
  benefit from a skill you don't have, it names it and (one-word confirm) runs
@@ -243,18 +243,20 @@ just asks in plain language.
243
243
 
244
244
  ## The catalog
245
245
 
246
- 257 skills, grouped by what you're trying to do. Click any skill to read its
246
+ 264 skills, grouped by what you're trying to do. Click any skill to read its
247
247
  `SKILL.md`. It fires on its own when a task matches.
248
248
 
249
249
  ### 🧭 Core & control plane
250
250
  The front door and the workspace brain.
251
251
 
252
- [init](skills/init/) · [harness](skills/harness/) · [orient](skills/orient/) · [suggest](skills/suggest/) · [author-skill](skills/author-skill/) · [sdd-init](skills/sdd-init/)
252
+ [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/)
253
253
 
254
254
  > **harness** is the Karpathy *chaos→knowledge* engine — a `01-TOOLS/` layer (one
255
255
  > folder per provider, each with a working `test_connection`) and a `02-DOCS/`
256
256
  > self-improving wiki. It governs software *or* a whole company. **orient** is the
257
257
  > always-on compass that keeps a non-technical human oriented after every step.
258
+ > **bro** is installed with every profile and rewrites any answer in plain, natural
259
+ > language when the user asks — without making its full body always-on.
258
260
 
259
261
  > #### 📦 The `02-DOCS/` brain is now 100% Open Knowledge Format (OKF v0.1) conformant
260
262
  >
@@ -279,7 +281,7 @@ The front door and the workspace brain.
279
281
  ### 📐 Spec-Driven Development
280
282
  Take a fuzzy intent to a shipped, verified change — phase by phase. `npx @ericrisco/rsc install --profile core`.
281
283
 
282
- [sdd](skills/sdd/) · [constitution](skills/constitution/) · [specify](skills/specify/) · [clarify](skills/clarify/) · [plan](skills/plan/) · [tasks](skills/tasks/) · [analyze](skills/analyze/) · [implement](skills/implement/) · [verify](skills/verify/) · [review](skills/review/) · [ship](skills/ship/) · [debug](skills/debug/) · [worktrees](skills/worktrees/) · [parallel](skills/parallel/)
284
+ [sdd](skills/sdd/) · [constitution](skills/constitution/) · [idea-refinement](skills/idea-refinement/) · [specify](skills/specify/) · [clarify](skills/clarify/) · [plan](skills/plan/) · [tasks](skills/tasks/) · [analyze](skills/analyze/) · [decision-challenge](skills/decision-challenge/) · [implement](skills/implement/) · [source-grounded-development](skills/source-grounded-development/) · [verify](skills/verify/) · [review](skills/review/) · [simplify-code](skills/simplify-code/) · [ship](skills/ship/) · [debug](skills/debug/) · [worktrees](skills/worktrees/) · [parallel](skills/parallel/)
283
285
 
284
286
  ### 💼 Run a business
285
287
 
@@ -354,7 +356,7 @@ Three engines + engine-agnostic disciplines. Every engine skill pins the current
354
356
 
355
357
  ### 🛠️ Ship & operate — devops
356
358
 
357
- [docker](skills/docker/) · [github-actions](skills/github-actions/) · [git-workflow](skills/git-workflow/) · [domains-dns](skills/domains-dns/) · [monitoring](skills/monitoring/) · [email-deliverability](skills/email-deliverability/) · [scaling](skills/scaling/) · [deployment](skills/deployment/)
359
+ [docker](skills/docker/) · [github-actions](skills/github-actions/) · [git-workflow](skills/git-workflow/) · [domains-dns](skills/domains-dns/) · [monitoring](skills/monitoring/) · [email-deliverability](skills/email-deliverability/) · [scaling](skills/scaling/) · [deployment](skills/deployment/) · [deprecation](skills/deprecation/)
358
360
 
359
361
  ### 🔒 Ship & operate — quality & security
360
362
 
package/manifest.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "1.0.10",
2
+ "version": "1.0.11",
3
3
  "counts": {
4
- "skills": 258
4
+ "skills": 264
5
5
  },
6
6
  "skills": [
7
7
  {
@@ -458,6 +458,29 @@
458
458
  ],
459
459
  "profiles": []
460
460
  },
461
+ {
462
+ "id": "bro",
463
+ "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`).",
464
+ "tags": [
465
+ "bro",
466
+ "human-writing",
467
+ "humano",
468
+ "plain-language",
469
+ "natural-language",
470
+ "rewrite",
471
+ "no-jargon"
472
+ ],
473
+ "recommends": [
474
+ "brand-voice",
475
+ "technical-writing",
476
+ "translation-l10n"
477
+ ],
478
+ "profiles": [
479
+ "minimal",
480
+ "core",
481
+ "full"
482
+ ]
483
+ },
461
484
  {
462
485
  "id": "building-agents",
463
486
  "description": "Use when building or restructuring an LLM agent — provider adapter, tool calling, structured output, RAG, agent loop, eval gate, cost routing, tracing, MCP server — model-agnostic across OpenAI/Anthropic/Gemini/OSS so a model swap is a config change. NOT vector-store SQL alone (that is `postgresdb`) or service deployment (that is `deployment`).",
@@ -1168,6 +1191,28 @@
1168
1191
  "full"
1169
1192
  ]
1170
1193
  },
1194
+ {
1195
+ "id": "decision-challenge",
1196
+ "description": "Use when a high-impact plan, migration or architecture claim needs a bounded adversarial challenge before commitment. Isolates claims from persuasion, attacks assumptions, reconciles doubts with evidence and emits proceed/hold/stop. NOT a finished-diff review (`review`/`code-review`), NOT SDD artifact consistency (`analyze`).",
1197
+ "tags": [
1198
+ "adversarial-review",
1199
+ "decision-quality",
1200
+ "assumptions",
1201
+ "risk",
1202
+ "preflight"
1203
+ ],
1204
+ "recommends": [
1205
+ "analyze",
1206
+ "review",
1207
+ "code-review",
1208
+ "decision-records",
1209
+ "security-scan"
1210
+ ],
1211
+ "profiles": [
1212
+ "core",
1213
+ "full"
1214
+ ]
1215
+ },
1171
1216
  {
1172
1217
  "id": "decision-records",
1173
1218
  "description": "Use when a hard-to-reverse choice (database, framework, vendor, auth model) must be frozen as an immutable numbered ADR — the context that forced it, the options weighed, the decision, consequences both ways — or superseded without erasing history. NOT a meeting recap (that is meeting-notes), NOT standing project principles (that is constitution).",
@@ -1232,6 +1277,28 @@
1232
1277
  ],
1233
1278
  "profiles": []
1234
1279
  },
1280
+ {
1281
+ "id": "deprecation",
1282
+ "description": "Use when retiring a public API, feature, integration, service or legacy path without abandoning consumers. Inventories usage, classifies advisory vs compulsory migration, ships compatibility/tooling, assigns owners and gates removal on evidence. NOT live schema expand-contract (`db-migrations`), NOT replacement API design (`api-design`).",
1283
+ "tags": [
1284
+ "deprecation",
1285
+ "migration",
1286
+ "compatibility",
1287
+ "consumer-migration",
1288
+ "removal"
1289
+ ],
1290
+ "recommends": [
1291
+ "api-design",
1292
+ "db-migrations",
1293
+ "deployment",
1294
+ "git-workflow",
1295
+ "monitoring"
1296
+ ],
1297
+ "profiles": [
1298
+ "core",
1299
+ "full"
1300
+ ]
1301
+ },
1235
1302
  {
1236
1303
  "id": "design",
1237
1304
  "description": "Use when designing or refreshing a web UI or landing page — visual concept, type/color/spacing/motion tokens, composition, rescuing a UI that reads AI-generic, or a graded design review. Brand-grounded and research-first; ships Tailwind v4 + Next.js 15 under WCAG 2.2 AA and Core Web Vitals budgets. NOT the words on the page (that is `marketing`), NOT the App Router build (that is `nextjs`).",
@@ -2278,6 +2345,27 @@
2278
2345
  ],
2279
2346
  "profiles": []
2280
2347
  },
2348
+ {
2349
+ "id": "idea-refinement",
2350
+ "description": "Use when a rough product, service, workflow or feature idea needs divergent options and assumption pressure-testing before it becomes a spec. Produces a decision brief with alternatives, evidence gaps, Not Now and a cheap experiment. NOT requirements or acceptance criteria (`specify`), NOT market sizing (`market-research`).",
2351
+ "tags": [
2352
+ "ideation",
2353
+ "idea-refinement",
2354
+ "product-discovery",
2355
+ "assumptions",
2356
+ "decision-brief"
2357
+ ],
2358
+ "recommends": [
2359
+ "specify",
2360
+ "market-research",
2361
+ "competitor-watch",
2362
+ "ab-testing"
2363
+ ],
2364
+ "profiles": [
2365
+ "core",
2366
+ "full"
2367
+ ]
2368
+ },
2281
2369
  {
2282
2370
  "id": "implement",
2283
2371
  "description": "Use when an approved plan and task list exist and it is time to turn them into working, tested code — the SDD phase after `analyze` and before `verify`. Enforces TDD: a failing test comes before the code that makes it pass, one task at a time, appended to a progress ledger that survives compaction. Delegates test tooling to the stack skill and fans disjoint tasks out via `parallel`. NOT spec writing (that is `specify`), NOT planning (that is `plan`), NOT the final lint/test/audit gate (that is `verify`).",
@@ -4346,6 +4434,28 @@
4346
4434
  ],
4347
4435
  "profiles": []
4348
4436
  },
4437
+ {
4438
+ "id": "simplify-code",
4439
+ "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`).",
4440
+ "tags": [
4441
+ "code-simplification",
4442
+ "refactoring",
4443
+ "readability",
4444
+ "complexity",
4445
+ "behavior-preservation"
4446
+ ],
4447
+ "recommends": [
4448
+ "code-review",
4449
+ "review",
4450
+ "verify",
4451
+ "debug",
4452
+ "performance"
4453
+ ],
4454
+ "profiles": [
4455
+ "core",
4456
+ "full"
4457
+ ]
4458
+ },
4349
4459
  {
4350
4460
  "id": "skill-scout",
4351
4461
  "description": "Use when a specialized skill probably exists but is not loaded, when a task feels harder than it should because generic reasoning is doing a domain skill's job, when the work just delivered was a repeatable procedure worth turning into a skill or an agent, or when auditing coverage for holes. Resolves against what already exists — installed skills, catalog skills and agent files — then names the gap and emits the install command. NOT routing among skills you already have (that is `suggest`), NOT writing the artifact when none exists (that is `author-skill` for skills, `building-agents` for agents).",
@@ -4423,6 +4533,27 @@
4423
4533
  ],
4424
4534
  "profiles": []
4425
4535
  },
4536
+ {
4537
+ "id": "source-grounded-development",
4538
+ "description": "Use when implementation or review depends on current version-specific framework, SDK, protocol or platform behaviour and memory is unsafe. Detects exact local versions, verifies primary sources, tests applicability and marks unknowns. NOT a standalone cited memo (`research-ops`), NOT diagnosis of a reproduced failure (`debug`).",
4539
+ "tags": [
4540
+ "official-docs",
4541
+ "primary-sources",
4542
+ "version-detection",
4543
+ "implementation",
4544
+ "verification"
4545
+ ],
4546
+ "recommends": [
4547
+ "research-ops",
4548
+ "decision-records",
4549
+ "debug",
4550
+ "technical-writing"
4551
+ ],
4552
+ "profiles": [
4553
+ "core",
4554
+ "full"
4555
+ ]
4556
+ },
4426
4557
  {
4427
4558
  "id": "specify",
4428
4559
  "description": "Use when a feature, change, or product idea is still fuzzy and must become an approved WHAT/WHY spec before any planning or code — the brainstorming front door of SDD. Turns a one-line intent into problem, goals, users, scope, behaviour and acceptance criteria, via one-question-at-a-time dialogue and 2-3 approaches with a recommendation, carrying zero implementation detail. Fires whenever someone jumps to HOW before WHAT is agreed, in any language. NOT the technical plan (that is `plan`), NOT the de-risking ambiguity sweep (that is `clarify`), NOT project-wide principles (that is `constitution`).",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ericrisco/rsc",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "description": "Eric Risco's agent-skills catalog as a granular, self-recommending CLI installer.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,6 +16,7 @@
16
16
  "validate": "node scripts/build-manifest.js --validate",
17
17
  "drift:check": "node scripts/drift-check.js",
18
18
  "drift:knowledge": "node scripts/drift-check.js --knowledge",
19
+ "routing:audit": "node scripts/routing-audit.js",
19
20
  "test": "node --test",
20
21
  "coverage": "c8 --reporter=text node --test",
21
22
  "version": "npm run manifest && git add manifest.json",
@@ -23,8 +24,7 @@
23
24
  },
24
25
  "dependencies": {
25
26
  "@iarna/toml": "^2.2.5",
26
- "ajv": "^8.18.0",
27
- "sql.js": "^1.14.1"
27
+ "ajv": "^8.18.0"
28
28
  },
29
29
  "devDependencies": {
30
30
  "c8": "^10.1.3"
@@ -1,4 +1,4 @@
1
- import initSqlJs from 'sql.js';
1
+ import { buildTextCorpus, rankText } from './lib/text-rank.js';
2
2
 
3
3
  // Stopwords (es + en + common EU words) — these match everywhere via LIKE and drown the signal.
4
4
  const STOP = new Set([
@@ -43,6 +43,43 @@ const SYNONYMS = {
43
43
  presentacion: ['presentations'], 'presentación': ['presentations'], diapositivas: ['slides'],
44
44
  curso: ['course', 'teaching'], 'enseñar': ['teaching'], ensenar: ['teaching'],
45
45
  marketing: ['marketing'], copy: ['copywriting'], texto: ['copywriting'], textos: ['copywriting'],
46
+ human: ['bro', 'human-writing', 'natural-language', 'plain-language'],
47
+ humano: ['bro', 'human-writing', 'natural-language', 'plain-language'],
48
+ humana: ['bro', 'human-writing', 'natural-language', 'plain-language'],
49
+ huma: ['bro', 'human-writing', 'natural-language', 'plain-language'],
50
+ natural: ['bro', 'human-writing', 'natural-language', 'plain-language'],
51
+ robotico: ['bro', 'human-writing'], robotica: ['bro', 'human-writing'],
52
+ robotic: ['bro', 'human-writing'], roboticamente: ['bro', 'human-writing'],
53
+ jerga: ['bro', 'plain-language', 'no-jargon'], jargon: ['bro', 'plain-language', 'no-jargon'],
54
+ // Cross-cutting workflow vocabulary. These map multilingual/colloquial
55
+ // wording to the exact compound tags used by the catalog.
56
+ challenge: ['adversarial-review', 'decision-quality', 'preflight'], challenged: ['adversarial-review'],
57
+ cuestiona: ['adversarial-review', 'decision-quality', 'preflight'], cuestionar: ['adversarial-review'],
58
+ 'qüestiona': ['adversarial-review', 'decision-quality', 'preflight'],
59
+ supuestos: ['assumptions', 'adversarial-review'], suposiciones: ['assumptions'],
60
+ afirmaciones: ['assumptions', 'adversarial-review'], claims: ['assumptions', 'adversarial-review'],
61
+ stress: ['adversarial-review', 'preflight'],
62
+ retire: ['deprecation', 'removal', 'consumer-migration'], retired: ['deprecation', 'removal'],
63
+ deprecated: ['deprecation', 'removal'], deprecada: ['deprecation', 'removal'],
64
+ retirar: ['deprecation', 'removal'], retirada: ['deprecation', 'removal', 'consumer-migration'],
65
+ legado: ['deprecation', 'compatibility'], legacy: ['deprecation', 'compatibility'],
66
+ simplifica: ['code-simplification', 'refactoring', 'readability'],
67
+ simplificar: ['code-simplification', 'refactoring', 'readability'],
68
+ simplify: ['code-simplification', 'refactoring', 'readability'],
69
+ simplified: ['code-simplification', 'readability'], clarity: ['readability', 'complexity'],
70
+ enginyos: ['readability', 'complexity'], caracterizacion: ['behavior-preservation'],
71
+ 'caracterización': ['behavior-preservation'], caracteritzacio: ['behavior-preservation'],
72
+ 'caracterització': ['behavior-preservation'], wrappers: ['code-simplification'],
73
+ official: ['official-docs', 'primary-sources'], oficiales: ['official-docs', 'primary-sources'],
74
+ oficial: ['official-docs', 'primary-sources'], documentacion: ['official-docs'],
75
+ 'documentación': ['official-docs'], fonts: ['primary-sources'], fuentes: ['primary-sources'],
76
+ sources: ['primary-sources'], lockfile: ['version-detection'],
77
+ specification: ['primary-sources'], specifications: ['primary-sources'],
78
+ version: ['version-detection'], versions: ['version-detection'], versioned: ['version-detection'],
79
+ 'versión': ['version-detection'], versio: ['version-detection'], 'versió': ['version-detection'],
80
+ cite: ['primary-sources'], cited: ['primary-sources'], citations: ['primary-sources'],
81
+ concepts: ['idea-refinement', 'ideation'], conceptos: ['idea-refinement', 'ideation'],
82
+ conceptes: ['idea-refinement', 'ideation'], premortem: ['idea-refinement', 'ideation'],
46
83
  };
47
84
 
48
85
  const SKILL_ROUTING_TERMS = ['suggest', 'detect', 'install', 'meta'];
@@ -78,6 +115,7 @@ const SKILL_ROUTING_VERBS = new Set([
78
115
  ]);
79
116
 
80
117
  const MIN_USEFUL_SCORE = 2;
118
+ const LEXICAL_WEIGHT = 100;
81
119
  const LOW_SIGNAL_TAG_TERMS = new Set(['web', 'frontend']);
82
120
 
83
121
  function fold(term) {
@@ -101,7 +139,10 @@ function expandedTerms(query) {
101
139
  for (const t of tokens) {
102
140
  if (STOP.has(t)) continue;
103
141
  set.add(t);
104
- for (const syn of SYNONYMS[t] || []) set.add(syn);
142
+ // Do not read inherited Object.prototype values (for example a query
143
+ // containing "constructor"). They are functions, not synonym arrays.
144
+ const synonyms = Object.hasOwn(SYNONYMS, t) ? SYNONYMS[t] : [];
145
+ for (const syn of synonyms) set.add(syn);
105
146
  }
106
147
  if (hasSkillNoun && hasSkillRoutingVerb) {
107
148
  for (const term of SKILL_ROUTING_TERMS) set.add(term);
@@ -109,48 +150,40 @@ function expandedTerms(query) {
109
150
  return [...set];
110
151
  }
111
152
 
153
+ export async function createRanker(manifest) {
154
+ const corpus = buildTextCorpus(manifest.skills);
155
+
156
+ return {
157
+ rank(query) {
158
+ const terms = expandedTerms(query);
159
+ if (!terms.length) return [];
160
+ const exact = scoreRows(manifest.skills, terms);
161
+ const exactById = new Map(exact.map((row) => [row.id, row.score]));
162
+ // Synonyms feed the statistical ranker as well as the exact scorer. This
163
+ // preserves Spanish/Catalan intent mapping while TF-IDF handles lexical
164
+ // variants and description-wide relevance.
165
+ const lexical = rankText(corpus, `${query} ${terms.join(' ')}`);
166
+ const lexicalById = new Map(lexical.map((row) => [row.id, row.score]));
167
+ const maxExact = Math.max(0, ...exact.map((row) => row.score));
168
+ const maxLexical = lexical[0]?.score || 0;
169
+ if (maxExact < MIN_USEFUL_SCORE && maxLexical < 0.08) return [];
170
+
171
+ const ids = new Set([...exactById.keys(), ...lexicalById.keys()]);
172
+ return [...ids].map((id) => ({
173
+ id,
174
+ score: (exactById.get(id) || 0) + (lexicalById.get(id) || 0) * LEXICAL_WEIGHT,
175
+ })).sort(byScore);
176
+ },
177
+ close() {},
178
+ };
179
+ }
180
+
112
181
  export async function rank(manifest, query) {
113
- const terms = expandedTerms(query);
114
- if (!terms.length) return [];
115
- const scored = scoreRows(manifest.skills, terms);
116
- if (!scored.length || Math.max(...scored.map((r) => r.score)) < MIN_USEFUL_SCORE) return [];
117
- const SQL = await initSqlJs();
118
- const db = new SQL.Database();
119
-
120
- const rows = manifest.skills.map((sk) => [
121
- sk.id,
122
- ` ${sk.id} ${sk.description} ${(sk.tags || []).join(' ')} `.toLowerCase(),
123
- ]);
124
-
125
- // Prefer FTS5 for fidelity with ECC; fall back to scored whole-word LIKE if
126
- // the sql.js wasm build was compiled without the FTS5 module.
182
+ const ranker = await createRanker(manifest);
127
183
  try {
128
- db.run('CREATE VIRTUAL TABLE s USING fts5(id, doc);');
129
- const stmt = db.prepare('INSERT INTO s (id, doc) VALUES (?, ?)');
130
- for (const r of rows) stmt.run(r);
131
- stmt.free();
132
- const match = terms.map((t) => `"${t}"*`).join(' OR ');
133
- const res = db.exec('SELECT id FROM s WHERE s MATCH ? ORDER BY rank', [match]);
134
- db.close();
135
- if (!res.length) return [];
136
- const ids = new Set(res[0].values.map(([id]) => id));
137
- return scored.filter((r) => ids.has(r.id)).sort(byScore);
138
- } catch {
139
- db.run('CREATE TABLE s (id TEXT, doc TEXT);');
140
- const stmt = db.prepare('INSERT INTO s (id, doc) VALUES (?, ?)');
141
- for (const r of rows) stmt.run(r);
142
- stmt.free();
143
- // Whole-word match: pad doc with spaces and look for "% term %".
144
- const score = terms.map(() => '(doc LIKE ?)').join(' + ');
145
- const params = terms.map((t) => `% ${t} %`);
146
- const res = db.exec(
147
- `SELECT id, (${score}) AS score FROM s WHERE score > 0 ORDER BY score DESC, id`,
148
- params,
149
- );
150
- db.close();
151
- if (!res.length) return [];
152
- const ids = new Set(res[0].values.map(([id]) => id));
153
- return scored.filter((r) => ids.has(r.id)).sort(byScore);
184
+ return ranker.rank(query);
185
+ } finally {
186
+ ranker.close();
154
187
  }
155
188
  }
156
189
 
@@ -0,0 +1,11 @@
1
+ // Skills every installation path equips, even when the user picks a profile,
2
+ // adds one skill directly, or browses the catalog by hand.
3
+ //
4
+ // `orient` and `suggest` are runtime always-on. `bro` is installed by default
5
+ // but remains conditionally loaded: its description routes human-language asks
6
+ // without paying for its body on unrelated turns.
7
+ export const DEFAULT_SKILL_FLOOR = Object.freeze(['orient', 'suggest', 'bro']);
8
+
9
+ export function withDefaultSkillFloor(skillIds = []) {
10
+ return [...new Set([...DEFAULT_SKILL_FLOOR, ...skillIds])];
11
+ }
@@ -4,8 +4,8 @@
4
4
  // catalog grows deliberately, never silently.
5
5
 
6
6
  export const DOMAINS = [
7
- { title: 'Core & control plane', ids: ['init', 'harness', 'orient', 'suggest', 'author-skill', 'sdd-init'] },
8
- { title: 'Spec-Driven Development', ids: ['sdd', 'constitution', 'specify', 'clarify', 'plan', 'tasks', 'analyze', 'implement', 'verify', 'review', 'ship', 'debug', 'worktrees', 'parallel'] },
7
+ { title: 'Core & control plane', ids: ['init', 'harness', 'orient', 'suggest', 'bro', 'author-skill', 'sdd-init'] },
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'] },
11
11
  { title: 'Legal, privacy & compliance', ids: ['gdpr-privacy', 'terms-conditions', 'compliance', 'data-policy', 'ip-trademark'] },
@@ -22,7 +22,7 @@ export const DOMAINS = [
22
22
  { title: 'Game development', ids: ['godot', 'unity', 'unreal', 'game-design', 'game-storytelling', 'level-design', 'gamedev-shaders', 'gamedev-multiplayer', 'gamedev-physics', 'gamedev-pathing', 'gamedev-shipping'] },
23
23
  { title: 'Databases & data layer', ids: ['postgresdb', 'mysql', 'mongodb', 'redis', 'supabase', 'neon', 'planetscale', 'sqlite-turso', 'prisma-orm', 'drizzle-orm', 'firebase', 'dynamodb', 'vector-db', 'clickhouse-analytics', 'duckdb', 'db-migrations', 'backups'] },
24
24
  { title: 'Ship & operate — platforms', ids: ['vercel', 'netlify', 'cloudflare', 'railway', 'render', 'fly-io', 'coolify', 'hetzner', 'digitalocean', 'aws-essentials', 'gcp-essentials'] },
25
- { title: 'Ship & operate — devops', ids: ['docker', 'github-actions', 'git-workflow', 'domains-dns', 'monitoring', 'email-deliverability', 'scaling', 'deployment'] },
25
+ { title: 'Ship & operate — devops', ids: ['docker', 'github-actions', 'git-workflow', 'domains-dns', 'monitoring', 'email-deliverability', 'scaling', 'deployment', 'deprecation'] },
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: 'Design & content craft', ids: ['design', 'presentations', 'course-storytelling', 'course-builder', 'technical-writing', 'translation-l10n'] },
28
28
  { title: 'Knowledge & meta', ids: ['knowledge-ops', 'codebase-onboarding', 'research-ops', 'decision-records', 'continuous-learning', 'skill-scout', 'context-budget', 'roast-me', 'fable-operator'] },
@@ -1,6 +1,7 @@
1
1
  import { skillById } from './manifest.js';
2
2
 
3
3
  const OUTCOMES = {
4
+ bro: 'Writing that sounds natural and human',
4
5
  suggest: 'An assistant that proposes what you need as you go',
5
6
  harness: 'A tidy workspace: connect and document your company',
6
7
  init: 'Guided project bootstrap',
@@ -0,0 +1,112 @@
1
+ const STOP = new Set([
2
+ 'a', 'an', 'and', 'any', 'are', 'as', 'at', 'be', 'before', 'by', 'for', 'from',
3
+ 'in', 'into', 'is', 'it', 'its', 'my', 'need', 'needs', 'of', 'on', 'or', 'our',
4
+ 'so', 'that', 'the', 'them', 'this', 'to', 'use', 'want', 'we', 'when', 'with',
5
+ 'you', 'your', 'help', 'me', 'i',
6
+ 'un', 'una', 'unos', 'unas', 'con', 'de', 'del', 'la', 'el', 'los', 'las',
7
+ 'para', 'que', 'porque', 'en', 'mi', 'mis', 'quiero', 'hacer', 'como',
8
+ 'amb', 'aquest', 'aquesta', 'els', 'les', 'per', 'vull', 'vols',
9
+ ]);
10
+
11
+ function fold(text) {
12
+ return text.normalize('NFD').replace(/\p{M}/gu, '');
13
+ }
14
+
15
+ function stem(token) {
16
+ // Deliberately light: enough to join "simplify/simplified" and
17
+ // "conflicts/conflict" without pretending to be a language model.
18
+ for (const suffix of ['ally', 'ing', 'ed', 'es', 'al']) {
19
+ if (token.length > suffix.length + 3 && token.endsWith(suffix)) {
20
+ token = token.slice(0, -suffix.length);
21
+ break;
22
+ }
23
+ }
24
+ if (token.length > 3 && token.endsWith('s') && !token.endsWith('ss')) token = token.slice(0, -1);
25
+ if (token.length > 4 && token.endsWith('e')) token = token.slice(0, -1);
26
+ if (
27
+ token.length > 4
28
+ && token[token.length - 1] === token[token.length - 2]
29
+ && !'aeiou'.includes(token[token.length - 1])
30
+ ) token = token.slice(0, -1);
31
+ if (token.length > 3 && token.endsWith('y')) token = `${token.slice(0, -1)}i`;
32
+ return token;
33
+ }
34
+
35
+ export function tokenize(text) {
36
+ return fold(text.toLowerCase())
37
+ .replace(/[^\p{L}\p{N}\s-]/gu, ' ')
38
+ .split(/[\s-]+/)
39
+ .filter((token) => token.length > 2 && !STOP.has(token))
40
+ .map(stem);
41
+ }
42
+
43
+ function frequencies(tokens) {
44
+ const result = new Map();
45
+ for (const token of tokens) result.set(token, (result.get(token) || 0) + 1);
46
+ return result;
47
+ }
48
+
49
+ function vector(frequency, idf) {
50
+ const result = new Map();
51
+ for (const [term, count] of frequency) result.set(term, count * idf(term));
52
+ return result;
53
+ }
54
+
55
+ export function cosine(left, right) {
56
+ let dot = 0;
57
+ let leftNorm = 0;
58
+ let rightNorm = 0;
59
+ for (const [term, weight] of left) {
60
+ leftNorm += weight * weight;
61
+ const other = right.get(term);
62
+ if (other) dot += weight * other;
63
+ }
64
+ for (const weight of right.values()) rightNorm += weight * weight;
65
+ if (!leftNorm || !rightNorm) return 0;
66
+ return dot / (Math.sqrt(leftNorm) * Math.sqrt(rightNorm));
67
+ }
68
+
69
+ export function buildTextCorpus(skills) {
70
+ const frequenciesById = new Map();
71
+ for (const skill of skills) {
72
+ const idTokens = tokenize(skill.id.replace(/-/g, ' '));
73
+ const tagTokens = tokenize((skill.tags || []).join(' '));
74
+ frequenciesById.set(skill.id, frequencies([
75
+ ...idTokens, ...idTokens, ...idTokens,
76
+ ...tagTokens, ...tagTokens,
77
+ ...tokenize(skill.description),
78
+ ]));
79
+ }
80
+
81
+ const documentFrequency = new Map();
82
+ for (const terms of frequenciesById.values()) {
83
+ for (const term of terms.keys()) {
84
+ documentFrequency.set(term, (documentFrequency.get(term) || 0) + 1);
85
+ }
86
+ }
87
+ const size = frequenciesById.size;
88
+ const idf = (term) => Math.log(1 + size / (1 + (documentFrequency.get(term) || 0)));
89
+ const vectors = new Map();
90
+ for (const [id, terms] of frequenciesById) vectors.set(id, vector(terms, idf));
91
+ return { idf, vectors };
92
+ }
93
+
94
+ export function rankText(corpus, prompt) {
95
+ const query = vector(frequencies(tokenize(prompt)), corpus.idf);
96
+ return [...corpus.vectors]
97
+ .map(([id, skillVector]) => ({ id, score: cosine(query, skillVector) }))
98
+ .filter((row) => row.score > 0)
99
+ .sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
100
+ }
101
+
102
+ export function descriptionCollisions(corpus, minimum = 0.5) {
103
+ const rows = [...corpus.vectors];
104
+ const collisions = [];
105
+ for (let left = 0; left < rows.length; left += 1) {
106
+ for (let right = left + 1; right < rows.length; right += 1) {
107
+ const score = cosine(rows[left][1], rows[right][1]);
108
+ if (score >= minimum) collisions.push({ left: rows[left][0], right: rows[right][0], score });
109
+ }
110
+ }
111
+ return collisions.sort((a, b) => b.score - a.score || a.left.localeCompare(b.left));
112
+ }
package/scripts/lib/ui.js CHANGED
@@ -92,11 +92,11 @@ function frame(phase, cols, sparks, white) {
92
92
  }
93
93
 
94
94
  // Animated ASCII wordmark — the exaggerated "WOW". Static plain text when not a TTY.
95
- export async function banner() {
95
+ export async function banner(skillCount = 'all') {
96
96
  if (!stdout.isTTY) {
97
97
  say('');
98
98
  for (const l of ART) say(l);
99
- say(' 231 skills · one CLI · zero bloat');
99
+ say(` ${skillCount} skills · one CLI · zero bloat`);
100
100
  return;
101
101
  }
102
102
  const rows = ART.length;
@@ -133,7 +133,7 @@ export async function banner() {
133
133
  // settle on a final rainbow snapshot
134
134
  stdout.write(`\x1b[${rows}A`);
135
135
  stdout.write(frame(15, W));
136
- say(C.dim(' 231 skills · one CLI · zero bloat'));
136
+ say(C.dim(` ${skillCount} skills · one CLI · zero bloat`));
137
137
  stdout.write('\x1b[?25h'); // show cursor
138
138
  }
139
139