@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 +6 -6
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/lib/eval-integrity.js +1 -1
- package/scripts/lib/spec-gate.js +98 -0
- package/scripts/spec-gate.js +57 -4
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/@ericrisco/rsc)
|
|
8
8
|
[](https://www.npmjs.com/package/@ericrisco/rsc)
|
|
9
|
-
[](#the-catalog)
|
|
10
10
|
[](LICENSE)
|
|
11
11
|
[](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"* — **
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
package/package.json
CHANGED
|
@@ -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
|
|
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.
|
package/scripts/lib/spec-gate.js
CHANGED
|
@@ -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
|
+
}
|
package/scripts/spec-gate.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
}
|
|
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
|
}
|