@ericrisco/rsc 1.1.2 → 1.1.4

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
@@ -6,7 +6,7 @@
6
6
 
7
7
  [![npm](https://img.shields.io/npm/v/@ericrisco/rsc?color=63d68a&labelColor=12161c&label=npm)](https://www.npmjs.com/package/@ericrisco/rsc)
8
8
  [![downloads](https://img.shields.io/npm/dm/@ericrisco/rsc?color=63d68a&labelColor=12161c&label=downloads)](https://www.npmjs.com/package/@ericrisco/rsc)
9
- [![skills](https://img.shields.io/badge/skills-264-63d68a?labelColor=12161c)](#the-catalog)
9
+ [![skills](https://img.shields.io/badge/skills-281-63d68a?labelColor=12161c)](#the-catalog)
10
10
  [![license](https://img.shields.io/badge/license-MIT-63d68a?labelColor=12161c)](LICENSE)
11
11
  [![stars](https://img.shields.io/github/stars/ericrisco/rsc-harness?color=63d68a&labelColor=12161c)](https://github.com/ericrisco/rsc-harness/stargazers)
12
12
 
@@ -17,7 +17,7 @@ skills that fit — one at a time — into every assistant you pick, and keeps t
17
17
  equipped as you work.
18
18
 
19
19
  From *"document my company"* to *"ship a FastAPI service"* to *"grow my YouTube
20
- channel"* — **264 skills across 21 domains**, every one researched against live
20
+ channel"* — **281 skills across 23 domains**, every one researched against live
21
21
  2025-2026 sources and **adversarially scored ≥ 8.5/10** before it shipped.
22
22
 
23
23
  ```bash
@@ -26,7 +26,7 @@ npx @ericrisco/rsc # plain-language wizard — no jargon, installs wh
26
26
 
27
27
  <br>
28
28
 
29
- <img src="https://raw.githubusercontent.com/ericrisco/rsc-harness/main/site/meta-harness.png" alt="rsc — the model (Claude, GPT, Gemini) is the brain your coding agent rents, and you already have that. Claude Code, Codex and opencode sit in the middle. Below them the rsc meta-harness adds everything the model lacks: memory (02-DOCS/) so it stops inventing, arms (01-TOOLS/) so it can execute, a trade (264 skills) so it stops guessing, and reflexes (deterministic hooks) so it cannot forget the rule mid-session." width="960">
29
+ <img src="https://raw.githubusercontent.com/ericrisco/rsc-harness/main/site/meta-harness.png" alt="rsc — the model (Claude, GPT, Gemini) is the brain your coding agent rents, and you already have that. Claude Code, Codex and opencode sit in the middle. Below them the rsc meta-harness adds everything the model lacks: memory (02-DOCS/) so it stops inventing, arms (01-TOOLS/) so it can execute, a trade (281 skills) so it stops guessing, and reflexes (deterministic hooks) so it cannot forget the rule mid-session." width="960">
30
30
 
31
31
  **The model is the only part your agent already has.** `rsc` is the meta-harness that adds
32
32
  the rest: a **memory** so it stops inventing, **arms** so it can execute, a **trade** so it stops
@@ -139,7 +139,7 @@ $ rsc
139
139
  ██████╗ ███████╗ ██████╗ ← animated gradient wordmark
140
140
  ██╔══██╗██╔════╝██╔════╝
141
141
  ██████╔╝███████╗██║
142
- 264 skills · one CLI · zero bloat
142
+ 281 skills · one CLI · zero bloat
143
143
 
144
144
  What do you want to do? ↑↓ move · enter select
145
145
  ❯ Base install — the essentials (8 skills)
@@ -187,7 +187,7 @@ 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
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
- rsc install --profile full # everything (all 264)
190
+ rsc install --profile full # everything (all 281 skills)
191
191
  rsc install --profile full --without go
192
192
  rsc consult "I want to launch a SaaS" # recommend only, no install
193
193
  rsc registry refresh # write .rsc/skill-registry.{json,md}
@@ -253,7 +253,7 @@ just asks in plain language.
253
253
 
254
254
  ## The catalog
255
255
 
256
- 264 skills, grouped by what you're trying to do. Click any skill to read its
256
+ 281 skills, grouped by what you're trying to do. Click any skill to read its
257
257
  `SKILL.md`. It fires on its own when a task matches.
258
258
 
259
259
  ### 🧭 Core & control plane
package/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1.2",
2
+ "version": "1.1.4",
3
3
  "counts": {
4
4
  "skills": 281
5
5
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ericrisco/rsc",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "Eric Risco's agent-skills catalog as a granular, self-recommending CLI installer.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,7 +9,7 @@
9
9
  //
10
10
  // A lift measured against a control that read the answer key is a lower bound of unknown size.
11
11
  // Publishing it as a number is the claim-without-evidence this repo keeps paying for (P2), and it
12
- // feeds skill-harden's --holdout, which decides what enters a 264-skill catalog.
12
+ // feeds skill-harden's --holdout, which decides what enters a 281-skill catalog.
13
13
  //
14
14
  // This module answers the question deterministically, from the transcripts themselves — never from
15
15
  // what an agent says about its own behaviour, which is the way this fails open.
@@ -34,6 +34,10 @@ const UNCHECKED = [
34
34
  'que ninguna sección contenga una suposición sin marcar (no es detectable por máquina; es el juicio que la fase existe para ejercer)',
35
35
  'que el contenido de cada sección sea correcto, y no sólo presente',
36
36
  'que los criterios de aceptación sean de verdad binarios',
37
+ // Added with the status check below, for the same reason the three above exist: a green that is
38
+ // narrower than the rule it appears to enforce is the decorative gate all over again.
39
+ 'si una spec dice "implementada" sin nombrar commit, PR ni versión (no hay nada que ir a mirar)',
40
+ 'si el trabajo de una spec que se declara sin aterrizar llegó por otra vía (git no lo distingue)',
37
41
  ];
38
42
 
39
43
  function fold(s) {
@@ -120,3 +124,97 @@ export function specCompleteness(markdown) {
120
124
  unchecked: UNCHECKED,
121
125
  };
122
126
  }
127
+
128
+ // ─────────────────────────────────────────────────────────────────────────────
129
+ // Does the status still match the repository?
130
+ //
131
+ // The section check above asks whether a spec is COMPLETE. This asks whether it is still TRUE — and
132
+ // nothing asked that before, which is how seven of twenty-five statuses came to contradict the repo:
133
+ // one claiming "sin push ni PR" over work that had been in main for weeks, four sitting in
134
+ // `awaiting-approval` over work already published. Spec: 02-DOCS/wiki/sdd/specs/spec-status-drift.md
135
+ //
136
+ // The vocabulary below was extracted from the 25 real statuses, not imagined. That matters: an
137
+ // invented recognizer reaches for the bare word `implementada`, and the bare word is not checkable —
138
+ // it names nothing to go and look for. Only a commit, a PR or a released version do.
139
+
140
+ /** The claim shapes this recognizer knows. Anything else is silence, not a finding. */
141
+ export const CLAIM_KINDS = ['sha', 'pr', 'version', 'not-landed'];
142
+
143
+ // Deliberately absent: the bare words `implementada`, `desplegada`, `descartada`, and the phrase
144
+ // `no aprobada`. The first two name nothing to check. The last two look like landing claims and are
145
+ // not — `descartada` is a spec closed on purpose after being measured, `no aprobada` describes HOW
146
+ // approval happened (in autopilot, not item by item), not whether code shipped. Treating either as a
147
+ // landing claim would invent drift in the two best-closed specs of the set.
148
+ const SHA = /\b[0-9a-f]{7,40}\b/g;
149
+ const PR = /\bPR\s*#(\d+)/gi;
150
+ const VERSION = /\bpublicada en (\d+\.\d+\.\d+)/gi;
151
+ const NOT_LANDED = /\bsin push\b|\bsin PR\b|\bawaiting-approval\b/i;
152
+
153
+ /**
154
+ * Pull the checkable claims out of a status string. Pure: no git, no I/O.
155
+ * @param {string} statusText
156
+ * @returns {Array<{kind: string, value: string, raw: string}>}
157
+ */
158
+ export function statusClaims(statusText) {
159
+ const text = typeof statusText === 'string' ? statusText : '';
160
+ const claims = [];
161
+ for (const m of text.matchAll(PR)) claims.push({ kind: 'pr', value: m[1], raw: m[0] });
162
+ for (const m of text.matchAll(VERSION)) claims.push({ kind: 'version', value: m[1], raw: m[0] });
163
+ for (const m of text.matchAll(SHA)) claims.push({ kind: 'sha', value: m[0], raw: m[0] });
164
+ const neg = NOT_LANDED.exec(text);
165
+ if (neg) claims.push({ kind: 'not-landed', value: neg[0], raw: neg[0] });
166
+ return claims;
167
+ }
168
+
169
+ /**
170
+ * Judge each claim against the repository.
171
+ *
172
+ * Four verdicts and none of them collapse into another. `unverifiable` is the one that keeps this
173
+ * honest — a claim we could not check is not a claim that passed, and it is not drift either.
174
+ *
175
+ * `not-landed` is always handed to a human, and that is a deliberate retreat from what the spec
176
+ * first promised. No query distinguishes "never landed" from "landed by another route": the case
177
+ * that started this work names a commit that genuinely is not in main, while its deliverable has
178
+ * been there for weeks. Checking the named commit would bless the misleading status; inferring the
179
+ * other from anything else would be a heuristic inside a gate. So the deterministic part narrows,
180
+ * and only what it narrowed goes to judgement — principle 1, and the shape `knowledge-drift` already
181
+ * uses.
182
+ *
183
+ * @param {Array<object>} claims from `statusClaims`
184
+ * @param {?{hasCommit: Function, isAncestor: Function, hasPr: Function, hasTag: Function}} probe
185
+ * null when there is no repository to ask — every claim then reports unverifiable.
186
+ * @returns {Array<{kind: string, value: string, verdict: string, reason: string}>}
187
+ */
188
+ export function checkClaims(claims, probe) {
189
+ return (claims || []).map((c) => {
190
+ if (c.kind === 'not-landed') {
191
+ return {
192
+ ...c,
193
+ verdict: 'needs-human',
194
+ reason: 'git cannot tell "never landed" from "landed by another route" — look at whether the deliverable is in main, not at the branch',
195
+ };
196
+ }
197
+ if (!probe) return { ...c, verdict: 'unverifiable', reason: 'no repository to ask' };
198
+ try {
199
+ if (c.kind === 'sha') {
200
+ if (!probe.hasCommit(c.value)) {
201
+ return { ...c, verdict: 'unverifiable', reason: `commit ${c.value} is not in this repository` };
202
+ }
203
+ return probe.isAncestor(c.value)
204
+ ? { ...c, verdict: 'holds', reason: `${c.value} is in main` }
205
+ : { ...c, verdict: 'stale', reason: `${c.value} is NOT in main` };
206
+ }
207
+ if (c.kind === 'pr') {
208
+ return probe.hasPr(c.value)
209
+ ? { ...c, verdict: 'holds', reason: `PR #${c.value} is merged into main` }
210
+ : { ...c, verdict: 'stale', reason: `no merge of PR #${c.value} in main` };
211
+ }
212
+ return probe.hasTag(c.value)
213
+ ? { ...c, verdict: 'holds', reason: `v${c.value} is tagged` }
214
+ : { ...c, verdict: 'stale', reason: `no v${c.value} tag in this repository` };
215
+ } catch (e) {
216
+ // Could not look. Saying `stale` here would invent a finding out of our own failure.
217
+ return { ...c, verdict: 'unverifiable', reason: e.message };
218
+ }
219
+ });
220
+ }
@@ -7,7 +7,8 @@
7
7
  import { readFileSync, readdirSync } from 'node:fs';
8
8
  import { join, dirname, basename } from 'node:path';
9
9
  import { fileURLToPath } from 'node:url';
10
- import { specCompleteness } from './lib/spec-gate.js';
10
+ import { execFileSync } from 'node:child_process';
11
+ import { specCompleteness, statusClaims, checkClaims } from './lib/spec-gate.js';
11
12
 
12
13
  const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
13
14
  const DEFAULT_DIR = join(ROOT, '02-DOCS', 'wiki', 'sdd', 'specs');
@@ -22,6 +23,35 @@ function defaultTargets() {
22
23
  }
23
24
  }
24
25
 
26
+ // The repository, asked instead of assumed. Returns null when there is nothing to ask — a
27
+ // report-only instrument that crashes where it cannot look is worse than one that says so.
28
+ function gitProbe(root) {
29
+ const git = (args) => execFileSync('git', args, { cwd: root, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
30
+ let main;
31
+ try {
32
+ git(['rev-parse', '--git-dir']);
33
+ // Prefer the remote tip — a stale local `main` reports merged work as missing. But CI checks out
34
+ // detached with no `origin/main` and no local `main`, and falling through to "no repository to
35
+ // ask" there means the check silently stops checking exactly where it runs unattended. HEAD is
36
+ // the honest last resort: on CI it IS the branch under test.
37
+ main = ['origin/main', 'main', 'HEAD'].find((ref) => {
38
+ try { git(['rev-parse', '--verify', ref]); return true; } catch { return false; }
39
+ });
40
+ if (!main) throw new Error('no ref to compare against');
41
+ } catch {
42
+ return null;
43
+ }
44
+ return {
45
+ hasCommit: (sha) => { try { git(['cat-file', '-e', `${sha}^{commit}`]); return true; } catch { return false; } },
46
+ isAncestor: (sha) => { try { git(['merge-base', '--is-ancestor', sha, main]); return true; } catch { return false; } },
47
+ // The squash-merge subject carries `(#N)`; --fixed-strings so the parens are not a regex.
48
+ hasPr: (n) => { try { return git(['log', main, '--grep', `(#${n})`, '--fixed-strings', '-1', '--format=%H']) !== ''; } catch { return false; } },
49
+ hasTag: (v) => { try { return git(['tag', '-l', `v${v}`]) !== ''; } catch { return false; } },
50
+ };
51
+ }
52
+
53
+ const statusOf = (text) => (/^status:[ \t]*(.*)$/m.exec(text) || [, ''])[1];
54
+
25
55
  function main() {
26
56
  const args = process.argv.slice(2);
27
57
  const targets = args.length ? args : defaultTargets();
@@ -31,8 +61,21 @@ function main() {
31
61
  }
32
62
 
33
63
  let failed = 0;
64
+ let drifted = 0;
65
+ // One probe per repository, not one for the tool's own. A spec's claims are about the history of
66
+ // the project the spec belongs to — checking them against wherever this script happens to live
67
+ // gives the right answer only by coincidence, and the wrong one the moment the gate is pointed at
68
+ // another checkout.
69
+ const probes = new Map();
70
+ const probeFor = (path) => {
71
+ const dir = dirname(path);
72
+ if (!probes.has(dir)) probes.set(dir, gitProbe(dir));
73
+ return probes.get(dir);
74
+ };
75
+ let anyProbe = false;
34
76
  for (const path of targets) {
35
- const r = specCompleteness(readFileSync(path, 'utf8'));
77
+ const text = readFileSync(path, 'utf8');
78
+ const r = specCompleteness(text);
36
79
  const name = basename(path);
37
80
  if (r.ok) {
38
81
  const typed = r.openPoints.length - r.untyped.length;
@@ -40,17 +83,27 @@ function main() {
40
83
  if (r.untyped.length) {
41
84
  console.log(` untyped (treated as open questions): ${r.untyped.length}`);
42
85
  }
43
- } else {
86
+ }
87
+ if (!r.ok) {
44
88
  failed += 1;
45
89
  console.log(`FAIL ${name}`);
46
90
  if (r.missing.length) console.log(` missing section(s): ${r.missing.join(', ')}`);
47
91
  if (r.empty.length) console.log(` section(s) with no content: ${r.empty.join(', ')}`);
48
92
  }
93
+ // A status that still matches the repository prints nothing: the report grows only where there
94
+ // is something to act on (P5).
95
+ const probe = probeFor(path);
96
+ anyProbe = anyProbe || Boolean(probe);
97
+ for (const v of checkClaims(statusClaims(statusOf(text)), probe)) {
98
+ if (v.verdict === 'holds') continue;
99
+ if (v.verdict === 'stale') drifted += 1;
100
+ console.log(` status ${v.verdict.toUpperCase()}: ${v.raw} — ${v.reason}`);
101
+ }
49
102
  }
50
103
 
51
104
  // A green here is narrower than the rule it enforces, so it says so every run rather than
52
105
  // letting the reader assume the gate covered more than it did.
53
- console.log(`\n${targets.length - failed}/${targets.length} pass. Not checked by this gate:`);
106
+ console.log(`\n${targets.length - failed}/${targets.length} pass, ${drifted} status claim(s) contradicted by the repo${anyProbe ? '' : ' (no repository to ask)'}. Not checked by this gate:`);
54
107
  for (const u of specCompleteness('').unchecked) console.log(` - ${u}`);
55
108
  if (failed) process.exit(1);
56
109
  }