@ericrisco/rsc 1.0.5 → 1.0.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/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.5",
2
+ "version": "1.0.6",
3
3
  "counts": {
4
4
  "skills": 258
5
5
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ericrisco/rsc",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Eric Risco's agent-skills catalog as a granular, self-recommending CLI installer.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -7,6 +7,16 @@ set -euo pipefail
7
7
  # >= 5 should_trigger entries
8
8
  # >= 4 should_not_trigger entries
9
9
  # >= 1 capability scenario
10
+ # every should_not_trigger route_to resolves (see below)
11
+ #
12
+ # route_to is the one that used to be decorative: the rubric listed "route_to names a
13
+ # real skill" as a deterministic gate and nothing checked it, so evals accumulated
14
+ # routes to skills that were never authored. Each value must now be one of:
15
+ # - a real catalog skill id (a directory under skills/ with a SKILL.md)
16
+ # - "none" — deliberately nothing to route to
17
+ # - "external:<name>" — a skill outside this catalog, on purpose. Some skills
18
+ # legitimately defer to tools that ship elsewhere; the prefix makes that a
19
+ # visible, greppable claim instead of a typo indistinguishable from drift.
10
20
  #
11
21
  # Uses python3 for real YAML parsing when available. If python3 is missing,
12
22
  # falls back to a basic structural grep check and SKIPs the deep validation
@@ -36,7 +46,7 @@ warned_skip=0
36
46
  validate_py() {
37
47
  cy="$1"
38
48
  CASES_YAML="$cy" MIN_TRIGGER="$MIN_TRIGGER" MIN_NOT_TRIGGER="$MIN_NOT_TRIGGER" \
39
- MIN_CAPABILITY="$MIN_CAPABILITY" python3 - <<'PYEOF'
49
+ MIN_CAPABILITY="$MIN_CAPABILITY" VALID_IDS="$VALID_IDS" python3 - <<'PYEOF'
40
50
  import os, sys
41
51
  try:
42
52
  import yaml
@@ -72,7 +82,39 @@ st = count("should_trigger")
72
82
  sn = count("should_not_trigger")
73
83
  cap = count("capability")
74
84
 
85
+ # --- route_to resolution -------------------------------------------------------
86
+ # Accepts a real catalog id, "none", or "external:<name>". A value may be a list,
87
+ # or a string holding several targets separated by comma / | / " or ".
88
+ valid = set(filter(None, os.environ.get("VALID_IDS", "").split(",")))
89
+ NOTHING = {"none", "n/a", "-", ""}
90
+ bad_routes = []
91
+
92
+ def targets(value):
93
+ items = value if isinstance(value, list) else [value]
94
+ out = []
95
+ for item in items:
96
+ for part in str(item).replace("|", ",").replace(" or ", ",").split(","):
97
+ out.append(part.strip().strip("`"))
98
+ return out
99
+
100
+ for case in (data.get("should_not_trigger") or []):
101
+ if not isinstance(case, dict) or case.get("route_to") is None:
102
+ continue
103
+ for t in targets(case["route_to"]):
104
+ low = t.lower()
105
+ if low in NOTHING:
106
+ continue
107
+ if low.startswith("external:"):
108
+ if not low[len("external:"):].strip():
109
+ bad_routes.append('"external:" with no name')
110
+ continue
111
+ if valid and t not in valid:
112
+ bad_routes.append(t)
113
+
75
114
  reasons = []
115
+ if bad_routes:
116
+ uniq = sorted(set(bad_routes))
117
+ reasons.append("route_to does not resolve: %s (use a real skill id, \"none\", or \"external:<name>\")" % ", ".join(uniq))
76
118
  if st < 0:
77
119
  reasons.append("should_trigger is not a list")
78
120
  elif st < mt:
@@ -112,8 +154,17 @@ validate_grep() {
112
154
 
113
155
  echo "eval-lint: scanning $SKILLS_DIR"
114
156
  echo "minimums: should_trigger>=$MIN_TRIGGER should_not_trigger>=$MIN_NOT_TRIGGER capability>=$MIN_CAPABILITY"
157
+ echo "route_to must resolve: a catalog skill id, \"none\", or \"external:<name>\""
115
158
  echo
116
159
 
160
+ # The set of real skill ids, built once from disk — the same surface the router sees.
161
+ VALID_IDS=""
162
+ for sp in "$SKILLS_DIR"/*; do
163
+ [ -f "$sp/SKILL.md" ] || continue
164
+ VALID_IDS="$VALID_IDS,$(basename "$sp")"
165
+ done
166
+ VALID_IDS="${VALID_IDS#,}"
167
+
117
168
  # Iterate skills deterministically; bash 3.2-safe (no mapfile).
118
169
  for skill_path in "$SKILLS_DIR"/*; do
119
170
  [ -d "$skill_path" ] || continue
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { rmSync, existsSync, cpSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { rmSync, existsSync, cpSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from 'node:fs';
3
3
  import { join, dirname } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { planInstall } from './install-plan.js';
@@ -112,9 +112,35 @@ export async function applyInstall({ skillIds, target, home, cwd = process.cwd()
112
112
  writeState(paths.stateFile, state);
113
113
  mkdirSync(dirname(versionFile(cwd)), { recursive: true });
114
114
  writeFileSync(versionFile(cwd), CLI_VERSION + '\n');
115
+ ignoreLocalState(cwd);
115
116
  return { ...state, backup };
116
117
  }
117
118
 
119
+ // `.rsc/` holds local machine state — hook scripts, install markers, the sello's
120
+ // seals, the automation-gap log. None of it belongs in a user's repository, and the
121
+ // gap log in particular is prose about what happened in their project: a routine
122
+ // `git add -A` would publish it. Nothing used to keep that out, so add the entry.
123
+ //
124
+ // Additive and idempotent by construction: append one line to an existing
125
+ // .gitignore, never rewrite or reorder it, and never create the file in a directory
126
+ // that is not a git repository.
127
+ export function ignoreLocalState(cwd = process.cwd()) {
128
+ if (!existsSync(join(cwd, '.git'))) return null;
129
+ const gi = join(cwd, '.gitignore');
130
+ let text = '';
131
+ try { text = existsSync(gi) ? readFileSync(gi, 'utf8') : ''; } catch { return null; }
132
+ const already = text.split('\n').some((l) => {
133
+ const s = l.trim().replace(/\/$/, '');
134
+ return s === '.rsc' || s === '/.rsc';
135
+ });
136
+ if (already) return null;
137
+ const prefix = text === '' ? '' : (text.endsWith('\n') ? '' : '\n');
138
+ try {
139
+ appendFileSync(gi, `${prefix}\n# rsc local state (hooks, seals, logs) — machine-local, never committed\n.rsc/\n`);
140
+ return gi;
141
+ } catch { return null; }
142
+ }
143
+
118
144
  export function listInstalled({ target, home, cwd = process.cwd() }) {
119
145
  const paths = targetPaths(target, home, cwd);
120
146
  return Object.keys(readState(paths.stateFile).skills);
@@ -16,7 +16,7 @@ dishonest 9.
16
16
  | 3 | **Body craft & progressive disclosure** | 0.15 | **The smallest body that routes correctly** — ≤400 lines is a ceiling, not a target, and there is no minimum: a skill that does its job in 60 lines scores higher than the same skill padded to 200. Every `references/` file is pointed to inline from this body (an unlinked reference is never loaded, so it is dead weight). rsc voice, every fence language-tagged, a checklist/decision table only where the flow branches, an anti-patterns table present. |
17
17
  | 4 | **Correctness, grounding & freshness** | **0.25** | Every load-bearing claim is accurate and **current (2025–2026)**: real library/API versions, no hallucinated flags/endpoints. The spec cites **authoritative sources with dates**; nothing rests on stale or invented facts. This is the heaviest dimension by design. |
18
18
  | 5 | **Actionability** | 0.15 | Concrete and runnable: real numbers, paths, commands, decision rules — not adjectives. A practitioner could act from it without leaving the skill. |
19
- | 6 | **Evals quality** | 0.10 | ≥5 `should_trigger` (incl. non-obvious), ≥4 `should_not_trigger` each with a `route_to` that names a **real** sibling, ≥1 `capability` with a meaningful `must_include` rubric. The cases are genuine, not filler. |
19
+ | 6 | **Evals quality** | 0.10 | ≥5 `should_trigger` (incl. non-obvious), ≥4 `should_not_trigger` each with a `route_to` that resolves — a **real** catalog id, `none`, or `external:<name>` (enforced by `eval-lint`), ≥1 `capability` with a meaningful `must_include` rubric. The cases are genuine, not filler. |
20
20
  | 7 | **Originality & safety** | 0.10 | rsc voice, not a clone of any external catalog (no borrowed urgency blocks / rationalization tables / `*-reviewer-prompt.md` conventions / verbatim flowcharts). `verify.sh` present iff there is a checkable artifact. No destructive or unsafe advice presented without guardrails. |
21
21
 
22
22
  **Weighted score** = Σ(subscore × weight), rounded to 1 decimal. **Ship gate: ≥ 8.5.**
@@ -17,7 +17,7 @@ Everything lives in `evals/cases.yaml` (the cases) and `evals/README.md` (how to
17
17
 
18
18
  `scripts/eval-lint.sh` parses every `cases.yaml` and fails the build only on the **structural minimums**: that `evals/cases.yaml` exists, that `should_trigger`, `should_not_trigger`, and `capability` are present as lists, and that their item counts meet **≥ 5 / ≥ 4 / ≥ 1**. (Without python3+PyYAML it degrades to a presence-only key check.) Run it before shipping to catch a missing or undersized section.
19
19
 
20
- It does **not** read inside the entries. Whether each `should_not_trigger` carries a `route_to`, whether that `route_to` names a sibling that actually exists in this repo, whether your `should_trigger` set includes a genuinely non-obvious or non-English phrasing, and whether each `capability` scenario has a real `must_include` rubric of gradeable points — none of that is enforced by the linter. Those are **author and review responsibilities**: verify them by hand (and in the self-audit / code-review pass) before shipping. A green eval-lint means the shape is right, not that the cases are good.
20
+ It also resolves every `route_to`: each value must be a real catalog skill id, `none`, or `external:<name>` for a skill that deliberately lives outside this catalog — a stale route now fails the build. What it still does **not** read: whether each `should_not_trigger` actually carries a `route_to`, whether your `should_trigger` set includes a genuinely non-obvious or non-English phrasing, and whether each `capability` scenario has a real `must_include` rubric of gradeable points. Those are **author and review responsibilities**: verify them by hand (and in the self-audit / code-review pass) before shipping. A green eval-lint means the shape is right, not that the cases are good.
21
21
 
22
22
  ## cases.yaml structure
23
23
 
@@ -33,7 +33,7 @@ should_trigger:
33
33
 
34
34
  should_not_trigger:
35
35
  - prompt: "A near-miss that looks close but belongs elsewhere."
36
- route_to: "sibling-id" # a skill that ACTUALLY exists in this repo, or "none"
36
+ route_to: "sibling-id" # a real catalog id, "none", or "external:<name>" — checked by eval-lint
37
37
  why: "Why it is NOT this skill and why the sibling owns it."
38
38
  # … ≥ 4 total
39
39
 
@@ -53,7 +53,7 @@ capability:
53
53
 
54
54
  ## Writing good `should_not_trigger` cases
55
55
 
56
- These are where descriptions get sharpened. Each near-miss should be genuinely tempting — adjacent in topic but owned by a sibling. The `route_to` must name a skill that **exists in this repo**; a `route_to` pointing at a non-existent skill is a defect. Pick the siblings most likely to be confused with this one and write a case that disambiguates each.
56
+ These are where descriptions get sharpened. Each near-miss should be genuinely tempting — adjacent in topic but owned by a sibling. The `route_to` must name a skill that **exists in this repo** — `eval-lint` fails the build otherwise. If the right owner genuinely ships outside this catalog, say so with `external:<name>` rather than inventing an id: the prefix makes the claim visible instead of indistinguishable from drift. Pick the siblings most likely to be confused with this one and write a case that disambiguates each.
57
57
 
58
58
  For `author-skill`, the natural confusables are `specify`/`plan` (building a feature, not a skill), `building-agents` (agent loops), `harness` (generic docs/wiki), and `init` (bootstrapping). Route each near-miss to whichever it truly belongs to.
59
59
 
@@ -24,7 +24,7 @@ should_not_trigger:
24
24
  route_to: "none"
25
25
  why: "One-shot throwaway prompt with no tools/eval/production path — the skill says just call the SDK directly."
26
26
  - prompt: "Tune Anthropic prompt caching and extended thinking in our anthropic-only client.py to cut latency on a file that only imports the anthropic SDK."
27
- route_to: "claude-api"
27
+ route_to: "external:claude-api"
28
28
  why: "Single-vendor Anthropic-SDK internals tuning in a file that only imports anthropic defers to the dedicated Anthropic skill; this skill stays multi-provider."
29
29
  - prompt: "Write the pgvector schema and an HNSW-indexed nearest-neighbour SQL query, plus tuning advice — just the database side, no LLM calls."
30
30
  route_to: "postgresdb"
@@ -22,14 +22,14 @@ should_not_trigger:
22
22
  route_to: "marketing"
23
23
  why: "Sales/landing copy for the course is marketing's domain. The skill teaches; it never bolts a pitch on, and explicitly defers selling words to marketing."
24
24
  - prompt: "Audit module 14 of my course for gaps, redundancy, anachronisms, and outdated stack references, and give me a written report."
25
- route_to: "review-content"
25
+ route_to: "external:review-content"
26
26
  why: "Diagnostic content-audit pass with a written report. The skill says run review-content first and bring findings here; it rebuilds, it does not audit."
27
27
  - prompt: "Design a color system, typography scale, and diagram style for my course slides."
28
28
  route_to: "design"
29
29
  why: "Pure visual system / pixels. The skill defers the visual layer to design; it owns narrative, not aesthetics."
30
30
  - prompt: "Fact-check whether the historical claims in my lesson about the origins of TCP are actually accurate, with sources."
31
- route_to: "deep-research"
32
- why: "Pure fact-checking/research of subject matter, not reframing teaching. The skill forbids inventing proof and defers verification to deep-research."
31
+ route_to: "research-ops"
32
+ why: "Pure fact-checking/research of subject matter, not reframing teaching. The skill forbids inventing proof and defers verification to a research pass — `research-ops` in this catalog, a deep-research tool where the environment provides one."
33
33
 
34
34
  capability:
35
35
  - scenario: "A learner profile already exists and is complete in 02-DOCS/wiki/teaching/. The user gives a flat, definition-only lesson on 'idempotency' for a mid-level backend course and asks to make it land."