@ericrisco/rsc 1.0.14 → 1.0.16

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
@@ -1,19 +1,15 @@
1
1
  <div align="center">
2
2
 
3
- ```
4
- ████████ ███████ ███████
5
- ██ ██ ██ ██ ██
6
- ██ ██ ██ ██
7
- ██ ██ ██ ██
8
- ████████ ██████ ██
9
- ██ ██ ██ ██
10
- ██ ██ ██ ██
11
- ██ ██ ██ ██ ██
12
- ██ ██ ███████ ███████
13
- ```
3
+ <img src="https://raw.githubusercontent.com/ericrisco/rsc-harness/main/site/og.png" alt="rsc — 264 agent skills, one CLI, zero bloat" width="820">
14
4
 
15
5
  # `rsc` — 264 agent skills, one CLI, zero bloat
16
6
 
7
+ [![npm](https://img.shields.io/npm/v/@ericrisco/rsc?color=63d68a&labelColor=12161c&label=npm)](https://www.npmjs.com/package/@ericrisco/rsc)
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)
10
+ [![license](https://img.shields.io/badge/license-MIT-63d68a?labelColor=12161c)](LICENSE)
11
+ [![stars](https://img.shields.io/github/stars/ericrisco/rsc-harness?color=63d68a&labelColor=12161c)](https://github.com/ericrisco/rsc-harness/stargazers)
12
+
17
13
  **A self-recommending skill catalog for 17 coding assistants** — Claude Code,
18
14
  Codex, GitHub Copilot, Cursor, Gemini, Windsurf, Cline, Antigravity, Zed and more.
19
15
  Describe what you want in plain language. It reads your repo, installs *only* the
package/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.14",
2
+ "version": "1.0.16",
3
3
  "counts": {
4
4
  "skills": 264
5
5
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ericrisco/rsc",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "description": "Eric Risco's agent-skills catalog as a granular, self-recommending CLI installer.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,13 +29,89 @@ export const ROLE_MARKERS = {
29
29
  loader: 'return its full text as',
30
30
  };
31
31
 
32
- // Write verbs that indicate a transcript actually WROTE a path rather than merely naming it. A
33
- // mention is not a mutation: the grader quotes repo paths constantly and is not violating anything.
34
- const WRITE_SIGNALS = [
35
- 'Write', 'Edit', 'MultiEdit', 'NotebookEdit',
36
- 'cat >', 'tee ', '>>', 'mv ', 'cp ', 'rm ',
32
+ // Tools that write. Checked against a tool call's actual target path, never against nearby text.
33
+ const WRITE_TOOLS = new Set(['Write', 'Edit', 'MultiEdit', 'NotebookEdit']);
34
+
35
+ // Shell constructs that write TO a path. `cat 02-DOCS/x` is a read and must not match; `cat > 02-DOCS/x`
36
+ // must. Each pattern is anchored on the redirect/verb so the protected path has to be its TARGET.
37
+ const SHELL_WRITE_TO = (p) => [
38
+ new RegExp(`>>?\\s*\\S*${p}`), // > path, >> path
39
+ new RegExp(`\\btee\\s+(-\\S+\\s+)*\\S*${p}`),
40
+ new RegExp(`\\b(mv|cp|rsync)\\s+[^|;&]*\\s\\S*${p}`),
41
+ new RegExp(`\\brm\\s+(-\\S+\\s+)*\\S*${p}`),
42
+ new RegExp(`\\b(mkdir|touch)\\s+(-\\S+\\s+)*\\S*${p}`),
37
43
  ];
38
44
 
45
+ /**
46
+ * Structurally extract the tool calls from a transcript. This is the fix for the defect the first
47
+ * real run exposed: the previous version looked for a write VERB within ~400 chars of a protected
48
+ * path in the raw text. In a treatment transcript the injected SKILL.md itself names
49
+ * 02-DOCS/wiki/... paths, and tool names appear in every JSON envelope, so the proximity window
50
+ * matched almost always. It blocked a run in which nothing had been written — verified against the
51
+ * filesystem, which held no new files at all. An over-blocking gate is still a broken gate: it would
52
+ * have made every treatment run BLOCKED and wedged skill-harden permanently.
53
+ */
54
+ export function extractToolCalls(transcriptText) {
55
+ const calls = [];
56
+ for (const line of String(transcriptText || '').split('\n')) {
57
+ if (!line.trim()) continue;
58
+ let o;
59
+ try { o = JSON.parse(line); } catch { continue; }
60
+ const msg = o && typeof o.message === 'object' && o.message ? o.message : o;
61
+ const content = msg && msg.content;
62
+ if (!Array.isArray(content)) continue;
63
+ for (const b of content) {
64
+ if (b && b.type === 'tool_use') calls.push({ name: b.name, input: b.input || {} });
65
+ }
66
+ }
67
+ return calls;
68
+ }
69
+
70
+ // The per-arm sandbox. A write under here is the CORRECT behaviour, however its path reads.
71
+ export const SANDBOX_MARKER = '.rsc/eval-sandbox/';
72
+
73
+ /**
74
+ * Did this tool call write to a path under `protected`?
75
+ *
76
+ * Two false positives were fixed here, both the same mistake — "the path appears in the string" is
77
+ * not "the write targets that location":
78
+ * 1. proximity matching in raw transcript text (the injected SKILL.md names 02-DOCS paths);
79
+ * 2. substring matching against a sandbox path that MIRRORS the wiki layout inside itself, e.g.
80
+ * .rsc/eval-sandbox/specify/treatment-1/02-DOCS/wiki/sdd/specs/x.md — a correct write that
81
+ * contains "02-DOCS/wiki/" as a substring.
82
+ * Both were caught only by checking the filesystem, which held no new files either time. The lesson
83
+ * is in the shape of the check, not in the patterns: anchor on the sandbox first, always.
84
+ */
85
+ export function callWritesTo(call, protectedPath) {
86
+ if (!call) return false;
87
+ if (WRITE_TOOLS.has(call.name)) {
88
+ const target = String(call.input.file_path || call.input.path || call.input.notebook_path || '');
89
+ if (target.includes(SANDBOX_MARKER)) return false;
90
+ return target.includes(protectedPath);
91
+ }
92
+ if (call.name === 'Bash') {
93
+ const cmd = String(call.input.command || '');
94
+ if (!cmd.includes(protectedPath)) return false;
95
+ const esc = protectedPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
96
+ const sandboxEsc = SANDBOX_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
97
+ return SHELL_WRITE_TO(esc).some((re) => {
98
+ const m = re.exec(cmd);
99
+ if (!m) return false;
100
+ // The matched write target itself must not sit inside the sandbox. Checking the whole command
101
+ // would let one sandboxed write excuse a real one in the same line.
102
+ return !new RegExp(`${sandboxEsc}[^\\s'"]*${esc}`).test(m[0]);
103
+ });
104
+ }
105
+ return false;
106
+ }
107
+
108
+ /** Did this tool call READ the given path? A path inside a tool input, not prose mentioning it. */
109
+ export function callReads(call, path) {
110
+ if (!call) return false;
111
+ const input = JSON.stringify(call.input || {});
112
+ return input.includes(path);
113
+ }
114
+
39
115
  // Paths an eval arm may never write. 02-DOCS is the project's brain and is untracked (P9), so a
40
116
  // stray write there has no undo.
41
117
  export const PROTECTED_PATHS = ['02-DOCS/wiki/'];
@@ -73,35 +149,29 @@ export function findViolations({ skillId, agents }) {
73
149
  const rubricPath = `skills/${skillId}/evals/cases.yaml`;
74
150
 
75
151
  for (const a of agents || []) {
76
- const text = String(a.text || '');
152
+ // Judged on TOOL CALLS, not on text. A transcript naming a path proves nothing: the treatment's
153
+ // own injected skill body names 02-DOCS paths, and the grader quotes repo paths constantly.
154
+ const calls = a.calls || extractToolCalls(a.text);
155
+
77
156
  if (a.role === 'baseline') {
78
- // Graded separately: reading the skill removes the control; reading the rubric is reading the
79
- // answer key, which also inflates the baseline's own score.
80
- const skillHits = countOf(text, skillPath);
157
+ // Only the control arm is judged on reads — the treatment is supposed to have the skill and
158
+ // the loader reads both files by design.
159
+ const skillHits = calls.filter((c) => callReads(c, skillPath)).length;
81
160
  if (skillHits > 0) {
82
161
  violations.push({ kind: 'baseline-read-skill', agent: a.name, pattern: skillPath, count: skillHits });
83
162
  }
84
- const rubricHits = countOf(text, rubricPath);
163
+ // Reading the rubric is reading the answer key: it also inflates the baseline's own score.
164
+ const rubricHits = calls.filter((c) => callReads(c, rubricPath)).length;
85
165
  if (rubricHits > 0) {
86
166
  violations.push({ kind: 'baseline-read-rubric', agent: a.name, pattern: rubricPath, count: rubricHits });
87
167
  }
88
168
  }
169
+
89
170
  if (a.role === 'baseline' || a.role === 'treatment') {
90
171
  for (const p of PROTECTED_PATHS) {
91
- if (!text.includes(p)) continue;
92
- // Require a write verb near the path, or a mention becomes a violation and the grader's
93
- // quotations would block every run.
94
- const wrote = WRITE_SIGNALS.some((verb) => {
95
- let i = text.indexOf(p);
96
- while (i !== -1) {
97
- const window = text.slice(Math.max(0, i - 400), i + 400);
98
- if (window.includes(verb)) return true;
99
- i = text.indexOf(p, i + p.length);
100
- }
101
- return false;
102
- });
103
- if (wrote) {
104
- violations.push({ kind: 'wrote-protected-path', agent: a.name, pattern: p, count: countOf(text, p) });
172
+ const hits = calls.filter((c) => callWritesTo(c, p)).length;
173
+ if (hits > 0) {
174
+ violations.push({ kind: 'wrote-protected-path', agent: a.name, pattern: p, count: hits });
105
175
  }
106
176
  }
107
177
  }
@@ -118,7 +188,7 @@ export function readAgents(dir) {
118
188
  .filter((f) => /^agent-.*\.jsonl$/.test(f))
119
189
  .map((f) => {
120
190
  const text = readFileSync(join(dir, f), 'utf8');
121
- return { name: f, role: classifyAgent(text), text };
191
+ return { name: f, role: classifyAgent(text), text, calls: extractToolCalls(text) };
122
192
  });
123
193
  }
124
194