@ericrisco/rsc 1.0.5 → 1.0.7

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.7",
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.7",
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.**
@@ -101,6 +101,28 @@ Render it at the dial's verbosity. Do not log a decision to `decisions.md` — a
101
101
  | Siding with the plan when it contradicts the constitution | The constitution wins by definition — flag CRITICAL. If the principle itself is wrong, that is a `constitution` change the user makes, not a quiet override here. |
102
102
  | Guessing what a vague requirement meant | Guessing defeats the gate. Mark AMBIGUOUS and route to `clarify`; do not encode your guess. |
103
103
 
104
+ ## Result envelope
105
+
106
+ End with the parseable block every SDD phase shares, so the dispatcher can chain without
107
+ interpreting prose (contract: `../sdd/SKILL.md`):
108
+
109
+ ```json result-envelope
110
+ {
111
+ "status": "complete|blocked|failed",
112
+ "executive_summary": "Cross-read of spec/plan/tasks against the constitution; findings ranked.",
113
+ "artifact": "02-DOCS/wiki/sdd/analysis/<slug>.md",
114
+ "next_recommended": "implement",
115
+ "risk": "low|medium|high",
116
+ "skill_resolution": {
117
+ "used": ["analyze"],
118
+ "missing": [],
119
+ "fallback": [],
120
+ "compact_rules": ["Read adversarially across artifacts, not inside one.", "A finding without a location is an opinion."]
121
+ },
122
+ "evidence": ["report path exists", "blockers listed with artifact + location", "constitution conflicts named"]
123
+ }
124
+ ```
125
+
104
126
  ## Next in the chain
105
127
 
106
128
  On `GATE: PASS` (or once the user consciously accepts the remaining MEDIUM/LOW findings), proceed to **`implement`** — execute the tasks with TDD discipline, delegating concrete test tooling to the relevant stack skill (`fastapi`, `nextjs`, `go`, `flutter`, `postgresdb`). On `GATE: BLOCKED`, route each CRITICAL/HIGH finding to its owning phase (`clarify`, `plan`, `tasks`, or `constitution`), let the user resolve, then re-run `analyze`. The gate only opens once.
@@ -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"
@@ -113,6 +113,28 @@ After baking back, the spec line becomes a bounded, testable behavior with accep
113
113
 
114
114
  The gate is passed when the spec, constitution and profile were all read (settled questions cited, not re-asked); all ten taxonomy categories were considered; only the build-changing gaps were put to the user, as dial-sized decisions with recommendations; every answer is baked into the spec body with observable acceptance criteria, logged under `## Clarifications` and bounded under `## Out of scope`; and the final re-read opened no new gap.
115
115
 
116
+ ## Result envelope
117
+
118
+ End with the parseable block every SDD phase shares, so the dispatcher can chain without
119
+ interpreting prose (contract: `../sdd/SKILL.md`):
120
+
121
+ ```json result-envelope
122
+ {
123
+ "status": "complete|blocked|failed",
124
+ "executive_summary": "Open points resolved; the spec is de-risked and ready to plan against.",
125
+ "artifact": "02-DOCS/wiki/sdd/specs/<slug>.md",
126
+ "next_recommended": "plan",
127
+ "risk": "low|medium|high",
128
+ "skill_resolution": {
129
+ "used": ["clarify"],
130
+ "missing": [],
131
+ "fallback": [],
132
+ "compact_rules": ["Ask only what changes the spec.", "An unanswered question is recorded, never invented."]
133
+ },
134
+ "evidence": ["answers folded back into the spec", "remaining open points listed with their owner"]
135
+ }
136
+ ```
137
+
116
138
  ## Next in the chain
117
139
 
118
140
  Hand off to **`plan`** — turn the now-sharp spec into a technical implementation plan (architecture, interfaces, data flow, testing strategy, risks), deferring stack specifics to the relevant stack skill. The chain continues: clarify → **plan** → tasks → analyze → implement → verify → review → ship. `debug` is callable any time if what you are "clarifying" turns out to be a runtime fault, not a spec gap.
@@ -132,6 +132,28 @@ This skill's `02-DOCS` record is the constitution at `02-DOCS/wiki/sdd/constitut
132
132
 
133
133
  Every later rsc-sdd phase reads this file before it works. The harness maintains and improves the article over the life of the project; this skill is the place that ratifies and amends it.
134
134
 
