@ericrisco/rsc 1.0.4 → 1.0.5
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 +2 -2
- package/package.json +1 -1
- package/scripts/doctor.js +3 -0
- package/scripts/lib/capabilities.js +189 -0
- package/scripts/rsc.js +45 -1
- package/skills/skill-scout/SKILL.md +39 -3
- package/skills/suggest/SKILL.md +6 -0
- package/targets/sello.mjs +6 -0
package/manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.0.
|
|
2
|
+
"version": "1.0.5",
|
|
3
3
|
"counts": {
|
|
4
4
|
"skills": 258
|
|
5
5
|
},
|
|
@@ -4348,7 +4348,7 @@
|
|
|
4348
4348
|
},
|
|
4349
4349
|
{
|
|
4350
4350
|
"id": "skill-scout",
|
|
4351
|
-
"description": "Use when a specialized skill probably exists but is not loaded, when a task feels harder than it should because generic reasoning is doing a domain skill's job,
|
|
4351
|
+
"description": "Use when a specialized skill probably exists but is not loaded, when a task feels harder than it should because generic reasoning is doing a domain skill's job, when the work just delivered was a repeatable procedure worth turning into a skill or an agent, or when auditing coverage for holes. Resolves against what already exists — installed skills, catalog skills and agent files — then names the gap and emits the install command. NOT routing among skills you already have (that is `suggest`), NOT writing the artifact when none exists (that is `author-skill` for skills, `building-agents` for agents).",
|
|
4352
4352
|
"tags": [
|
|
4353
4353
|
"skill-discovery",
|
|
4354
4354
|
"capability-gap",
|
package/package.json
CHANGED
package/scripts/doctor.js
CHANGED
|
@@ -7,6 +7,7 @@ import { loadManifest } from './lib/manifest.js';
|
|
|
7
7
|
import { listBackups } from './lib/backups.js';
|
|
8
8
|
import { SDD_GATE_TEXT } from '../targets/hook-once.mjs';
|
|
9
9
|
import { isEnabled, checkSello, readSello, countFindings, readEffectiveConfig, validateRiskConfig } from '../targets/sello.mjs';
|
|
10
|
+
import { countGaps } from './lib/capabilities.js';
|
|
10
11
|
|
|
11
12
|
// The sello's health, surfaced where the user already looks (spec: non-blocking
|
|
12
13
|
// findings live in the project and are SUMMARIZED here, never nagged about).
|
|
@@ -56,6 +57,8 @@ export function doctor({ target, home, cwd }) {
|
|
|
56
57
|
},
|
|
57
58
|
contextBudget: contextBudget({ target, home, cwd }),
|
|
58
59
|
sello: selloStatus(root),
|
|
60
|
+
// Counted, never interpreted — by spec, the gap log's reader is the user.
|
|
61
|
+
automationGaps: countGaps(root),
|
|
59
62
|
};
|
|
60
63
|
for (const [id, e] of Object.entries(state.skills)) {
|
|
61
64
|
for (const f of e.files) if (!existsSync(f)) report.missing.push(`${id}:${f}`);
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// capabilities.js — "what do I already have that solves this?", answered by a
|
|
2
|
+
// command instead of by the model's memory.
|
|
3
|
+
//
|
|
4
|
+
// The automation-gap rule says: never propose BUILDING a skill or an agent before
|
|
5
|
+
// checking whether one already exists. A rule like that is only real if something
|
|
6
|
+
// enumerates the existing set — otherwise it is a decorative gate, the defect class
|
|
7
|
+
// this repo has paid for repeatedly. This file is that something.
|
|
8
|
+
//
|
|
9
|
+
// Three sources, one shape:
|
|
10
|
+
// installed — skill artifacts ON DISK, project and user scope, each tagged
|
|
11
|
+
// available — catalog skills not installed anywhere (ids; --full adds descriptions)
|
|
12
|
+
// agents — agent files on disk, project and user scope, each tagged
|
|
13
|
+
//
|
|
14
|
+
// Both skills and agents are read from DISK, never from the install state file: a
|
|
15
|
+
// state entry whose files were deleted would otherwise answer "covered — use it"
|
|
16
|
+
// for something that is not there, and that answer has no recovery path.
|
|
17
|
+
//
|
|
18
|
+
// 8 of 17 targets have file-based agents; on the other 9 the agent is advisory and
|
|
19
|
+
// there is nothing to enumerate. That is `agentsSupported: false`, never an error —
|
|
20
|
+
// the caller drops the agent branch instead of failing.
|
|
21
|
+
|
|
22
|
+
import { existsSync, readdirSync, statSync, appendFileSync, readFileSync, mkdirSync } from 'node:fs';
|
|
23
|
+
import { join, dirname, basename, resolve } from 'node:path';
|
|
24
|
+
import { homedir } from 'node:os';
|
|
25
|
+
import { loadManifest } from './manifest.js';
|
|
26
|
+
import { targetPaths } from '../../targets/index.js';
|
|
27
|
+
import { developerAgentPath, targetHasAgents } from '../../targets/agents.js';
|
|
28
|
+
|
|
29
|
+
// AGENT_TARGETS is not exported, so derive dir + extension from the one path helper
|
|
30
|
+
// that owns it — duplicating that table is how two copies drift apart. The extension
|
|
31
|
+
// is taken as everything after the agent's name, because `extname()` cannot recover a
|
|
32
|
+
// COMPOUND extension: copilot writes `developer.agent.md`, where extname says '.md',
|
|
33
|
+
// which both mis-names the agent and degrades the filter to every markdown file.
|
|
34
|
+
const AGENT_NAME = 'developer';
|
|
35
|
+
function agentSpecFor(target, root) {
|
|
36
|
+
const p = developerAgentPath(target, root);
|
|
37
|
+
if (!p) return null;
|
|
38
|
+
const base = basename(p);
|
|
39
|
+
const i = base.indexOf(AGENT_NAME);
|
|
40
|
+
const ext = i === 0 ? base.slice(AGENT_NAME.length) : base.slice(base.indexOf('.'));
|
|
41
|
+
return { dir: dirname(p), ext };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Real files only. Directories (a dir named `foo.md` is not an agent), dangling
|
|
45
|
+
// symlinks and dotfiles are not capabilities.
|
|
46
|
+
function entriesWithExt(dir, ext) {
|
|
47
|
+
if (!dir || !existsSync(dir)) return [];
|
|
48
|
+
try {
|
|
49
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
50
|
+
.filter((e) => !e.name.startsWith('.') && e.name.endsWith(ext) && e.name.length > ext.length)
|
|
51
|
+
.filter((e) => {
|
|
52
|
+
if (e.isFile()) return true;
|
|
53
|
+
if (!e.isSymbolicLink()) return false;
|
|
54
|
+
try { return statSync(join(dir, e.name)).isFile(); } catch { return false; } // dangling
|
|
55
|
+
})
|
|
56
|
+
.map((e) => e.name)
|
|
57
|
+
.sort();
|
|
58
|
+
} catch { return []; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Project scope first, then user scope — unless they are the same directory (a
|
|
62
|
+
// project that IS the home dir, plausible for the non-code harnesses), where one
|
|
63
|
+
// artifact must not be reported twice.
|
|
64
|
+
function bothScopes(cwd, home, forRoot) {
|
|
65
|
+
const project = forRoot(cwd);
|
|
66
|
+
const user = forRoot(home);
|
|
67
|
+
const same = project?.dir && user?.dir && resolve(project.dir) === resolve(user.dir);
|
|
68
|
+
return same ? [[project, 'project']] : [[project, 'project'], [user, 'user']];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function listAgents({ target, home, cwd = process.cwd() } = {}) {
|
|
72
|
+
if (!targetHasAgents(target)) return { supported: false, agents: [] };
|
|
73
|
+
const agents = [];
|
|
74
|
+
for (const [spec, scope] of bothScopes(cwd, home || homedir(), (r) => agentSpecFor(target, r))) {
|
|
75
|
+
if (!spec) continue;
|
|
76
|
+
for (const name of entriesWithExt(spec.dir, spec.ext)) {
|
|
77
|
+
agents.push({ id: name.slice(0, -spec.ext.length), scope, path: join(spec.dir, name) });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { supported: true, agents };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Installed skills, read from disk in both scopes. `skillExt` targets (cursor) store
|
|
84
|
+
// each skill as a single file; the rest use a directory per skill.
|
|
85
|
+
export function listSkills({ target, home, cwd = process.cwd() } = {}) {
|
|
86
|
+
const out = [];
|
|
87
|
+
for (const [paths, scope] of bothScopes(cwd, home || homedir(), (r) => {
|
|
88
|
+
try { const p = targetPaths(target, r, r); return { dir: p.root, probe: p.skillDir('__probe__') }; }
|
|
89
|
+
catch { return null; }
|
|
90
|
+
})) {
|
|
91
|
+
if (!paths?.dir || !existsSync(paths.dir)) continue;
|
|
92
|
+
const ext = basename(paths.probe).replace('__probe__', ''); // '' for dir-per-skill targets
|
|
93
|
+
try {
|
|
94
|
+
for (const e of readdirSync(paths.dir, { withFileTypes: true })) {
|
|
95
|
+
if (e.name.startsWith('.')) continue;
|
|
96
|
+
if (ext) { if (e.isFile() && e.name.endsWith(ext)) out.push({ id: e.name.slice(0, -ext.length), scope }); }
|
|
97
|
+
else if (e.isDirectory() && existsSync(join(paths.dir, e.name, 'SKILL.md'))) out.push({ id: e.name, scope });
|
|
98
|
+
}
|
|
99
|
+
} catch { /* unreadable scope → contributes nothing */ }
|
|
100
|
+
}
|
|
101
|
+
return out.sort((a, b) => a.id.localeCompare(b.id) || a.scope.localeCompare(b.scope));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Keep the discriminator: a description's value is its `NOT x (that is y)` boundary,
|
|
105
|
+
// so never cut it off. Trim the middle instead when the whole thing is long.
|
|
106
|
+
export function shortDesc(d, limit = 220) {
|
|
107
|
+
const s = String(d || '').replace(/\s+/g, ' ').trim();
|
|
108
|
+
if (s.length <= limit) return s;
|
|
109
|
+
const not = s.search(/\bNOT\b/);
|
|
110
|
+
if (not > 0) {
|
|
111
|
+
const tail = s.slice(not);
|
|
112
|
+
const head = s.slice(0, Math.max(0, limit - tail.length - 2));
|
|
113
|
+
if (head) return `${head.trim()}… ${tail}`;
|
|
114
|
+
}
|
|
115
|
+
return `${s.slice(0, limit - 1)}…`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// `full: true` adds catalog descriptions. Off by default because the catalog's
|
|
119
|
+
// descriptions are ~40 KB and `rsc catalog --available` already serves meaning-based
|
|
120
|
+
// matching — paying for the same bytes twice is the cost this harness keeps cutting.
|
|
121
|
+
export function capabilities({ target, home, cwd = process.cwd(), full = false } = {}) {
|
|
122
|
+
const manifest = loadManifest();
|
|
123
|
+
const byId = new Map(manifest.skills.map((s) => [s.id, s]));
|
|
124
|
+
const skills = listSkills({ target, home, cwd });
|
|
125
|
+
const installedIds = new Set(skills.map((s) => s.id));
|
|
126
|
+
const { supported, agents } = listAgents({ target, home, cwd });
|
|
127
|
+
return {
|
|
128
|
+
target,
|
|
129
|
+
agentsSupported: supported,
|
|
130
|
+
installed: skills.map((s) => ({ ...s, description: shortDesc(byId.get(s.id)?.description) })),
|
|
131
|
+
available: manifest.skills
|
|
132
|
+
.filter((s) => !installedIds.has(s.id))
|
|
133
|
+
.map((s) => (full ? { id: s.id, description: shortDesc(s.description) } : { id: s.id }))
|
|
134
|
+
.sort((a, b) => a.id.localeCompare(b.id)),
|
|
135
|
+
agents,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// --- the gap log ---------------------------------------------------------------
|
|
140
|
+
//
|
|
141
|
+
// Written through this function, never by a skill composing markdown by hand — a
|
|
142
|
+
// free-form writer drifts in format within a few sessions and then nothing can read
|
|
143
|
+
// it back. Nothing DOES read it back (by spec): its reader is the user, and doctor
|
|
144
|
+
// only counts entries.
|
|
145
|
+
//
|
|
146
|
+
// PRIVACY: `procedure` is the assistant's own description of what it observed doing.
|
|
147
|
+
// The user's literal request must never be passed here. This function enforces SHAPE
|
|
148
|
+
// ONLY — it cannot tell a description from a paraphrase, and it does not pretend to.
|
|
149
|
+
// The rule lives in skills/skill-scout/SKILL.md, where it is held by whoever writes.
|
|
150
|
+
|
|
151
|
+
export const GAP_VERDICTS = [
|
|
152
|
+
'covered-installed', 'covered-catalog', 'covered-agent',
|
|
153
|
+
'proposed-accepted', 'proposed-declined',
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
export function gapLogPath(cwd = process.cwd()) {
|
|
157
|
+
return join(cwd, '.rsc', 'automation-gaps.md');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Any whitespace run collapses, not just \n: a lone \r is a CommonMark line ending,
|
|
161
|
+
// so it would render as an extra (forged, dated) entry while `cat` hid it.
|
|
162
|
+
const oneLine = (s) => String(s || '').replace(/\s+/g, ' ').trim();
|
|
163
|
+
|
|
164
|
+
export function appendGap({ procedure, verdict, cwd = process.cwd(), now }) {
|
|
165
|
+
const text = oneLine(procedure);
|
|
166
|
+
if (!text) throw new Error('gap-log: a procedure description is required. Recover: pass --procedure "<what you observed doing>" — your own description of the work, never the user\'s words.');
|
|
167
|
+
if (!GAP_VERDICTS.includes(verdict)) {
|
|
168
|
+
throw new Error(`gap-log: verdict must be one of ${GAP_VERDICTS.join(', ')}. Recover: re-run with a valid --verdict.`);
|
|
169
|
+
}
|
|
170
|
+
const p = gapLogPath(cwd);
|
|
171
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
172
|
+
let needsHeader = true;
|
|
173
|
+
try { needsHeader = statSync(p).size === 0; } catch { needsHeader = true; }
|
|
174
|
+
const header = needsHeader
|
|
175
|
+
? '# Huecos de automatización\n\nProcedimientos que el asistente observó y qué pasó con ellos. Lo escribe `rsc capabilities gap-log`.\nDebe contener solo la descripción que el asistente hizo de su propio trabajo, no peticiones del usuario — es una regla que sostiene quien escribe, no algo que el código pueda comprobar. Fichero local: no lo commitees.\n\n'
|
|
176
|
+
: '';
|
|
177
|
+
const d = now || new Date();
|
|
178
|
+
const day = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
179
|
+
appendFileSync(p, `${header}- ${day} · ${text} · **${verdict}**\n`);
|
|
180
|
+
return p;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function countGaps(cwd = process.cwd()) {
|
|
184
|
+
try {
|
|
185
|
+
const p = gapLogPath(cwd);
|
|
186
|
+
if (!existsSync(p)) return 0;
|
|
187
|
+
return readFileSync(p, 'utf8').split('\n').filter((l) => /^- \d{4}-\d{2}-\d{2} · /.test(l)).length;
|
|
188
|
+
} catch { return 0; } // a malformed log must never take down `doctor`
|
|
189
|
+
}
|
package/scripts/rsc.js
CHANGED
|
@@ -327,6 +327,49 @@ async function main() {
|
|
|
327
327
|
say('Use: npx @ericrisco/rsc registry refresh | registry status');
|
|
328
328
|
return;
|
|
329
329
|
}
|
|
330
|
+
case 'capabilities': {
|
|
331
|
+
// "What do I already have that solves this?" — the deterministic step the
|
|
332
|
+
// automation-gap rule requires BEFORE anyone proposes creating a skill or an
|
|
333
|
+
// agent. Without this command that rule would rest on the model's memory.
|
|
334
|
+
const { capabilities, appendGap, countGaps } = await import('./lib/capabilities.js');
|
|
335
|
+
const { resolveRoot } = await import('../targets/sello.mjs');
|
|
336
|
+
// A string flag must not swallow the next FLAG as its value (`flag()` returns
|
|
337
|
+
// the following token blindly), or `--procedure --verdict x` records "--verdict".
|
|
338
|
+
const str = (name) => {
|
|
339
|
+
const v = flag(name);
|
|
340
|
+
return typeof v === 'string' && !v.startsWith('--') ? v : undefined;
|
|
341
|
+
};
|
|
342
|
+
// Subcommand by presence, not by position: `capabilities --target claude gap-log`
|
|
343
|
+
// used to fall through and silently record nothing.
|
|
344
|
+
if (argv.includes('gap-log')) {
|
|
345
|
+
// The writer and doctor must agree on the root, or a run from a subdirectory
|
|
346
|
+
// writes a log nothing counts.
|
|
347
|
+
const root = resolveRoot(process.cwd());
|
|
348
|
+
try {
|
|
349
|
+
const p = appendGap({ procedure: str('procedure'), verdict: str('verdict'), cwd: root });
|
|
350
|
+
say(`📝 recorded (${countGaps(root)} total): ${p}`);
|
|
351
|
+
} catch (e) { say(e.message); process.exitCode = 1; }
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const full = argv.includes('--full');
|
|
355
|
+
const reports = targets.map((t) => capabilities({ target: t, full }));
|
|
356
|
+
if (argv.includes('--json')) {
|
|
357
|
+
return void say(JSON.stringify(reports.length === 1 ? reports[0] : reports, null, 2));
|
|
358
|
+
}
|
|
359
|
+
for (const caps of reports) {
|
|
360
|
+
if (reports.length > 1) say(`# target: ${caps.target}`);
|
|
361
|
+
for (const s of caps.installed) say(`skill\t${s.id}\tinstalled-${s.scope}\t${s.description}`);
|
|
362
|
+
for (const s of caps.available) say(`skill\t${s.id}\tavailable${s.description ? `\t${s.description}` : ''}`);
|
|
363
|
+
if (caps.agentsSupported) {
|
|
364
|
+
for (const a of caps.agents) say(`agent\t${a.id}\t${a.scope}\t${a.path}`);
|
|
365
|
+
if (!caps.agents.length) say('# no agent files found (this target supports them)');
|
|
366
|
+
} else {
|
|
367
|
+
say(`# ${caps.target} has no file-based agents — the agent option does not apply here`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (!full) say('# catalog shown as ids; add --full for descriptions, or use `catalog --available` to match by meaning');
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
330
373
|
case 'sello': {
|
|
331
374
|
// Deterministic transitions of the sello (review receipt). The `review` skill
|
|
332
375
|
// orchestrates the lenses; every state change and every check runs HERE, token-free.
|
|
@@ -508,7 +551,8 @@ async function main() {
|
|
|
508
551
|
return void (await runPurge(argv.includes('--dry-run'), argv.includes('--with-docs')));
|
|
509
552
|
default:
|
|
510
553
|
say(`rsc: unknown command '${cmd}'.`);
|
|
511
|
-
say('Use: npx @ericrisco/rsc | add <id...> | install --profile <p> | consult "<text>" | list | audit | registry refresh | doctor | sync | sello <on|off|status|…> | backups | restore <id|latest> | upgrade | uninstall <id> | purge');
|
|
554
|
+
say('Use: npx @ericrisco/rsc | add <id...> | install --profile <p> | consult "<text>" | list | capabilities [--full|gap-log] | audit | registry refresh | doctor | sync | sello <on|off|status|…> | backups | restore <id|latest> | upgrade | uninstall <id> | purge');
|
|
555
|
+
process.exitCode = 1;
|
|
512
556
|
}
|
|
513
557
|
}
|
|
514
558
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: skill-scout
|
|
3
|
-
description: "Use when a specialized skill probably exists but is not loaded, when a task feels harder than it should because generic reasoning is doing a domain skill's job,
|
|
3
|
+
description: "Use when a specialized skill probably exists but is not loaded, when a task feels harder than it should because generic reasoning is doing a domain skill's job, when the work just delivered was a repeatable procedure worth turning into a skill or an agent, or when auditing coverage for holes. Resolves against what already exists — installed skills, catalog skills and agent files — then names the gap and emits the install command. NOT routing among skills you already have (that is `suggest`), NOT writing the artifact when none exists (that is `author-skill` for skills, `building-agents` for agents)."
|
|
4
4
|
tags: [skill-discovery, capability-gap, meta, recommendations, coverage-audit]
|
|
5
5
|
recommends: [suggest, author-skill, context-budget, continuous-learning, knowledge-ops]
|
|
6
6
|
origin: risco
|
|
@@ -8,9 +8,9 @@ origin: risco
|
|
|
8
8
|
|
|
9
9
|
# skill-scout — find the skill you don't have
|
|
10
10
|
|
|
11
|
-
You are the **gap detector** for the skills catalog. Before a task starts
|
|
11
|
+
You are the **gap detector** for the skills catalog. Before a task starts, in the middle of one that is dragging, or right after delivering work that turned out to be a procedure — you answer exactly one question: *is there a skill that should be handling this, and is it loaded?* You do **not** do the work. You name what to load, then get out of the way.
|
|
12
12
|
|
|
13
|
-
Every run ends in one of
|
|
13
|
+
Every run ends in one of the verdicts in the table below:
|
|
14
14
|
|
|
15
15
|
- **INSTALLED-elsewhere** — the skill exists in the catalog and on disk somewhere (user scope, another project, a plugin) but is not active *here*. → emit a scope-fix / install command.
|
|
16
16
|
- **MISSING** — the skill exists in the catalog but is not installed at all. → emit an install command and a one-line why.
|
|
@@ -26,11 +26,46 @@ Why this matters: skills load by **progressive disclosure**. At session start Cl
|
|
|
26
26
|
| Clear match, exists at user scope but not in this project (or vice versa) | INSTALLED-elsewhere | Emit the scope-fix (copy to the missing scope) + why. |
|
|
27
27
|
| 2–3 plausible candidates, none dominant | AMBIGUOUS | Name the top 2 with a one-line distinction; ask which fits. Do not guess. |
|
|
28
28
|
| Present skill already covers this task | NO GAP | Say so and hand to `../suggest/SKILL.md` (routing among present skills) — that is its job, not yours. |
|
|
29
|
+
| An installed **agent** already does this work | AGENT-COVERS | Name it, use it (delegate), propose nothing. Agents are listed by `npx @ericrisco/rsc capabilities`; they are invisible to the catalog, so a scan that skips them proposes duplicates. |
|
|
29
30
|
| Catalog match present but its description is too vague to ever fire | WEAK-DESCRIPTION | Treat as a gap; route to `../author-skill/SKILL.md` to fix the frontmatter. |
|
|
30
31
|
| No catalog id fits at all | NONEXISTENT | Route to `../author-skill/SKILL.md`. Do not fabricate an id. |
|
|
31
32
|
|
|
32
33
|
The branch you must never collapse: **MISSING vs NONEXISTENT.** Confusing them either sends the user to write a skill that already exists, or has them wait for an install command for a skill that was never authored.
|
|
33
34
|
|
|
35
|
+
## The automation gap — the trigger that fires *after* the work
|
|
36
|
+
|
|
37
|
+
Everything above is preventive: you scan *before* generic reasoning does a specialist's job. There is a second moment, and it is the opposite one — **the work is already done and delivered**, and only then you notice its shape:
|
|
38
|
+
|
|
39
|
+
> The last thing you did by hand was a **procedure**: several mostly-deterministic steps, a recognizable result, repeatable by someone else from a description, and plausibly needed again.
|
|
40
|
+
|
|
41
|
+
Then, in this order and never skipping ahead:
|
|
42
|
+
|
|
43
|
+
1. **Deliver first.** This never delays, conditions or replaces the work asked for. Raising it beforehand turns a simple request into a conversation about harness architecture.
|
|
44
|
+
2. **Enumerate what exists** — `npx @ericrisco/rsc capabilities` lists installed skills (with their scope), **agent files** and the catalog ids in one pass. Read it; do not recall it. Add `--full` for catalog descriptions, or use `catalog --available` when you need to match by meaning — the default stays cheap on purpose. If the command cannot run, **propose nothing** and say the check did not happen: never propose building on a check that did not occur.
|
|
45
|
+
3. **Something covers it** → use it (install with the usual one-word confirm if it is catalog-only), and **propose nothing**. Proposing to build what already exists is the worst outcome available here — it costs attention and it spends the credibility of every later suggestion.
|
|
46
|
+
4. **Nothing covers it** → one sentence at the end of the work: the procedure you saw, and whether it fits as a skill or an agent. Then stop; the user decides. Their "no" ends it for this procedure for the rest of the session.
|
|
47
|
+
5. **Record it either way** — `npx @ericrisco/rsc capabilities gap-log --procedure "<what YOU observed doing>" --verdict <covered-installed|covered-catalog|covered-agent|proposed-accepted|proposed-declined>`.
|
|
48
|
+
|
|
49
|
+
**Scale the sentence to the dial**, never whether it appears. Read the accompaniment level in `02-DOCS/wiki/harness/user-profile.md`: at L0/L1 it is one dry line ("this looked like a procedure — worth a skill?"); at L2/L3 add why it qualified and what the skill-vs-agent difference means for them. A profile that asked for brevity still gets the observation, just not the essay.
|
|
50
|
+
|
|
51
|
+
**The privacy boundary is yours to hold**, not the command's — it validates shape and nothing else, and it cannot tell your description from a paraphrase of the request. `--procedure` carries *your* account of the work you did. Never the user's words. The test: if the line could be reconstructed from what they typed, it does not belong in the log.
|
|
52
|
+
|
|
53
|
+
**Which log:** this one (`.rsc/automation-gaps.md`, verdicts `covered-*`/`proposed-*`) is for the after-the-work automation gap. The coverage-audit log below (`skill-gaps.jsonl`, verdicts `MISSING`/`NONEXISTENT`) is for a deliberate catalog audit. One event, one log — never both.
|
|
54
|
+
|
|
55
|
+
**Silence is a correct and common outcome.** A one-off, an exploration, a one-liner, or work that failed is not a procedure — a procedure is extracted from something that worked. There is no switch to turn this off, which is exactly why the bar is high: propose on everything and the feature becomes noise a user cannot escape.
|
|
56
|
+
|
|
57
|
+
### Skill or agent
|
|
58
|
+
|
|
59
|
+
Decided by the nature of the work, never its size:
|
|
60
|
+
|
|
61
|
+
| | Fits a **skill** | Fits an **agent** |
|
|
62
|
+
| --- | --- | --- |
|
|
63
|
+
| Shape | Knowledge + procedure that must **fire at the right moment**, in the conversation, user present | Work you want to **delegate**: its own context, possibly in parallel, possibly another model |
|
|
64
|
+
| Tell | "Whenever X comes up, do it this way" | "Go do X and come back with the result" |
|
|
65
|
+
| Build with | `../author-skill/SKILL.md` | `../building-agents/SKILL.md` |
|
|
66
|
+
|
|
67
|
+
When both fit, name the cheaper one to try and say the other is possible. When it is genuinely ambiguous, ask instead of choosing. On a target with no file-based agents (`capabilities` says so), the agent option does not exist — do not offer it.
|
|
68
|
+
|
|
34
69
|
## Symptoms that the task should have a skill
|
|
35
70
|
|
|
36
71
|
Watch for these. Any one is enough to run a scan:
|
|
@@ -129,5 +164,6 @@ One JSON object per line, append-only. `recommended_id` must be a real catalog i
|
|
|
129
164
|
|
|
130
165
|
- `../suggest/SKILL.md` — routing among the skills you already have. suggest is routing; you are procurement. If the right skill is already present, it's suggest's job, not yours.
|
|
131
166
|
- `../author-skill/SKILL.md` — building a new skill when none exists. You own the *exists / missing / nonexistent* decision; the moment the verdict is NONEXISTENT (or WEAK-DESCRIPTION), hand off here.
|
|
167
|
+
- `../building-agents/SKILL.md` — designing the agent when the gap is delegated work rather than in-conversation procedure (see *Skill or agent* above).
|
|
132
168
|
- `context-budget` — when the loaded set is too heavy for the window. That is token budgeting of what *is* loaded; orthogonal to finding what is *absent*. (Not yet on disk; do not link until it ships.)
|
|
133
169
|
- `continuous-learning` / `knowledge-ops` — when the pattern is a recurring *learning* to capture, not a missing skill to install. (Not yet on disk.)
|
package/skills/suggest/SKILL.md
CHANGED
|
@@ -71,6 +71,12 @@ and never to interrupt a flow with a nice-to-have. Never recommend something alr
|
|
|
71
71
|
nothing for natural-language or non-English intent. Never let it decide, and never read its silence
|
|
72
72
|
as "no skill exists" — the catalog plus your judgment is the source of truth.
|
|
73
73
|
|
|
74
|
+
### Automation gap — after the work
|
|
75
|
+
|
|
76
|
+
Delivered work a repeatable **procedure**? Before proposing to *build* anything, run
|
|
77
|
+
`npx @ericrisco/rsc capabilities`: covered by a skill or agent → use it, say nothing.
|
|
78
|
+
Not covered → one line, skill or agent. Rules: `skill-scout`.
|
|
79
|
+
|
|
74
80
|
---
|
|
75
81
|
|
|
76
82
|
## 3. First contact
|
package/targets/sello.mjs
CHANGED
|
@@ -35,8 +35,14 @@ import { spawnSync } from 'node:child_process';
|
|
|
35
35
|
// sello would mutate the change being sealed, so `freeze` → `approve` could never
|
|
36
36
|
// converge (a permanent, unrecoverable delivery lockout). Excluded whether the
|
|
37
37
|
// project tracks .rsc/ or not.
|
|
38
|
+
// Every tool-written journal under .rsc/ belongs here, not just the sello's own:
|
|
39
|
+
// `.rsc/` is a non-lowerable tier-2 risk class, so ANY file the harness writes
|
|
40
|
+
// automatically would escalate risk and invalidate an approved seal on every write.
|
|
41
|
+
// The automation-gap log is exactly that — it is appended after every piece of work,
|
|
42
|
+
// with no switch to stop it, so leaving it out made the lockout permanent.
|
|
38
43
|
export const SELLO_STATE_PATHS = [
|
|
39
44
|
'.rsc/sello.json', '.rsc/sello-config.json', '.rsc/sello-findings.md', '.rsc/sello-log.jsonl',
|
|
45
|
+
'.rsc/automation-gaps.md',
|
|
40
46
|
];
|
|
41
47
|
|
|
42
48
|
export function selloPaths(root) {
|