@ericrisco/rsc 2.0.20 → 2.2.0
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 +63 -21
- package/manifest.json +18 -85
- package/package.json +5 -1
- package/scripts/consult.js +8 -8
- package/scripts/install-apply.js +48 -11
- package/scripts/lib/default-skill-floor.js +5 -4
- package/scripts/lib/divergence.js +7 -3
- package/scripts/lib/domains.js +2 -2
- package/scripts/lib/onboarding-apply.js +43 -26
- package/scripts/lib/onboarding.js +5 -5
- package/scripts/lib/recommend.js +1 -1
- package/scripts/lib/repair.js +4 -2
- package/scripts/lib/retired-skills.js +20 -0
- package/scripts/rsc.js +25 -19
- package/scripts/skill-behavior-rubric.md +1 -1
- package/scripts/skill-migration-brief.md +3 -3
- package/skills/analyze/SKILL.md +3 -8
- package/skills/analyze/evals/cases.yaml +1 -1
- package/skills/author-skill/SKILL.md +3 -3
- package/skills/author-skill/evals/cases.yaml +1 -1
- package/skills/author-skill/references/rsc-conventions.md +1 -1
- package/skills/clarify/SKILL.md +7 -14
- package/skills/clarify/evals/cases.yaml +2 -2
- package/skills/code-review/SKILL.md +1 -1
- package/skills/constitution/SKILL.md +10 -13
- package/skills/constitution/evals/cases.yaml +3 -3
- package/skills/continuous-learning/SKILL.md +1 -1
- package/skills/continuous-learning/evals/cases.yaml +1 -1
- package/skills/debug/SKILL.md +8 -12
- package/skills/debug/evals/cases.yaml +1 -1
- package/skills/decision-challenge/SKILL.md +1 -1
- package/skills/deprecation/SKILL.md +1 -1
- package/skills/ftd/SKILL.md +1 -3
- package/skills/ftd/evals/cases.yaml +1 -1
- package/skills/harness/SKILL.md +10 -9
- package/skills/harness/references/claude-md-template.md +1 -1
- package/skills/harness/references/wiki-protocol.md +5 -6
- package/skills/idea-refinement/SKILL.md +1 -1
- package/skills/implement/SKILL.md +14 -18
- package/skills/implement/evals/cases.yaml +1 -1
- package/skills/init/SKILL.md +36 -45
- package/skills/init/evals/README.md +3 -3
- package/skills/init/evals/cases.yaml +7 -7
- package/skills/init/references/accompaniment-and-profile.md +22 -40
- package/skills/init/references/discovery.md +2 -2
- package/skills/init/references/recommend-skills.md +5 -5
- package/skills/init/scripts/verify.sh +3 -7
- package/skills/knowledge-ops/SKILL.md +1 -1
- package/skills/orient/SKILL.md +52 -41
- package/skills/orient/evals/README.md +11 -9
- package/skills/orient/evals/cases.yaml +39 -29
- package/skills/orient/references/explain-ladder.md +93 -0
- package/skills/orient/references/orientation-contract.md +70 -18
- package/skills/parallel/SKILL.md +1 -1
- package/skills/parallel/evals/cases.yaml +2 -2
- package/skills/plan/SKILL.md +9 -13
- package/skills/review/SKILL.md +5 -8
- package/skills/roast-me/SKILL.md +1 -1
- package/skills/sdd/SKILL.md +10 -14
- package/skills/sdd/evals/README.md +2 -2
- package/skills/sdd/evals/cases.yaml +4 -4
- package/skills/sdd/references/model-routing.md +5 -12
- package/skills/sdd-init/SKILL.md +3 -3
- package/skills/ship/SKILL.md +6 -11
- package/skills/simplify-code/SKILL.md +1 -1
- package/skills/skill-scout/SKILL.md +1 -1
- package/skills/source-grounded-development/SKILL.md +1 -1
- package/skills/specify/SKILL.md +13 -14
- package/skills/specify/evals/cases.yaml +1 -1
- package/skills/specify/references/eliciting-requirements.md +5 -5
- package/skills/suggest/SKILL.md +3 -9
- package/skills/suggest/evals/cases.yaml +1 -1
- package/skills/tasks/SKILL.md +6 -7
- package/skills/tasks/evals/cases.yaml +1 -1
- package/skills/unslop/SKILL.md +39 -10
- package/skills/unslop/evals/README.md +7 -7
- package/skills/unslop/evals/cases.yaml +10 -9
- package/skills/verify/SKILL.md +5 -8
- package/skills/verify/evals/cases.yaml +1 -1
- package/skills/worktrees/SKILL.md +14 -18
- package/skills/worktrees/evals/cases.yaml +1 -1
- package/targets/_md-block.js +7 -1
- package/targets/auto-update.mjs +200 -0
- package/targets/claude.js +2 -0
- package/targets/clone-bootstrap.mjs +5 -2
- package/targets/opt-outs.js +1 -0
- package/targets/session-start.mjs +5 -78
- package/targets/update-wiring.js +153 -0
- package/skills/bro/SKILL.md +0 -76
- package/skills/bro/evals/README.md +0 -22
- package/skills/bro/evals/cases.yaml +0 -62
- package/skills/eli5/SKILL.md +0 -63
- package/skills/eli5/evals/README.md +0 -22
- package/skills/eli5/evals/cases.yaml +0 -54
- package/skills/show-me/SKILL.md +0 -140
- package/skills/show-me/evals/README.md +0 -23
- package/skills/show-me/evals/cases.yaml +0 -61
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// rsc update check, shared by every assistant that can run it. Standalone (Node built-ins only),
|
|
3
|
+
// copied to `.rsc/auto-update.mjs` at install, like the other hooks.
|
|
4
|
+
//
|
|
5
|
+
// Three ways in, one rule:
|
|
6
|
+
// - Claude Code: `session-start.mjs` imports `updateNotice` and prints it with the always-on body.
|
|
7
|
+
// - Codex, Gemini CLI, Cursor: a session-start hook runs `node .rsc/auto-update.mjs hook <target>`
|
|
8
|
+
// and gets the notice back in that assistant's own output shape.
|
|
9
|
+
// - OpenCode: its plugin imports `updateNotice` and hands the notice to the model.
|
|
10
|
+
// - Any other assistant has no hook. The always-on body asks the agent to run
|
|
11
|
+
// `node .rsc/auto-update.mjs` on its first turn, and to relay what it prints.
|
|
12
|
+
import { readFileSync, existsSync, writeFileSync, openSync } from 'node:fs';
|
|
13
|
+
import { spawn } from 'node:child_process';
|
|
14
|
+
import { join, resolve } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
|
|
17
|
+
// Update check: compare the installed version (.rsc/.version, written at install)
|
|
18
|
+
// against the latest published on npm, and nudge the agent to offer an update.
|
|
19
|
+
// Fail-silent (offline / missing baseline / parse error → nothing). Disable with
|
|
20
|
+
// RSC_NO_UPDATE_CHECK=1. RSC_LATEST overrides the npm lookup (tests / mirrors).
|
|
21
|
+
function isNewer(a, b) {
|
|
22
|
+
const pa = String(a).split('.').map(Number);
|
|
23
|
+
const pb = String(b).split('.').map(Number);
|
|
24
|
+
for (let i = 0; i < 3; i++) {
|
|
25
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return true;
|
|
26
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return false;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// "rsc X is out" was being ignored — the maintainer's own workspace sat on 2.0.4 for days with this
|
|
32
|
+
// notice on every session, and two people in two days reported bugs that were already fixed. A
|
|
33
|
+
// version number names nothing anyone recognises. A symptom does: someone who sees a red error after
|
|
34
|
+
// every turn updates the moment they are told that is what the update fixes (`suggest` §3: say what
|
|
35
|
+
// is wrong as a symptom, not as a cause).
|
|
36
|
+
//
|
|
37
|
+
// The symptoms travel for free: the registry's `/latest` document is the published package.json, so
|
|
38
|
+
// its `rscFixes` field — `{ fixedIn, symptom }` — arrives in the response this check already makes.
|
|
39
|
+
// It is text a model will read, so every entry is reduced to one short plain line or dropped.
|
|
40
|
+
const SEMVER_ONLY = /^\d+\.\d+\.\d+$/;
|
|
41
|
+
function fixesSince(list, installed, latest) {
|
|
42
|
+
if (!Array.isArray(list)) return [];
|
|
43
|
+
const out = [];
|
|
44
|
+
for (const item of list) {
|
|
45
|
+
if (!item || typeof item !== 'object' || typeof item.symptom !== 'string') continue;
|
|
46
|
+
if (!SEMVER_ONLY.test(String(item.fixedIn))) continue;
|
|
47
|
+
if (!isNewer(item.fixedIn, installed) || isNewer(item.fixedIn, latest)) continue;
|
|
48
|
+
const symptom = item.symptom.replace(/[\u0000-\u001f\u007f]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 200);
|
|
49
|
+
if (symptom) out.push({ fixedIn: item.fixedIn, symptom });
|
|
50
|
+
}
|
|
51
|
+
return out.slice(0, 8);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Auto-update. Asking was not enough: the notice depended on an agent relaying it, and people kept
|
|
55
|
+
// running bugs that were already fixed. So a release in the SAME major installs itself; a new major
|
|
56
|
+
// can change how the harness works, and still asks. On by default; `.rsc/.no-auto-update` (a project
|
|
57
|
+
// decision, travels in `optOuts`) brings back the question for every release.
|
|
58
|
+
//
|
|
59
|
+
// It runs in the background because `npx` can take longer than a hook may, and it changes nothing in
|
|
60
|
+
// this session anyway: the skills are already loaded. The version is the exact one seen here, never
|
|
61
|
+
// `@latest` — what is published in between does not slip in. One attempt per version: a failure falls
|
|
62
|
+
// back to asking, and is retried at most once a day.
|
|
63
|
+
const stateFile = (root) => join(root, '.rsc', 'auto-update.json');
|
|
64
|
+
const logFile = (root) => join(root, '.rsc', 'auto-update.log');
|
|
65
|
+
const AUTO_RUNNING_MS = 15 * 60 * 1000;
|
|
66
|
+
const AUTO_RETRY_MS = 24 * 60 * 60 * 1000;
|
|
67
|
+
const majorOf = (v) => Number(String(v).split('.')[0]) || 0;
|
|
68
|
+
|
|
69
|
+
function readAutoState(root) {
|
|
70
|
+
try { return JSON.parse(readFileSync(stateFile(root), 'utf8')); } catch { return null; }
|
|
71
|
+
}
|
|
72
|
+
function writeAutoState(root, state) {
|
|
73
|
+
try { writeFileSync(stateFile(root), `${JSON.stringify(state, null, 2)}\n`); } catch { /* best effort */ }
|
|
74
|
+
}
|
|
75
|
+
function declaredTargets(root) {
|
|
76
|
+
try {
|
|
77
|
+
const list = JSON.parse(readFileSync(join(root, '.rsc.json'), 'utf8')).targets;
|
|
78
|
+
return Array.isArray(list) ? list.filter((t) => typeof t === 'string' && /^[a-z0-9-]+$/.test(t)) : [];
|
|
79
|
+
} catch { return []; }
|
|
80
|
+
}
|
|
81
|
+
function launchUpdate(root, version, env) {
|
|
82
|
+
// RSC_AUTO_UPDATE_CMD (a JSON argv) replaces npx in tests; the arguments after it are the real ones.
|
|
83
|
+
const base = env.RSC_AUTO_UPDATE_CMD
|
|
84
|
+
? JSON.parse(env.RSC_AUTO_UPDATE_CMD)
|
|
85
|
+
: ['npx', '-y', `@ericrisco/rsc@${version}`];
|
|
86
|
+
const targets = declaredTargets(root);
|
|
87
|
+
const args = [...base.slice(1), 'sync', ...(targets.length ? ['--target', targets.join(',')] : [])];
|
|
88
|
+
const log = openSync(logFile(root), 'w');
|
|
89
|
+
const child = spawn(base[0], args, {
|
|
90
|
+
cwd: root,
|
|
91
|
+
detached: true,
|
|
92
|
+
stdio: ['ignore', log, log],
|
|
93
|
+
windowsHide: true,
|
|
94
|
+
shell: process.platform === 'win32', // npx is npx.cmd there; every argument was validated above
|
|
95
|
+
env: { ...env, RSC_NO_UPDATE_CHECK: '1', RSC_AUTO_UPDATE_TO: version },
|
|
96
|
+
});
|
|
97
|
+
child.on('error', () => {});
|
|
98
|
+
child.unref();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function updateNotice(root, env = process.env) {
|
|
102
|
+
let out = '';
|
|
103
|
+
const say = (text) => { out += text; };
|
|
104
|
+
try {
|
|
105
|
+
const installed = readFileSync(join(root, '.rsc', '.version'), 'utf8').trim();
|
|
106
|
+
// A background update that finished since the last session: say so once, then keep quiet.
|
|
107
|
+
const prior = readAutoState(root);
|
|
108
|
+
if (prior && !prior.reported && SEMVER_ONLY.test(String(prior.to)) && !isNewer(prior.to, installed)) {
|
|
109
|
+
say(`
|
|
110
|
+
===== rsc updated =====
|
|
111
|
+
rsc updated itself in the background, from ${prior.from} to ${installed}. It is active in this session.
|
|
112
|
+
ACTION: tell the user in one line. Files of the harness may show as changed in git.
|
|
113
|
+
=======================
|
|
114
|
+
`);
|
|
115
|
+
writeAutoState(root, { ...prior, reported: true });
|
|
116
|
+
}
|
|
117
|
+
let doc = null;
|
|
118
|
+
if (env.RSC_LATEST_JSON) doc = JSON.parse(env.RSC_LATEST_JSON);
|
|
119
|
+
else if (env.RSC_LATEST) doc = { version: env.RSC_LATEST };
|
|
120
|
+
else {
|
|
121
|
+
const ctrl = new AbortController();
|
|
122
|
+
const timer = setTimeout(() => ctrl.abort(), 1500);
|
|
123
|
+
const res = await fetch('https://registry.npmjs.org/@ericrisco%2frsc/latest', { signal: ctrl.signal });
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
doc = await res.json();
|
|
126
|
+
}
|
|
127
|
+
const latest = doc?.version;
|
|
128
|
+
if (installed && SEMVER_ONLY.test(String(latest)) && isNewer(latest, installed)) {
|
|
129
|
+
const newMajor = majorOf(latest) !== majorOf(installed);
|
|
130
|
+
const auto = !existsSync(join(root, '.rsc', '.no-auto-update')) && !newMajor;
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
const since = prior && prior.to === latest ? now - new Date(prior.startedAt).getTime() : Infinity;
|
|
133
|
+
if (auto && since >= AUTO_RETRY_MS) {
|
|
134
|
+
launchUpdate(root, latest, env);
|
|
135
|
+
writeAutoState(root, { from: installed, to: latest, startedAt: new Date(now).toISOString() });
|
|
136
|
+
say(`
|
|
137
|
+
===== rsc updating =====
|
|
138
|
+
rsc ${latest} is out — you have ${installed}. It is installing itself in the background now and
|
|
139
|
+
takes effect in the next session. Nothing to do.
|
|
140
|
+
ACTION: tell the user in one line, then continue with the task.
|
|
141
|
+
========================
|
|
142
|
+
`);
|
|
143
|
+
} else if (auto && since < AUTO_RUNNING_MS) {
|
|
144
|
+
say(`
|
|
145
|
+
===== rsc updating =====
|
|
146
|
+
rsc ${latest} is still installing itself in the background. It takes effect in the next session.
|
|
147
|
+
========================
|
|
148
|
+
`);
|
|
149
|
+
} else {
|
|
150
|
+
// Asking: auto-update is off here, the release is a new major, or the background attempt failed.
|
|
151
|
+
let why = '';
|
|
152
|
+
if (auto) why = `The automatic update did not finish (log: .rsc/auto-update.log); it retries once a day.\n`;
|
|
153
|
+
else if (newMajor) why = 'It is a new major version: it can change how the harness works, so it asks first.\n';
|
|
154
|
+
let fixes = [];
|
|
155
|
+
try { fixes = fixesSince(doc.rscFixes, installed, latest); } catch { fixes = []; }
|
|
156
|
+
if (fixes.length) {
|
|
157
|
+
say(`
|
|
158
|
+
===== rsc update available =====
|
|
159
|
+
rsc ${latest} is out — you have ${installed}. It fixes problems this install still has:
|
|
160
|
+
${fixes.map((f) => ` - ${f.symptom} (fixed in ${f.fixedIn})`).join('\n')}
|
|
161
|
+
${why}ACTION: before starting the task, tell the user in one line that the update fixes what they
|
|
162
|
+
may be seeing, and if they say yes, run it in this project:
|
|
163
|
+
npx @ericrisco/rsc@latest
|
|
164
|
+
================================
|
|
165
|
+
`);
|
|
166
|
+
} else {
|
|
167
|
+
say(`
|
|
168
|
+
===== rsc update available =====
|
|
169
|
+
rsc ${latest} is out — you have ${installed}.
|
|
170
|
+
${why}ACTION: tell the user a new version is available and, if they say yes, run:
|
|
171
|
+
npx @ericrisco/rsc@latest
|
|
172
|
+
(That reinstalls and refreshes the skill content to the latest.)
|
|
173
|
+
================================
|
|
174
|
+
`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} catch { /* offline / no baseline / parse error → stay silent */ }
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// What each assistant's session-start hook accepts. Codex and Gemini take Claude's shape; Cursor has
|
|
183
|
+
// its own field names. Plain text for an agent that runs this by hand.
|
|
184
|
+
export function hookOutput(target, notice) {
|
|
185
|
+
if (!notice) return target === 'agent' ? '' : '{}';
|
|
186
|
+
if (target === 'agent') return notice;
|
|
187
|
+
if (target === 'cursor') return JSON.stringify({ additional_context: notice });
|
|
188
|
+
return JSON.stringify({ hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: notice } });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Run as a script: `node auto-update.mjs [hook <target>] [root]`. Never fails a session.
|
|
192
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
193
|
+
const hook = process.argv[2] === 'hook';
|
|
194
|
+
const target = hook ? String(process.argv[3] || 'agent') : 'agent';
|
|
195
|
+
const root = (hook ? process.argv[4] : process.argv[2]) || process.cwd();
|
|
196
|
+
let notice = '';
|
|
197
|
+
try { if (!process.env.RSC_NO_UPDATE_CHECK) notice = (await updateNotice(root)).trim(); } catch { /* fail open */ }
|
|
198
|
+
if (!hook && !notice) notice = 'rsc is up to date.';
|
|
199
|
+
process.stdout.write(`${hookOutput(target, notice)}\n`);
|
|
200
|
+
}
|
package/targets/claude.js
CHANGED
|
@@ -118,6 +118,8 @@ export function wireHook(paths, sourceMd, policy = {}) {
|
|
|
118
118
|
// Same reason: session-start imports the reaper for the landed-worktree sweep, so it travels as a
|
|
119
119
|
// sibling too. Missing it does not break startup — the sweep is wrapped — it just goes silent.
|
|
120
120
|
copyFileSync(join(HERE, 'worktree-reaper.mjs'), join(paths.projectRoot, '.rsc', 'worktree-reaper.mjs'));
|
|
121
|
+
// session-start imports the shared update check, so it travels as a sibling too.
|
|
122
|
+
copyFileSync(join(HERE, 'auto-update.mjs'), join(paths.projectRoot, '.rsc', 'auto-update.mjs'));
|
|
121
123
|
copyFileSync(join(HERE, 'session-start.mjs'), scriptDest);
|
|
122
124
|
// The one harness file that belongs in the committed tree rather than in `.rsc/`: it is the only
|
|
123
125
|
// thing standing between a clone and seven stack traces, so it has to survive `git clone`.
|
|
@@ -43,7 +43,7 @@ const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9a-z.-]+)?$/i;
|
|
|
43
43
|
* nothing checks is just drift with a head start.
|
|
44
44
|
*/
|
|
45
45
|
export const PROJECT_OPT_OUTS = [
|
|
46
|
-
'audit', 'claudemd-check', 'danger-guard', 'feature-gate', 'gitmoji', 'ship-guard', 'worktree-cleanup',
|
|
46
|
+
'audit', 'auto-update', 'claudemd-check', 'danger-guard', 'feature-gate', 'gitmoji', 'ship-guard', 'worktree-cleanup',
|
|
47
47
|
];
|
|
48
48
|
|
|
49
49
|
/**
|
|
@@ -93,7 +93,10 @@ export function readManifest(root) {
|
|
|
93
93
|
// accepted, and anything else is simply not there.
|
|
94
94
|
const ID = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
95
95
|
const ids = (v) => (Array.isArray(v) ? v.filter((x) => typeof x === 'string' && ID.test(x)) : []);
|
|
96
|
-
|
|
96
|
+
// Retired skills read as their successors. Canonical map: scripts/lib/retired-skills.js — inlined
|
|
97
|
+
// because this file cannot import anything, and tests/retired-skills.test.js holds the two equal.
|
|
98
|
+
const RETIRED = { eli5: 'orient', 'show-me': 'orient', bro: 'unslop' };
|
|
99
|
+
const skills = [...new Set(ids(parsed.skills).map((id) => (Object.hasOwn(RETIRED, id) ? RETIRED[id] : id)))];
|
|
97
100
|
const own = ids(parsed.ownSkills);
|
|
98
101
|
// A manifest with nothing declared is not a clone waiting to be equipped; there is nothing to offer.
|
|
99
102
|
if (!skills.length && !own.length) return { state: 'absent' };
|
package/targets/opt-outs.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
/** Decisions of the project: they belong in `.rsc.json` and are rebuilt in a clone. */
|
|
19
19
|
export const PROJECT_OPT_OUTS = [
|
|
20
20
|
'audit', // cadence of this project's skill audit
|
|
21
|
+
'auto-update', // installing new rsc releases alone: one machine ahead moves the team's catalogVersion
|
|
21
22
|
'claudemd-check', // the root CLAUDE.md context budget
|
|
22
23
|
'danger-guard', // foot-gun denial, keyed to the committed user profile
|
|
23
24
|
'feature-gate', // per-turn re-injection of the SDD gate
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// workspace has no harness profile yet, and an auto-ingest nudge when there is
|
|
7
7
|
// un-ingested material waiting in the inbox.
|
|
8
8
|
import { readFileSync, existsSync, readdirSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { updateNotice } from './auto-update.mjs';
|
|
9
10
|
import { join, resolve } from 'node:path';
|
|
10
11
|
import { homedir } from 'node:os';
|
|
11
12
|
import { claimOnce, readHookInput } from './hook-once.mjs';
|
|
@@ -120,7 +121,7 @@ if (!existsSync(profile) && !existsSync(optout)) {
|
|
|
120
121
|
process.stdout.write(`
|
|
121
122
|
===== rsc onboarding =====
|
|
122
123
|
Fresh setup: 02-DOCS/wiki/harness/user-profile.md is missing.
|
|
123
|
-
ACTION: invoke \`init\` now (first contact: technical
|
|
124
|
+
ACTION: invoke \`init\` now (first contact, one question: technical or with analogies) before the task.
|
|
124
125
|
If the user does not want a harness here: create .rsc/.no-harness
|
|
125
126
|
==========================
|
|
126
127
|
`);
|
|
@@ -252,80 +253,6 @@ A no holds for this session. Permanent off: .rsc/.no-worktree-cleanup
|
|
|
252
253
|
} catch { /* reaper missing or git unhappy → say nothing; this is never worth breaking startup for */ }
|
|
253
254
|
}
|
|
254
255
|
|
|
255
|
-
// Update check:
|
|
256
|
-
//
|
|
257
|
-
|
|
258
|
-
// RSC_NO_UPDATE_CHECK=1. RSC_LATEST overrides the npm lookup (tests / mirrors).
|
|
259
|
-
function isNewer(a, b) {
|
|
260
|
-
const pa = String(a).split('.').map(Number);
|
|
261
|
-
const pb = String(b).split('.').map(Number);
|
|
262
|
-
for (let i = 0; i < 3; i++) {
|
|
263
|
-
if ((pa[i] || 0) > (pb[i] || 0)) return true;
|
|
264
|
-
if ((pa[i] || 0) < (pb[i] || 0)) return false;
|
|
265
|
-
}
|
|
266
|
-
return false;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// "rsc X is out" was being ignored — the maintainer's own workspace sat on 2.0.4 for days with this
|
|
270
|
-
// notice on every session, and two people in two days reported bugs that were already fixed. A
|
|
271
|
-
// version number names nothing anyone recognises. A symptom does: someone who sees a red error after
|
|
272
|
-
// every turn updates the moment they are told that is what the update fixes (`suggest` §3: say what
|
|
273
|
-
// is wrong as a symptom, not as a cause).
|
|
274
|
-
//
|
|
275
|
-
// The symptoms travel for free: the registry's `/latest` document is the published package.json, so
|
|
276
|
-
// its `rscFixes` field — `{ fixedIn, symptom }` — arrives in the response this check already makes.
|
|
277
|
-
// It is text a model will read, so every entry is reduced to one short plain line or dropped.
|
|
278
|
-
const SEMVER_ONLY = /^\d+\.\d+\.\d+$/;
|
|
279
|
-
function fixesSince(list, installed, latest) {
|
|
280
|
-
if (!Array.isArray(list)) return [];
|
|
281
|
-
const out = [];
|
|
282
|
-
for (const item of list) {
|
|
283
|
-
if (!item || typeof item !== 'object' || typeof item.symptom !== 'string') continue;
|
|
284
|
-
if (!SEMVER_ONLY.test(String(item.fixedIn))) continue;
|
|
285
|
-
if (!isNewer(item.fixedIn, installed) || isNewer(item.fixedIn, latest)) continue;
|
|
286
|
-
const symptom = item.symptom.replace(/[\u0000-\u001f\u007f]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 200);
|
|
287
|
-
if (symptom) out.push({ fixedIn: item.fixedIn, symptom });
|
|
288
|
-
}
|
|
289
|
-
return out.slice(0, 8);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (!process.env.RSC_NO_UPDATE_CHECK) {
|
|
293
|
-
try {
|
|
294
|
-
const installed = readFileSync(join(root, '.rsc', '.version'), 'utf8').trim();
|
|
295
|
-
let doc = null;
|
|
296
|
-
if (process.env.RSC_LATEST_JSON) doc = JSON.parse(process.env.RSC_LATEST_JSON);
|
|
297
|
-
else if (process.env.RSC_LATEST) doc = { version: process.env.RSC_LATEST };
|
|
298
|
-
else {
|
|
299
|
-
const ctrl = new AbortController();
|
|
300
|
-
const timer = setTimeout(() => ctrl.abort(), 1500);
|
|
301
|
-
const res = await fetch('https://registry.npmjs.org/@ericrisco%2frsc/latest', { signal: ctrl.signal });
|
|
302
|
-
clearTimeout(timer);
|
|
303
|
-
doc = await res.json();
|
|
304
|
-
}
|
|
305
|
-
const latest = doc?.version;
|
|
306
|
-
if (installed && latest && isNewer(latest, installed)) {
|
|
307
|
-
let fixes = [];
|
|
308
|
-
try { fixes = fixesSince(doc.rscFixes, installed, latest); } catch { fixes = []; }
|
|
309
|
-
if (fixes.length) {
|
|
310
|
-
process.stdout.write(`
|
|
311
|
-
===== rsc update available =====
|
|
312
|
-
rsc ${latest} is out — you have ${installed}. It fixes problems this install still has:
|
|
313
|
-
${fixes.map((f) => ` - ${f.symptom} (fixed in ${f.fixedIn})`).join('\n')}
|
|
314
|
-
ACTION: before starting the task, tell the user in one line that the update fixes what they
|
|
315
|
-
may be seeing, and if they say yes, run it in this project:
|
|
316
|
-
npx @ericrisco/rsc@latest
|
|
317
|
-
================================
|
|
318
|
-
`);
|
|
319
|
-
} else {
|
|
320
|
-
process.stdout.write(`
|
|
321
|
-
===== rsc update available =====
|
|
322
|
-
rsc ${latest} is out — you have ${installed}.
|
|
323
|
-
ACTION: tell the user a new version is available and, if they say yes, run:
|
|
324
|
-
npx @ericrisco/rsc@latest
|
|
325
|
-
(That reinstalls and refreshes the skill content to the latest.)
|
|
326
|
-
================================
|
|
327
|
-
`);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
} catch { /* offline / no baseline / parse error → stay silent */ }
|
|
331
|
-
}
|
|
256
|
+
// Update check and auto-update: the rule lives in auto-update.mjs, shared with the other assistants.
|
|
257
|
+
// Disable with RSC_NO_UPDATE_CHECK=1.
|
|
258
|
+
if (!process.env.RSC_NO_UPDATE_CHECK) process.stdout.write(await updateNotice(root));
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Wiring for the update check (`auto-update.mjs`). Every assistant gets the module in `.rsc/`; the
|
|
2
|
+
// ones with a session-start event also get a hook that runs it, in the same project-local config
|
|
3
|
+
// files the session memory uses, under a needle of its own so either can be wired or removed
|
|
4
|
+
// without touching the other.
|
|
5
|
+
//
|
|
6
|
+
// Kept apart from `memory.js` on purpose: the memory promises it makes no network request, and a
|
|
7
|
+
// test holds every file it wires to that. The update check exists to ask npm.
|
|
8
|
+
//
|
|
9
|
+
// Claude Code is not in CONFIG: its `session-start.mjs` already imports the module. An assistant
|
|
10
|
+
// with no hook at all is told by the always-on body to run the module on its first turn.
|
|
11
|
+
import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { execFileSync } from 'node:child_process';
|
|
13
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
|
|
16
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const NEEDLE = '.rsc/auto-update.mjs';
|
|
18
|
+
const PLUGIN_MARKER = 'rsc-update:managed';
|
|
19
|
+
|
|
20
|
+
const CONFIG = Object.freeze({
|
|
21
|
+
codex: '.codex/hooks.json',
|
|
22
|
+
cursor: '.cursor/hooks.json',
|
|
23
|
+
gemini: '.gemini/settings.json',
|
|
24
|
+
opencode: '.opencode/plugins/rsc-update.js',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const EVENT = Object.freeze({ codex: 'SessionStart', gemini: 'SessionStart', cursor: 'sessionStart' });
|
|
28
|
+
|
|
29
|
+
/** Assistants where the update runs on its own. Every other one depends on the agent running it. */
|
|
30
|
+
export const UPDATE_HOOK_TARGETS = Object.freeze(['claude', ...Object.keys(CONFIG)].sort());
|
|
31
|
+
|
|
32
|
+
const git = (cwd, args) => {
|
|
33
|
+
try { return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); } catch { return null; }
|
|
34
|
+
};
|
|
35
|
+
const rel = (cwd, path) => relative(cwd, path).split(sep).join('/');
|
|
36
|
+
const tracked = (cwd, path) => Boolean(git(cwd, ['ls-files', '--', rel(cwd, path)]));
|
|
37
|
+
|
|
38
|
+
function exclude(cwd, path) {
|
|
39
|
+
if (git(cwd, ['rev-parse', '--is-inside-work-tree']) !== 'true') return;
|
|
40
|
+
const value = git(cwd, ['rev-parse', '--git-path', 'info/exclude']);
|
|
41
|
+
if (!value) return;
|
|
42
|
+
const file = isAbsolute(value) ? value : resolve(cwd, value);
|
|
43
|
+
const pattern = `/${rel(cwd, path)}`;
|
|
44
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
45
|
+
const body = existsSync(file) ? readFileSync(file, 'utf8') : '';
|
|
46
|
+
if (!body.split('\n').includes(pattern)) appendFileSync(file, `${body && !body.endsWith('\n') ? '\n' : ''}${pattern}\n`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readConfig(path) {
|
|
50
|
+
if (!existsSync(path)) return {};
|
|
51
|
+
try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const validHookConfig = (c) => Boolean(c) && typeof c === 'object' && !Array.isArray(c)
|
|
55
|
+
&& (!Object.hasOwn(c, 'hooks') || (c.hooks && typeof c.hooks === 'object' && !Array.isArray(c.hooks) && Object.values(c.hooks).every(Array.isArray)));
|
|
56
|
+
|
|
57
|
+
function handler(target) {
|
|
58
|
+
const script = target === 'codex'
|
|
59
|
+
? '"$(git rev-parse --show-toplevel)/.rsc/auto-update.mjs"'
|
|
60
|
+
: '".rsc/auto-update.mjs"';
|
|
61
|
+
const value = { type: 'command', command: `node ${script} hook ${target}`, timeout: target === 'gemini' ? 10000 : 10 };
|
|
62
|
+
if (target === 'codex') value.commandWindows = `node "$((git rev-parse --show-toplevel).Trim())/.rsc/auto-update.mjs" hook ${target}`;
|
|
63
|
+
if (target === 'gemini') value.name = 'rsc-update';
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function strip(config) {
|
|
68
|
+
if (!validHookConfig(config) || !config.hooks) return config;
|
|
69
|
+
for (const event of Object.keys(config.hooks)) {
|
|
70
|
+
config.hooks[event] = config.hooks[event].filter((e) => !JSON.stringify(e).replaceAll('\\\\', '/').includes(NEEDLE));
|
|
71
|
+
if (!config.hooks[event].length) delete config.hooks[event];
|
|
72
|
+
}
|
|
73
|
+
if (!Object.keys(config.hooks).length) delete config.hooks;
|
|
74
|
+
return config;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const PLUGIN = `// ${PLUGIN_MARKER}
|
|
78
|
+
// rsc update check for OpenCode: checked once per OpenCode run, and the notice goes to the model to be
|
|
79
|
+
// relayed. It rides on EVERY system prompt of the run, not just the first: OpenCode's first call is
|
|
80
|
+
// its title generator, which would swallow a once-only notice before the agent ever sees it.
|
|
81
|
+
// Turn auto-update off with .rsc/.no-auto-update.
|
|
82
|
+
import { updateNotice } from '../../.rsc/auto-update.mjs';
|
|
83
|
+
|
|
84
|
+
export const RscUpdatePlugin = async ({ directory, worktree }) => {
|
|
85
|
+
const cwd = worktree || directory;
|
|
86
|
+
const pending = process.env.RSC_NO_UPDATE_CHECK ? Promise.resolve('') : updateNotice(cwd).catch(() => '');
|
|
87
|
+
return {
|
|
88
|
+
'experimental.chat.system.transform': async (_input, output) => {
|
|
89
|
+
const said = (await pending).trim();
|
|
90
|
+
if (said && Array.isArray(output?.system)) output.system.push(said + '\\nMention this only in your first reply of the session.');
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
`;
|
|
95
|
+
|
|
96
|
+
export function updateManagedPaths(target, cwd = process.cwd()) {
|
|
97
|
+
const module = join(cwd, '.rsc', 'auto-update.mjs');
|
|
98
|
+
return CONFIG[target] ? [module, join(cwd, ...CONFIG[target].split('/'))] : [module];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Wire it. Always: being off is the runtime's `.no-auto-update`, and the check itself stays on. */
|
|
102
|
+
export function wireUpdate(target, cwd = process.cwd()) {
|
|
103
|
+
const module = join(cwd, '.rsc', 'auto-update.mjs');
|
|
104
|
+
mkdirSync(dirname(module), { recursive: true });
|
|
105
|
+
copyFileSync(join(HERE, 'auto-update.mjs'), module);
|
|
106
|
+
if (!CONFIG[target]) {
|
|
107
|
+
return { mode: target === 'claude' ? 'hook' : 'agent', paths: [module] };
|
|
108
|
+
}
|
|
109
|
+
const configPath = join(cwd, ...CONFIG[target].split('/'));
|
|
110
|
+
if (tracked(cwd, configPath)) return { mode: 'agent', reason: 'config-tracked', paths: [module] };
|
|
111
|
+
if (target === 'opencode' && existsSync(configPath) && !readFileSync(configPath, 'utf8').includes(PLUGIN_MARKER)) {
|
|
112
|
+
return { mode: 'agent', reason: 'plugin-collision', paths: [module] };
|
|
113
|
+
}
|
|
114
|
+
const config = target === 'opencode' ? null : readConfig(configPath);
|
|
115
|
+
if (target !== 'opencode' && !validHookConfig(config)) return { mode: 'agent', reason: 'config-invalid', paths: [module] };
|
|
116
|
+
|
|
117
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
118
|
+
if (target === 'opencode') writeFileSync(configPath, PLUGIN);
|
|
119
|
+
else {
|
|
120
|
+
strip(config);
|
|
121
|
+
config.hooks ||= {};
|
|
122
|
+
const event = EVENT[target];
|
|
123
|
+
config.hooks[event] ||= [];
|
|
124
|
+
const h = handler(target);
|
|
125
|
+
if (target === 'cursor') { config.version ||= 1; delete h.type; delete h.timeout; config.hooks[event].push(h); }
|
|
126
|
+
else config.hooks[event].push({ hooks: [h] });
|
|
127
|
+
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
128
|
+
}
|
|
129
|
+
const paths = updateManagedPaths(target, cwd);
|
|
130
|
+
for (const path of paths) exclude(cwd, path);
|
|
131
|
+
return { mode: 'hook', paths };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function unwireUpdate(target, cwd = process.cwd()) {
|
|
135
|
+
if (!CONFIG[target]) return [];
|
|
136
|
+
const path = join(cwd, ...CONFIG[target].split('/'));
|
|
137
|
+
const touched = [];
|
|
138
|
+
if (target === 'opencode') {
|
|
139
|
+
if (existsSync(path) && readFileSync(path, 'utf8').includes(PLUGIN_MARKER)) { rmSync(path, { force: true }); touched.push(path); }
|
|
140
|
+
} else if (existsSync(path) && readFileSync(path, 'utf8').replaceAll('\\\\', '/').includes(NEEDLE)) {
|
|
141
|
+
const config = readConfig(path);
|
|
142
|
+
if (config) { strip(config); writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`); touched.push(path); }
|
|
143
|
+
}
|
|
144
|
+
return touched;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function updateArtifactsPresent(target, cwd = process.cwd()) {
|
|
148
|
+
if (!CONFIG[target]) return [];
|
|
149
|
+
const path = join(cwd, ...CONFIG[target].split('/'));
|
|
150
|
+
if (!existsSync(path)) return [];
|
|
151
|
+
const body = readFileSync(path, 'utf8').replaceAll('\\\\', '/');
|
|
152
|
+
return body.includes(NEEDLE) || body.includes(PLUGIN_MARKER) ? [path] : [];
|
|
153
|
+
}
|
package/skills/bro/SKILL.md
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: bro
|
|
3
|
-
description: "Use whenever the user asks to make an answer or draft sound human, natural, plain-spoken or less AI-written — including a terse bro after the last response. Rewrites or drafts in the same language while preserving meaning, facts and channel. NOT a reusable brand voice (that is `brand-voice`), NOT translation (that is `translation-l10n`)."
|
|
4
|
-
tags: [bro, human-writing, humano, plain-language, natural-language, rewrite, no-jargon]
|
|
5
|
-
recommends: [unslop, brand-voice, technical-writing, translation-l10n]
|
|
6
|
-
profiles: [minimal, core, full]
|
|
7
|
-
origin: risco
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
# bro — say it like a person
|
|
11
|
-
|
|
12
|
-
Make the words sound like someone with something real to say. Keep the substance; remove the
|
|
13
|
-
performance around it.
|
|
14
|
-
|
|
15
|
-
## The contract
|
|
16
|
-
|
|
17
|
-
1. **Find the source without ceremony.** A bare “bro”, “write like a human”, “hazlo más natural”,
|
|
18
|
-
or equivalent points to the assistant's last answer. If the user pasted text, rewrite that. If
|
|
19
|
-
they requested new copy, draft it directly. Ask for text only when no source exists.
|
|
20
|
-
2. **Preserve the payload.** Keep names, facts, numbers, links, commitments, constraints,
|
|
21
|
-
uncertainty and necessary warnings. Do not improve the prose by inventing evidence, opinions,
|
|
22
|
-
anecdotes or certainty.
|
|
23
|
-
3. **Match the room.** Keep the user's language and fit the audience, relationship and channel. A
|
|
24
|
-
natural legal notice is still precise; a natural Slack message can be loose. “Human” does not
|
|
25
|
-
automatically mean casual.
|
|
26
|
-
4. **Cut the bot-shaped scaffolding.** Remove throat-clearing, needless summaries, repeated
|
|
27
|
-
conclusions, corporate filler, fake enthusiasm, abstract nouns and headings or bullets that do
|
|
28
|
-
not help the reader navigate.
|
|
29
|
-
5. **Restore a real cadence.** Prefer concrete verbs, direct sentences and natural contractions.
|
|
30
|
-
Vary sentence length when it helps. Use the words a person in this context would actually choose,
|
|
31
|
-
without manufacturing slang, typos or choppy fragments.
|
|
32
|
-
6. **Return the writing, not a review of the writing.** By default, output the rewritten or newly
|
|
33
|
-
drafted text with no “here is a more human version” preface. Explain the changes only if asked.
|
|
34
|
-
|
|
35
|
-
When the original contains code, commands, URLs, quoted terms or regulated wording, leave those
|
|
36
|
-
pieces exact unless the user explicitly asks to change them. Explain unavoidable jargon once in
|
|
37
|
-
plain language instead of deleting precision.
|
|
38
|
-
|
|
39
|
-
## What “human” means here
|
|
40
|
-
|
|
41
|
-
| Aim | The result |
|
|
42
|
-
| --- | --- |
|
|
43
|
-
| Plain, not simplistic | Easy to follow without losing the idea |
|
|
44
|
-
| Concise, not incomplete | No padding; all load-bearing facts remain |
|
|
45
|
-
| Warm, not performative | Appropriate care without canned empathy |
|
|
46
|
-
| Conversational, not sloppy | Natural rhythm without fake mistakes |
|
|
47
|
-
| Specific, not decorated | Concrete nouns and verbs instead of hype |
|
|
48
|
-
|
|
49
|
-
## Anti-patterns
|
|
50
|
-
|
|
51
|
-
| Anti-pattern | Why it fails | Do this instead |
|
|
52
|
-
| --- | --- | --- |
|
|
53
|
-
| Add typos, slang or filler to seem human | It performs humanity and can sound less credible | Match the actual relationship and channel |
|
|
54
|
-
| Replace technical terms with vague words | Readability bought with lost meaning is a defect | Keep or define the necessary term |
|
|
55
|
-
| Invent a personal story or strong opinion | A smoother fabrication is still a fabrication | Preserve only what the source supports |
|
|
56
|
-
| Use the same “casual” voice for every audience | A client email, README and condolence need different registers | Infer the room before choosing the cadence |
|
|
57
|
-
| Promise the result is undetectable as AI | No rewrite can honestly guarantee that | Promise clarity and naturalness, not detector outcomes |
|
|
58
|
-
| Announce and justify every edit | The preamble becomes the same friction the user asked to remove | Lead with the finished words |
|
|
59
|
-
|
|
60
|
-
## Where this ends
|
|
61
|
-
|
|
62
|
-
`unslop` owns the other half of this job: the **named** audit of a text that already exists, tell by
|
|
63
|
-
catalogued tell (puffery, `not just X but Y`, em dashes, bold-label lists, hedging), run before the
|
|
64
|
-
text ships. This skill owns the **register**: which language, which relationship, which channel, and
|
|
65
|
-
the cadence that fits them. A bare "bro" is always this skill. "Check this for AI tells before I
|
|
66
|
-
publish it" is `unslop`. When a text needs both, audit first, register last.
|
|
67
|
-
|
|
68
|
-
## Output boundary
|
|
69
|
-
|
|
70
|
-
Treat the rewritten text as an artifact: keep harness commentary outside it. If the task also
|
|
71
|
-
requires a brújula block, place that block after a clear separation so it cannot be copied into an
|
|
72
|
-
email, document or message by accident.
|
|
73
|
-
|
|
74
|
-
## Orientación (siempre)
|
|
75
|
-
|
|
76
|
-
Cierra cada turno con el **bloque-brújula** (📍 dónde estás · ✅ qué hiciste · 🧭 por qué · ➡️ siguiente, terminando en pregunta), calibrado al dial de `02-DOCS/wiki/harness/user-profile.md`. **Nunca termines en seco.** Protocolo completo: skill `orient` → `skills/orient/references/orientation-contract.md`. (Defiere a `suggest` el "¿instalo la skill que falta?".)
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
# Evals — bro
|
|
2
|
-
|
|
3
|
-
These cases test two different claims: that human-language requests route to `bro` without
|
|
4
|
-
stealing adjacent writing jobs, and that the loaded skill produces a more natural result without
|
|
5
|
-
changing the payload.
|
|
6
|
-
|
|
7
|
-
## Triggering
|
|
8
|
-
|
|
9
|
-
Load only `bro` and run every `should_trigger` and `should_not_trigger` prompt in 3–5 fresh
|
|
10
|
-
sessions. A positive passes when `bro` loads in a majority of trials. A negative passes when it
|
|
11
|
-
stays quiet; also check that the declared `route_to` skill is the genuine owner. Require at least
|
|
12
|
-
90% accuracy across all cases.
|
|
13
|
-
|
|
14
|
-
## Capability
|
|
15
|
-
|
|
16
|
-
Run the capability scenario in two fresh conditions: without `bro`, then with it. Use the same
|
|
17
|
-
source answer and one consistent grader. Score each `must_include` point over three trials. The
|
|
18
|
-
with-skill condition must satisfy at least 80% of the rubric and beat the baseline by at least 20
|
|
19
|
-
percentage points.
|
|
20
|
-
|
|
21
|
-
This is an LLM/human-judged A/B eval, not deterministic CI. Re-run it after changing either the
|
|
22
|
-
description or the body; routing and prose quality are both wording-sensitive.
|