135
+ ## Result envelope
136
+
137
+ End with the parseable block every SDD phase shares, so the dispatcher can chain without
138
+ interpreting prose (contract: `../sdd/SKILL.md`):
139
+
140
+ ```json result-envelope
141
+ {
142
+ "status": "complete|blocked|failed",
143
+ "executive_summary": "Constitution written with N numbered, testable rules the later phases inherit.",
144
+ "artifact": "02-DOCS/wiki/sdd/constitution.md",
145
+ "next_recommended": "specify",
146
+ "risk": "low|medium|high",
147
+ "skill_resolution": {
148
+ "used": ["constitution"],
149
+ "missing": [],
150
+ "fallback": [],
151
+ "compact_rules": ["Principles are inherited constraints, not choices to re-make.", "Every rule is testable or it is a preference."]
152
+ },
153
+ "evidence": ["constitution path exists", "rules numbered and testable", "decisions log appended"]
154
+ }
155
+ ```
156
+
135
157
  ## Next in the chain
136
158
 
137
159
  The constitution is the guardrail; now describe what to build. Hand off to **`../specify/SKILL.md`** — turn a fuzzy intent into a spec (what & why, no implementation), grounded in these principles. The full chain: **constitution → specify → clarify → plan → tasks → analyze → implement → verify → review → ship** (with `debug`, `worktrees`, `parallel` callable on demand). The dispatcher is `../sdd/SKILL.md`.
@@ -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."
@@ -141,6 +141,28 @@ regardless. (`implement` re-checks this as a hard gate before its first commit.)
141
141
  - Concrete framework/ORM/test-runner mechanics → the stack skill (`../fastapi/SKILL.md`,
142
142
  `../nextjs/SKILL.md`, `../go/SKILL.md`, `../flutter/SKILL.md`, `../postgresdb/SKILL.md`).
143
143
 
144
+ ## Result envelope
145
+
146
+ End with the parseable block every SDD phase shares, so the dispatcher can chain without
147
+ interpreting prose (contract: `../sdd/SKILL.md`):
148
+
149
+ ```json result-envelope
150
+ {
151
+ "status": "complete|blocked|failed",
152
+ "executive_summary": "Technical plan derived from the clarified spec, with the isolation decision made.",
153
+ "artifact": "02-DOCS/wiki/sdd/plans/<slug>.md",
154
+ "next_recommended": "tasks",
155
+ "risk": "low|medium|high",
156
+ "skill_resolution": {
157
+ "used": ["plan"],
158
+ "missing": [],
159
+ "fallback": [],
160
+ "compact_rules": ["The plan answers HOW; the spec owns WHAT.", "Name the isolation choice before the build starts."]
161
+ },
162
+ "evidence": ["plan path exists", "each spec acceptance criterion has an approach", "risks and rollback stated"]
163
+ }
164
+ ```
165
+
144
166
  ## Next in the chain
145
167
 
146
168
  Plan written, indexed, decisions logged → propose isolation (above), then hand off to
@@ -237,6 +237,28 @@ Review is mostly a conversation, but two artifacts persist into the harness wiki
237
237
 
238
238
  Index both in `02-DOCS/wiki/index.md` (the Knowledge map; root `CLAUDE.md` keeps only a short pointer) under the `sdd/` topic — the harness owns that map; this skill just keeps its rows honest.
239
239
 
240
+ ## Result envelope
241
+
242
+ End with the parseable block every SDD phase shares, so the dispatcher can chain without
243
+ interpreting prose (contract: `../sdd/SKILL.md`):
244
+
245
+ ```json result-envelope
246
+ {
247
+ "status": "complete|blocked|failed",
248
+ "executive_summary": "Adversarial review against spec/plan/constitution; verdict and blocking findings.",
249
+ "artifact": "02-DOCS/wiki/sdd/reviews/<slug>.md",
250
+ "next_recommended": "ship",
251
+ "risk": "low|medium|high",
252
+ "skill_resolution": {
253
+ "used": ["review"],
254
+ "missing": [],
255
+ "fallback": [],
256
+ "compact_rules": ["Rank or it is noise: blocker / should-fix / nit.", "A finding without a repro is a question, not a defect."]
257
+ },
258
+ "evidence": ["verdict stated (APPROVE | APPROVE WITH NITS | CHANGES REQUESTED)", "each blocker carries location + repro + fix"]
259
+ }
260
+ ```
261
+
240
262
  ## Next in the chain
241
263
 
242
264
  When the diff carries an **APPROVE** / **APPROVE WITH NITS** verdict and every blocker is resolved, hand off to **ship** — close the branch via PR / merge / cleanup, with **git authorship as Eric, never Claude**. If the review came back **CHANGES REQUESTED**, the loop goes back to **implement** (fix), then **verify** (re-prove green), then back here for re-review. Don't ship a diff that hasn't earned its verdict.