@ericrisco/rsc 1.1.6 → 1.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 +62 -4
- package/manifest.json +834 -2
- package/package.json +3 -2
- package/schema/ecc-originality.json +16 -0
- package/schema/ecc-originality.u64 +0 -0
- package/scripts/build-manifest.js +43 -3
- package/scripts/doctor.js +47 -1
- package/scripts/install-apply.js +148 -31
- package/scripts/lib/capabilities.js +21 -0
- package/scripts/lib/manifest-file.js +4 -1
- package/scripts/originality-check.js +137 -0
- package/scripts/rsc.js +122 -14
- package/targets/agent-catalog.js +220 -0
- package/targets/agent-sources.json +48 -0
- package/targets/agents.js +61 -13
- package/targets/commands.js +212 -0
- package/targets/memory.js +276 -0
- package/targets/opencode-memory-plugin.js +65 -0
- package/targets/session-memory-adapter.mjs +117 -0
- package/targets/session-memory-core.mjs +477 -0
- package/targets/session-memory.mjs +27 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync, readdirSync, readFileSync, statSync, writeFileSync,
|
|
3
|
+
} from 'node:fs';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { stackAgents } from '../targets/agent-catalog.js';
|
|
9
|
+
import { allAgentNames } from '../targets/agents.js';
|
|
10
|
+
import { resolveCommands } from '../targets/commands.js';
|
|
11
|
+
|
|
12
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
13
|
+
const META_PATH = join(ROOT, 'schema', 'ecc-originality.json');
|
|
14
|
+
const DATA_PATH = join(ROOT, 'schema', 'ecc-originality.u64');
|
|
15
|
+
const NGRAM_SIZE = 8;
|
|
16
|
+
const EXEMPTION_CATEGORIES = new Set(['name', 'error', 'tool-command', 'language-term']);
|
|
17
|
+
|
|
18
|
+
export function normalizeTokens(text) {
|
|
19
|
+
return String(text || '').normalize('NFKC').toLowerCase().match(/[\p{L}\p{N}_+.#/-]+/gu) || [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function fingerprint(tokens, offset, size = NGRAM_SIZE) {
|
|
23
|
+
return createHash('sha256').update(tokens.slice(offset, offset + size).join(' ')).digest('hex').slice(0, 16);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function fingerprintsOf(text, size = NGRAM_SIZE) {
|
|
27
|
+
const tokens = normalizeTokens(text);
|
|
28
|
+
const out = [];
|
|
29
|
+
for (let offset = 0; offset <= tokens.length - size; offset += 1) {
|
|
30
|
+
out.push({ fingerprint: fingerprint(tokens, offset, size), offset });
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function walk(directory) {
|
|
36
|
+
if (!existsSync(directory)) throw new Error(`originality: corpus directory missing: ${directory}`);
|
|
37
|
+
return readdirSync(directory).sort().flatMap((name) => {
|
|
38
|
+
const path = join(directory, name);
|
|
39
|
+
return statSync(path).isDirectory() ? walk(path) : [path];
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function catalogDocuments() {
|
|
44
|
+
const skillIds = readdirSync(join(ROOT, 'skills')).filter((id) => {
|
|
45
|
+
try { return statSync(join(ROOT, 'skills', id, 'SKILL.md')).isFile(); } catch { return false; }
|
|
46
|
+
});
|
|
47
|
+
const commands = resolveCommands({
|
|
48
|
+
target: 'cursor', skills: skillIds, agents: allAgentNames(), memoryMode: 'full',
|
|
49
|
+
});
|
|
50
|
+
return [
|
|
51
|
+
...stackAgents().map((agent) => ({ id: `agent:${agent.name}`, text: `${agent.desc}\n${agent.body}` })),
|
|
52
|
+
...commands.map((command) => ({ id: `command:${command.name}`, text: `${command.description}\n${command.body}` })),
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function fingerprintCorpus(metaPath = META_PATH, dataPath = DATA_PATH) {
|
|
57
|
+
const metadata = JSON.parse(readFileSync(metaPath, 'utf8'));
|
|
58
|
+
if (metadata.ngramSize !== NGRAM_SIZE) throw new Error(`originality: unsupported n-gram size ${metadata.ngramSize}`);
|
|
59
|
+
for (const exemption of metadata.exemptions || []) {
|
|
60
|
+
if (!EXEMPTION_CATEGORIES.has(exemption.category) || !/^[0-9a-f]{16}$/.test(exemption.fingerprint || '')) {
|
|
61
|
+
throw new Error('originality: every exemption needs a valid fingerprint and closed category');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const bytes = readFileSync(dataPath);
|
|
65
|
+
if (bytes.length % 8 !== 0 || bytes.length / 8 !== metadata.fingerprintCount) {
|
|
66
|
+
throw new Error('originality: fingerprint corpus length does not match its receipt');
|
|
67
|
+
}
|
|
68
|
+
const digest = createHash('sha256').update(bytes).digest('hex');
|
|
69
|
+
if (digest !== metadata.dataSha256) throw new Error('originality: fingerprint corpus failed its integrity receipt');
|
|
70
|
+
const fingerprints = new Set();
|
|
71
|
+
for (let offset = 0; offset < bytes.length; offset += 8) fingerprints.add(bytes.subarray(offset, offset + 8).toString('hex'));
|
|
72
|
+
return { metadata, fingerprints };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function checkCatalogOriginality({ corpus = fingerprintCorpus(), documents = catalogDocuments() } = {}) {
|
|
76
|
+
const exempt = new Set((corpus.metadata.exemptions || []).map((entry) => entry.fingerprint));
|
|
77
|
+
const matches = [];
|
|
78
|
+
for (const document of documents) {
|
|
79
|
+
for (const candidate of fingerprintsOf(document.text, corpus.metadata.ngramSize)) {
|
|
80
|
+
if (corpus.fingerprints.has(candidate.fingerprint) && !exempt.has(candidate.fingerprint)) {
|
|
81
|
+
matches.push({ document: document.id, tokenOffset: candidate.offset, fingerprint: candidate.fingerprint });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
ngramSize: corpus.metadata.ngramSize,
|
|
87
|
+
corpusRevision: corpus.metadata.revision,
|
|
88
|
+
documents: documents.length,
|
|
89
|
+
exemptions: exempt.size,
|
|
90
|
+
matches,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function refresh(corpusRoot) {
|
|
95
|
+
const files = ['agents', 'commands'].flatMap((directory) => walk(join(corpusRoot, directory)));
|
|
96
|
+
const hashes = new Set();
|
|
97
|
+
for (const path of files) {
|
|
98
|
+
for (const candidate of fingerprintsOf(readFileSync(path, 'utf8'))) hashes.add(candidate.fingerprint);
|
|
99
|
+
}
|
|
100
|
+
const sorted = [...hashes].sort();
|
|
101
|
+
const bytes = Buffer.alloc(sorted.length * 8);
|
|
102
|
+
sorted.forEach((hash, index) => Buffer.from(hash, 'hex').copy(bytes, index * 8));
|
|
103
|
+
writeFileSync(DATA_PATH, bytes);
|
|
104
|
+
const revision = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: corpusRoot, encoding: 'utf8' }).trim();
|
|
105
|
+
const metadata = {
|
|
106
|
+
schemaVersion: 1,
|
|
107
|
+
source: 'https://github.com/affaan-m/everything-claude-code',
|
|
108
|
+
revision,
|
|
109
|
+
generatedAt: new Date().toISOString().slice(0, 10),
|
|
110
|
+
included: ['agents/**', 'commands/**'],
|
|
111
|
+
ngramSize: NGRAM_SIZE,
|
|
112
|
+
normalization: 'NFKC lowercase; Unicode letters/numbers plus _+.#/-; whitespace joined',
|
|
113
|
+
fingerprintFormat: 'first 64 bits of SHA-256, sorted big-endian; collisions fail closed',
|
|
114
|
+
fingerprintCount: sorted.length,
|
|
115
|
+
dataSha256: createHash('sha256').update(bytes).digest('hex'),
|
|
116
|
+
exemptions: [],
|
|
117
|
+
};
|
|
118
|
+
writeFileSync(META_PATH, `${JSON.stringify(metadata, null, 2)}\n`);
|
|
119
|
+
return metadata;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function main() {
|
|
123
|
+
if (process.argv[2] === '--refresh') {
|
|
124
|
+
if (!process.argv[3]) throw new Error('Usage: node scripts/originality-check.js --refresh <ecc-checkout>');
|
|
125
|
+
const metadata = refresh(process.argv[3]);
|
|
126
|
+
console.log(`originality corpus refreshed (${metadata.fingerprintCount} fingerprints at ${metadata.revision})`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const result = checkCatalogOriginality();
|
|
130
|
+
if (result.matches.length) {
|
|
131
|
+
for (const match of result.matches) console.error(`${match.document}: shared ${result.ngramSize}-token fingerprint ${match.fingerprint} at token ${match.tokenOffset}`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
console.log(`originality OK (${result.documents} documents vs ${fingerprintCorpus().metadata.fingerprintCount} corpus fingerprints; ${result.exemptions} exemptions)`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (import.meta.url === `file://${process.argv[1]}`) main();
|
package/scripts/rsc.js
CHANGED
|
@@ -4,7 +4,8 @@ import { detectTarget, resolveTargets, TARGETS } from '../targets/index.js';
|
|
|
4
4
|
import { detectRepo } from './detect-repo.js';
|
|
5
5
|
import { rank } from './consult.js';
|
|
6
6
|
import { expandRecommends, toOutcomes, hasOutcome } from './lib/recommend.js';
|
|
7
|
-
import { applyInstall, listInstalled, uninstall, syncInstalled, purge, collisions } from './install-apply.js';
|
|
7
|
+
import { applyInstall, listInstalled, listInstalledAgents, listInstalledCommands, uninstall, syncInstalled, purge, collisions } from './install-apply.js';
|
|
8
|
+
import { stackAgentNames } from '../targets/agents.js';
|
|
8
9
|
import { doctor } from './doctor.js';
|
|
9
10
|
import { say, select, pickFrom, banner, confirm, isInteractive } from './lib/ui.js';
|
|
10
11
|
import { refreshRegistry, registryStatus } from './lib/registry.js';
|
|
@@ -20,6 +21,54 @@ const cmd = argv[0];
|
|
|
20
21
|
|
|
21
22
|
const targetLabel = (id) => TARGETS.find((t) => t.id === id)?.label || id;
|
|
22
23
|
|
|
24
|
+
function requestedIds(start = 1) {
|
|
25
|
+
const valueFlags = new Set(['--target', '--profile', '--without']);
|
|
26
|
+
const ids = [];
|
|
27
|
+
for (let i = start; i < argv.length; i++) {
|
|
28
|
+
if (valueFlags.has(argv[i])) { i++; continue; }
|
|
29
|
+
if (argv[i].startsWith('--')) continue;
|
|
30
|
+
ids.push(argv[i]);
|
|
31
|
+
}
|
|
32
|
+
return ids;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function editDistance(a, b) {
|
|
36
|
+
const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
37
|
+
for (let i = 1; i <= a.length; i++) {
|
|
38
|
+
let diagonal = prev[0];
|
|
39
|
+
prev[0] = i;
|
|
40
|
+
for (let j = 1; j <= b.length; j++) {
|
|
41
|
+
const above = prev[j];
|
|
42
|
+
prev[j] = Math.min(prev[j] + 1, prev[j - 1] + 1, diagonal + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
43
|
+
diagonal = above;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return prev[b.length];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function classifyRequested(ids) {
|
|
50
|
+
const skills = new Set(loadManifest().skills.map((skill) => skill.id));
|
|
51
|
+
const agents = new Set(stackAgentNames());
|
|
52
|
+
const out = { skills: [], agents: [], unknown: [] };
|
|
53
|
+
for (const id of ids) {
|
|
54
|
+
if (skills.has(id)) out.skills.push(id);
|
|
55
|
+
else if (agents.has(id)) out.agents.push(id);
|
|
56
|
+
else out.unknown.push(id);
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function reportUnknown(ids) {
|
|
62
|
+
if (!ids.length) return false;
|
|
63
|
+
const candidates = [...loadManifest().skills.map((skill) => skill.id), ...stackAgentNames()];
|
|
64
|
+
for (const id of ids) {
|
|
65
|
+
const close = [...candidates].sort((a, b) => editDistance(id, a) - editDistance(id, b) || a.localeCompare(b)).slice(0, 3);
|
|
66
|
+
console.error(`rsc: unknown skill or agent '${id}'. Closest: ${close.join(', ')}`);
|
|
67
|
+
}
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
23
72
|
// Commands that act ON one assistant. Only these stop when two are installed and no
|
|
24
73
|
// flag says which: `purge` sweeps every target by design, and the catalog/consult
|
|
25
74
|
// side barely touches one, so blocking them would be a regression, not a safeguard.
|
|
@@ -113,6 +162,8 @@ function printNextSteps(targets, ids) {
|
|
|
113
162
|
say('\n Add something by hand anytime: npx @ericrisco/rsc add <skill>');
|
|
114
163
|
say(' Browse the catalog / get picks: npx @ericrisco/rsc consult "whatever you need"');
|
|
115
164
|
say('────────────────────────────────────────────────────────');
|
|
165
|
+
if (targets.includes('codex')) say(' Codex: review and trust the project lifecycle hook once with `/hooks`; until then memory is reported as requiring trust.');
|
|
166
|
+
if (targets.includes('cursor')) say(' Cursor: startup memory is assisted because its sessionStart hook is fire-and-forget; the installed always-on rule performs the read-before-action fallback.');
|
|
116
167
|
printAgentHandoff();
|
|
117
168
|
}
|
|
118
169
|
|
|
@@ -268,15 +319,12 @@ async function main() {
|
|
|
268
319
|
case undefined:
|
|
269
320
|
return wizard(f ? targets : null);
|
|
270
321
|
case 'add': {
|
|
271
|
-
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
requested.push(argv[i]);
|
|
276
|
-
}
|
|
277
|
-
const ids = withDefaultSkillFloor(requested);
|
|
322
|
+
const requested = requestedIds();
|
|
323
|
+
const selected = classifyRequested(requested);
|
|
324
|
+
if (reportUnknown(selected.unknown)) return;
|
|
325
|
+
const ids = withDefaultSkillFloor(selected.skills);
|
|
278
326
|
if (!argv.includes('--force') && !(await guardCollisions(targets, ids))) return;
|
|
279
|
-
for (const t of targets) await applyInstall({ skillIds: ids, target: t });
|
|
327
|
+
for (const t of targets) await applyInstall({ skillIds: ids, agentIds: selected.agents, target: t });
|
|
280
328
|
say(`✅ Installed for ${targets.join(', ')}: ${requested.join(', ')}`);
|
|
281
329
|
return void say(' ↻ Reload/restart your assistant so the new skill activates.');
|
|
282
330
|
}
|
|
@@ -328,14 +376,66 @@ async function main() {
|
|
|
328
376
|
else say('\n(no harness wiki here — printed above only; run `harness` to keep a written record)');
|
|
329
377
|
return;
|
|
330
378
|
}
|
|
331
|
-
case 'list':
|
|
332
|
-
|
|
379
|
+
case 'list': {
|
|
380
|
+
const skills = listInstalled({ target }).map((id) => `skill\t${id}`);
|
|
381
|
+
const agents = listInstalledAgents({ target }).map((agent) => `agent\t${agent.id}\t${agent.source}${agent.skills.length ? `:${agent.skills.join(',')}` : ''}`);
|
|
382
|
+
const commands = listInstalledCommands({ target }).map((command) => `command\t${command.id}\t${command.path}`);
|
|
383
|
+
return void say([...skills, ...agents, ...commands].join('\n') || '(nothing installed)');
|
|
384
|
+
}
|
|
333
385
|
case 'doctor': {
|
|
334
386
|
const report = doctor({ target });
|
|
335
387
|
if (argv.includes('--json')) return void say(JSON.stringify(report, null, 2));
|
|
336
388
|
printContextBudget(report.contextBudget);
|
|
337
389
|
return void say(JSON.stringify({ ...report, contextBudget: undefined }, null, 2));
|
|
338
390
|
}
|
|
391
|
+
case 'memory': {
|
|
392
|
+
const sub = argv[1] || 'status';
|
|
393
|
+
const root = process.cwd();
|
|
394
|
+
if (sub === 'on' || sub === 'off') {
|
|
395
|
+
const { readManifest, writeManifest } = await import('./lib/manifest-file.js');
|
|
396
|
+
const current = readManifest(root) || { version: 1, targets: targets || [], skills: [], agents: [], ownSkills: [], optOuts: [] };
|
|
397
|
+
writeManifest(root, { ...current, memory: sub === 'off' ? false : { enabled: true } });
|
|
398
|
+
for (const t of targets) await syncInstalled({ target: t, cwd: root });
|
|
399
|
+
say(`rsc memory ${sub}: ${sub === 'on' ? 'enabled' : 'disabled'} for this project (${targets.join(', ') || 'declaration only'}).`);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const M = await import('../targets/session-memory-core.mjs');
|
|
403
|
+
if (sub === 'status') {
|
|
404
|
+
const reports = targets.map((t) => doctor({ target: t, cwd: root }).memory);
|
|
405
|
+
return void say(JSON.stringify(reports.length === 1 ? reports[0] : reports, null, 2));
|
|
406
|
+
}
|
|
407
|
+
if (sub === 'resume') {
|
|
408
|
+
const result = M.resume({ cwd: root });
|
|
409
|
+
return void say(argv.includes('--json') ? JSON.stringify(result, null, 2) : (result.context || '(no local continuation for this branch/worktree)'));
|
|
410
|
+
}
|
|
411
|
+
if (sub === 'save') {
|
|
412
|
+
const result = M.capture({
|
|
413
|
+
cwd: root,
|
|
414
|
+
sessionId: typeof flag('session') === 'string' ? flag('session') : `manual-${Date.now()}`,
|
|
415
|
+
target: target || 'manual',
|
|
416
|
+
event: 'save',
|
|
417
|
+
force: true,
|
|
418
|
+
});
|
|
419
|
+
return void say(JSON.stringify(result, null, 2));
|
|
420
|
+
}
|
|
421
|
+
if (sub === 'learn') {
|
|
422
|
+
const value = (name) => { const v = flag(name); return typeof v === 'string' && !v.startsWith('--') ? v : undefined; };
|
|
423
|
+
const result = M.learn({
|
|
424
|
+
cwd: root,
|
|
425
|
+
text: value('text'),
|
|
426
|
+
evidence: value('evidence'),
|
|
427
|
+
scope: value('scope') || 'project',
|
|
428
|
+
confidence: value('confidence'),
|
|
429
|
+
approved: argv.includes('--approve'),
|
|
430
|
+
});
|
|
431
|
+
if (!result.saved) process.exitCode = 1;
|
|
432
|
+
return void say(JSON.stringify(result, null, 2));
|
|
433
|
+
}
|
|
434
|
+
if (sub === 'metrics') return void say(JSON.stringify(M.metricsSummary({ cwd: root }), null, 2));
|
|
435
|
+
say('Use: npx @ericrisco/rsc memory on|off|status|save [--session id]|resume [--json]|learn --text "…" --evidence "…" --scope project|global --confidence 0..1 --approve|metrics');
|
|
436
|
+
process.exitCode = 1;
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
339
439
|
case 'sync': {
|
|
340
440
|
const dry = argv.includes('--dry-run');
|
|
341
441
|
for (const t of targets) {
|
|
@@ -446,6 +546,13 @@ async function main() {
|
|
|
446
546
|
} else {
|
|
447
547
|
say(`# ${caps.target} has no file-based agents — the agent option does not apply here`);
|
|
448
548
|
}
|
|
549
|
+
if (caps.commandsSupported) {
|
|
550
|
+
for (const command of caps.commands) say(`command\t${command.id}\tproject\t${command.path}`);
|
|
551
|
+
if (!caps.commands.length) say('# no managed command files found (this target supports them)');
|
|
552
|
+
} else {
|
|
553
|
+
say(`# ${caps.target} has no supported project command surface`);
|
|
554
|
+
}
|
|
555
|
+
say(`memory\t${caps.memory.mode}\t${caps.memory.status}\t${caps.memory.reason}`);
|
|
449
556
|
}
|
|
450
557
|
if (!full) say('# catalog shown as ids; add --full for descriptions, or use `catalog --available` to match by meaning');
|
|
451
558
|
return;
|
|
@@ -623,8 +730,9 @@ async function main() {
|
|
|
623
730
|
const dry = argv.includes('--dry-run');
|
|
624
731
|
// `uninstall --all` is an alias for a full purge.
|
|
625
732
|
if (argv.includes('--all')) return void (await runPurge(dry, argv.includes('--with-docs')));
|
|
626
|
-
const
|
|
627
|
-
|
|
733
|
+
const selected = classifyRequested(requestedIds());
|
|
734
|
+
if (reportUnknown(selected.unknown)) return;
|
|
735
|
+
const removed = await uninstall({ skillIds: selected.skills, agentIds: selected.agents, target, dryRun: dry });
|
|
628
736
|
return void say((dry ? 'Would remove:\n' : 'Removed:\n') + (removed.join('\n') || '(nothing)'));
|
|
629
737
|
}
|
|
630
738
|
case 'repair': {
|
|
@@ -652,7 +760,7 @@ async function main() {
|
|
|
652
760
|
return void (await runPurge(argv.includes('--dry-run'), argv.includes('--with-docs')));
|
|
653
761
|
default:
|
|
654
762
|
say(`rsc: unknown command '${cmd}'.`);
|
|
655
|
-
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 | repair | uninstall <id> | purge');
|
|
763
|
+
say('Use: npx @ericrisco/rsc | add <id...> | install --profile <p> | consult "<text>" | list | capabilities [--full|gap-log] | audit | registry refresh | doctor | sync | memory <on|off|status|save|resume|learn|metrics> | sello <on|off|status|…> | backups | restore <id|latest> | upgrade | repair | uninstall <id> | purge');
|
|
656
764
|
say('Any command takes --target <claude|codex|cursor|copilot|gemini|…> (comma-separate for several)');
|
|
657
765
|
say(' → without it, rsc uses the assistant already installed here; if two are, it asks instead of guessing.');
|
|
658
766
|
process.exitCode = 1;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
const REVIEW_TOOLS = Object.freeze(['read', 'search']);
|
|
2
|
+
const RESOLVER_TOOLS = Object.freeze(['read', 'search', 'edit', 'shell']);
|
|
3
|
+
|
|
4
|
+
const reviewerBody = ({ label, scope, probes, escalation = '' }) => `You are the ${label} reviewer. Review the requested diff or pull request; do not edit files.
|
|
5
|
+
|
|
6
|
+
Boundary: ${scope}
|
|
7
|
+
|
|
8
|
+
Work from changed lines outward just far enough to verify callers, imports, guards, and tests. Record an **attack list** even when every attack is clean. For each proposed finding, answer before reporting:
|
|
9
|
+
1. What exact changed line supports it?
|
|
10
|
+
2. What concrete input and state produce what wrong result?
|
|
11
|
+
3. Which caller, import, and relevant test did you inspect?
|
|
12
|
+
4. Why do existing guards not reduce the severity?
|
|
13
|
+
|
|
14
|
+
HIGH and CRITICAL findings require the exact line and the full failure path. If either is missing, lower the severity or omit the finding. Prefer a clean verdict with a useful attack list over speculative volume.
|
|
15
|
+
|
|
16
|
+
Stack attacks:
|
|
17
|
+
${probes.map((probe) => `- ${probe}`).join('\n')}
|
|
18
|
+
${escalation ? `\n${escalation}\n` : ''}
|
|
19
|
+
Return: scope, attack list, findings ordered by severity, and verdict. A finding names file:line, failure mode, evidence, and the smallest test that would expose it.`;
|
|
20
|
+
|
|
21
|
+
const resolverBody = ({ label, scope, signals, attempts = 3 }) => `You are the ${label} build resolver. Diagnose and repair one failing build with the smallest stack-correct change.
|
|
22
|
+
|
|
23
|
+
Boundary: ${scope}
|
|
24
|
+
|
|
25
|
+
1. Reproduce the named failure and preserve its exact command and first causal diagnostic.
|
|
26
|
+
2. Trace that diagnostic to configuration, dependency, generated code, compiler, linker, or runtime setup.
|
|
27
|
+
3. Change only what is needed for that cause; do not refactor, upgrade broadly, or clean unrelated warnings.
|
|
28
|
+
4. Re-run the narrow failing command, then the nearest relevant test/build gate.
|
|
29
|
+
5. Stop after ${attempts} unsuccessful repair attempts or as soon as the next step requires a refactor or product choice.
|
|
30
|
+
|
|
31
|
+
Signals to distinguish:
|
|
32
|
+
${signals.map((signal) => `- ${signal}`).join('\n')}
|
|
33
|
+
|
|
34
|
+
Return the root cause, commands run, files changed, proof of the result, and any remaining blocker.`;
|
|
35
|
+
|
|
36
|
+
const reviewer = ({ name, label, skills, not, probes, should, shouldNot, tier = 'balanced', escalation }) => ({
|
|
37
|
+
name,
|
|
38
|
+
role: 'reviewer',
|
|
39
|
+
tier,
|
|
40
|
+
tools: [...REVIEW_TOOLS],
|
|
41
|
+
skills,
|
|
42
|
+
desc: `${label} reviewer: inspects changed code for stack-specific failure modes and reports evidence without editing. NOT ${not}.`,
|
|
43
|
+
routing: { should, shouldNot },
|
|
44
|
+
body: reviewerBody({ label, scope: `Use for ${skills.join(' or ')} code. NOT ${not}.`, probes, escalation }),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const resolver = ({ name, label, skills, not, signals, should, shouldNot, tier = 'balanced' }) => ({
|
|
48
|
+
name,
|
|
49
|
+
role: 'build-resolver',
|
|
50
|
+
tier,
|
|
51
|
+
tools: [...RESOLVER_TOOLS],
|
|
52
|
+
skills,
|
|
53
|
+
desc: `${label} build resolver: reproduces a failing build and applies the minimum verified repair. NOT ${not}.`,
|
|
54
|
+
routing: { should, shouldNot },
|
|
55
|
+
body: resolverBody({ label, scope: `Use for ${skills.join(' or ')} build failures. NOT ${not}.`, signals }),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const REVIEWERS = [
|
|
59
|
+
reviewer({
|
|
60
|
+
name: 'django-reviewer', label: 'Django', skills: ['django'], not: 'general Python review or build repair',
|
|
61
|
+
probes: ['QuerySet evaluation, N+1 reads, and accidental full-table work.', 'Transaction boundaries, row locking, retries, and side effects inside atomic blocks.', 'Migration reversibility, historical models, nullable transitions, and deploy ordering.', 'Authorization at object/query boundaries rather than only at the view.'],
|
|
62
|
+
should: ['Review a Django ORM diff for query and transaction defects.'], shouldNot: ['Fix a failing Django migration command.'],
|
|
63
|
+
}),
|
|
64
|
+
reviewer({
|
|
65
|
+
name: 'java-reviewer', label: 'Java and Spring', skills: ['java', 'spring-boot'], not: 'Android/Kotlin review or generic build repair',
|
|
66
|
+
probes: ['Nullability and Optional contracts across public boundaries.', 'Equality, hashing, mutability, and collection aliasing.', 'Spring proxy boundaries: transactions, self-invocation, scopes, and configuration binding.', 'Thread ownership, blocking work, interruption, and resource closure.'],
|
|
67
|
+
should: ['Review a Spring transaction change.'], shouldNot: ['Repair a Gradle dependency resolution failure.'],
|
|
68
|
+
}),
|
|
69
|
+
reviewer({
|
|
70
|
+
name: 'kotlin-reviewer', label: 'Kotlin Android', skills: ['kotlin-android'], not: 'Java server review or Gradle repair',
|
|
71
|
+
probes: ['Coroutine scope ownership, cancellation, dispatcher choice, and exception propagation.', 'Lifecycle collection, recomposition stability, and state restoration.', 'Nullable/platform types and sealed-state exhaustiveness.', 'Context/activity retention and Android resource lifetime.'],
|
|
72
|
+
should: ['Review a coroutine-driven Android screen.'], shouldNot: ['Resolve an Android Gradle Plugin mismatch.'],
|
|
73
|
+
}),
|
|
74
|
+
reviewer({
|
|
75
|
+
name: 'flutter-reviewer', label: 'Flutter', skills: ['flutter'], not: 'native Android review or Flutter build repair',
|
|
76
|
+
probes: ['Widget identity, keys, rebuild scope, and stale BuildContext use.', 'Controller, subscription, isolate, and animation disposal.', 'Async mounted checks and state changes after teardown.', 'Platform-channel contracts and divergent iOS/Android behavior.'],
|
|
77
|
+
should: ['Review a Flutter widget state diff.'], shouldNot: ['Fix a CocoaPods build failure.'],
|
|
78
|
+
}),
|
|
79
|
+
reviewer({
|
|
80
|
+
name: 'react-reviewer', label: 'React and Next.js', skills: ['react', 'nextjs'], not: 'Vue review or frontend build repair',
|
|
81
|
+
probes: ['Render purity, hook dependency identity, stale closures, and effect cleanup.', 'Server/client boundaries, serialization, caching, and request-scoped data.', 'Concurrent rendering races, transitions, hydration, and optimistic rollback.', 'Keys, controlled state, accessibility semantics, and error/loading states.'],
|
|
82
|
+
should: ['Review a Next.js server/client boundary.'], shouldNot: ['Repair a webpack or Turbopack build.'],
|
|
83
|
+
}),
|
|
84
|
+
reviewer({
|
|
85
|
+
name: 'rust-reviewer', label: 'Rust', skills: ['rust'], not: 'C++ review or Cargo build repair',
|
|
86
|
+
probes: ['Ownership workarounds that hide lifetime or aliasing defects.', 'Unsafe blocks: stated invariant, pointer provenance, layout, and synchronization.', 'Error propagation, panic reachability, and poisoned/shared state.', 'Async cancellation, Send/Sync assumptions, locks across await, and task lifetime.'],
|
|
87
|
+
should: ['Review an unsafe Rust boundary.'], shouldNot: ['Fix a Cargo feature conflict.'],
|
|
88
|
+
}),
|
|
89
|
+
reviewer({
|
|
90
|
+
name: 'swift-reviewer', label: 'Swift iOS', skills: ['swift-ios'], not: 'Kotlin review or Xcode build repair',
|
|
91
|
+
probes: ['Actor isolation, Sendable claims, cancellation, and main-thread ownership.', 'Reference cycles, escaping closures, and resource lifetime.', 'Value/reference semantics, copy-on-write, and mutation through shared state.', 'View identity, navigation state, persistence, and platform availability.'],
|
|
92
|
+
should: ['Review Swift concurrency changes in an iOS app.'], shouldNot: ['Resolve a signing or linker failure.'],
|
|
93
|
+
}),
|
|
94
|
+
reviewer({
|
|
95
|
+
name: 'go-reviewer', label: 'Go', skills: ['go'], not: 'generic service review or Go build repair',
|
|
96
|
+
probes: ['Goroutine lifetime, cancellation ownership, channel closure, and races.', 'Error wrapping and identity where callers use errors.Is or errors.As.', 'Interface nil values, slice/map aliasing, loop capture, and deferred cleanup lifetime.', 'API and doc-comment contracts, zero values, receiver choice, and lock copying.'],
|
|
97
|
+
should: ['Review Go concurrency and error handling.'], shouldNot: ['Repair a missing Go module or compile error.'],
|
|
98
|
+
}),
|
|
99
|
+
reviewer({
|
|
100
|
+
name: 'cpp-reviewer', label: 'C++', skills: ['cpp'], not: 'Rust review or compiler/linker repair',
|
|
101
|
+
probes: ['Ownership expressed through RAII and any raw pointer whose lifetime is unstated.', 'Dangling views/references, moved-from values, invalidation, and destructor behavior.', 'Bounds, narrowing, signedness, undefined behavior, and exception guarantees.', 'Synchronization, data races, virtual destruction, and unsafe casts.'],
|
|
102
|
+
should: ['Review a C++ ownership or lifetime diff.'], shouldNot: ['Resolve a CMake or linker failure.'],
|
|
103
|
+
}),
|
|
104
|
+
reviewer({
|
|
105
|
+
name: 'vue-reviewer', label: 'Vue and Nuxt', skills: ['vue-nuxt'], not: 'React review or Vue build repair',
|
|
106
|
+
probes: ['Reactive identity, destructuring, watcher cleanup, and computed side effects.', 'SSR/client divergence, hydration, route middleware, and request-scoped state.', 'Composable lifetime, provide/inject contracts, and error/loading states.', 'Template keys, accessibility semantics, and unsafe rendered content.'],
|
|
107
|
+
should: ['Review a Nuxt SSR and hydration diff.'], shouldNot: ['Fix a Vite build.'],
|
|
108
|
+
}),
|
|
109
|
+
reviewer({
|
|
110
|
+
name: 'csharp-reviewer', label: 'C# and .NET', skills: ['csharp-dotnet'], not: 'Java review or dotnet build repair',
|
|
111
|
+
probes: ['Async cancellation, sync-over-async, disposal, and scoped service lifetime.', 'Nullable annotations versus runtime validation and serializer behavior.', 'LINQ query translation, repeated enumeration, tracking, and transaction scope.', 'Records/equality, mutable shared state, authorization, and configuration binding.'],
|
|
112
|
+
should: ['Review an ASP.NET or EF Core change.'], shouldNot: ['Repair a NuGet restore failure.'],
|
|
113
|
+
}),
|
|
114
|
+
reviewer({
|
|
115
|
+
name: 'php-reviewer', label: 'PHP and Laravel', skills: ['laravel', 'php'], not: 'Django review or Composer/build repair',
|
|
116
|
+
probes: ['Request validation, mass assignment, policy scope, and tenant isolation.', 'Eloquent loading, query count, transactions, and queued work after commit.', 'PHP type coercion, nullable boundaries, array shape assumptions, and error conversion.', 'Migration deploy ordering, cache/config behavior, and serialization.'],
|
|
117
|
+
should: ['Review Laravel authorization and ORM code.'], shouldNot: ['Resolve a Composer dependency error.'],
|
|
118
|
+
}),
|
|
119
|
+
reviewer({
|
|
120
|
+
name: 'typescript-reviewer', label: 'TypeScript', skills: ['typescript'], not: 'framework-specific UI review or TypeScript build repair',
|
|
121
|
+
probes: ['Runtime validation at boundaries where static types disappear.', 'Unsound assertions, any/unknown narrowing, variance, and optional-property semantics.', 'Promise ownership, rejection paths, cancellation, and event-listener cleanup.', 'Module/export contracts and type/value divergence across build targets.'],
|
|
122
|
+
should: ['Review a TypeScript library API diff.'], shouldNot: ['Fix tsconfig or bundler errors.'],
|
|
123
|
+
}),
|
|
124
|
+
reviewer({
|
|
125
|
+
name: 'fastapi-reviewer', label: 'FastAPI', skills: ['fastapi'], not: 'generic Python review or environment repair',
|
|
126
|
+
probes: ['Dependency lifetime and cleanup, override leakage, and request-scoped state.', 'Async blocking, cancellation, background task ownership, and exception mapping.', 'Pydantic input/output boundaries, partial updates, and response data exposure.', 'Authorization per resource, transaction completion, and OpenAPI contract drift.'],
|
|
127
|
+
should: ['Review a FastAPI endpoint and dependency change.'], shouldNot: ['Repair a Python package installation.'],
|
|
128
|
+
}),
|
|
129
|
+
reviewer({
|
|
130
|
+
name: 'python-reviewer', label: 'Python', skills: ['python'], not: 'FastAPI/Django-specific review or environment repair',
|
|
131
|
+
probes: ['Async tasks that are never awaited, cancellation loss, and TaskGroup failure propagation.', 'Mutable defaults, late binding, descriptor/protocol behavior, and truthiness edge cases.', 'Context-manager and iterator cleanup, exception chaining, and generator finalization.', 'Shared mutable state, import-time effects, typing/runtime gaps, and process/thread boundaries.'],
|
|
132
|
+
should: ['Review Python language and asyncio correctness.'], shouldNot: ['Review Django ORM behavior.'],
|
|
133
|
+
}),
|
|
134
|
+
reviewer({
|
|
135
|
+
name: 'postgres-reviewer', label: 'PostgreSQL', skills: ['postgresdb'], not: 'application ORM review or migration execution',
|
|
136
|
+
probes: ['Lock strength/order, long transactions, deadlocks, and concurrent DDL behavior.', 'Index/operator compatibility, selectivity, null semantics, and query-plan regressions.', 'RLS policy coverage, SECURITY DEFINER/search_path, grants, and tenant isolation.', 'Migration reversibility, constraint validation, backfill batching, and replication effects.'],
|
|
137
|
+
should: ['Review a PostgreSQL migration and policy diff.'], shouldNot: ['Run or repair application migrations.'],
|
|
138
|
+
}),
|
|
139
|
+
reviewer({
|
|
140
|
+
name: 'rag-reviewer', label: 'RAG', skills: ['rag'], not: 'general ML training review or retrieval implementation',
|
|
141
|
+
probes: ['Evaluation set leakage, temporal leakage, and mismatch between offline and live queries.', 'Chunk/metadata identity, permission filtering before retrieval, and deletion propagation.', 'Recall/rerank trade-offs, empty retrieval, citation grounding, and abstention.', 'Embedding/model/index version compatibility and rollback behavior.'],
|
|
142
|
+
should: ['Review a retrieval pipeline for leakage and grounding.'], shouldNot: ['Tune a neural training loop.'],
|
|
143
|
+
}),
|
|
144
|
+
reviewer({
|
|
145
|
+
name: 'mle-reviewer', label: 'production ML', skills: ['machine-learning'], not: 'notebook style review or model training implementation', tier: 'heavy',
|
|
146
|
+
probes: ['Temporal/entity leakage in splits and features available only after prediction time.', 'Training-serving schema skew, data contracts, null/category drift, and feature ownership.', 'Promotion gates that fail open, canary comparability, monitoring delay, and rollback without retraining.', 'Reproducible artifacts: data/code/model lineage, seeds, environment, and deterministic evaluation.'],
|
|
147
|
+
should: ['Review an ML promotion and rollback pipeline.'], shouldNot: ['Implement or tune the model.'],
|
|
148
|
+
}),
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
const RESOLVERS = [
|
|
152
|
+
resolver({ name: 'django-build-resolver', label: 'Django', skills: ['django'], not: 'ORM performance review or feature refactoring', signals: ['Migration graph conflicts versus unapplied migrations.', 'Settings/import errors versus database connectivity.', 'Static collection/template failures versus application tests.'], should: ['Fix a failing Django migrate/check command.'], shouldNot: ['Review an ORM diff for N+1 queries.'] }),
|
|
153
|
+
resolver({ name: 'java-build-resolver', label: 'Java and Spring', skills: ['java', 'spring-boot'], not: 'code review or broad dependency upgrades', signals: ['Compiler diagnostics versus annotation/generated-source failures.', 'Maven/Gradle resolution versus toolchain/JDK mismatch.', 'Spring context/bootstrap failures versus test assertion failures.'], should: ['Fix a Gradle/Maven/Spring build failure.'], shouldNot: ['Review transaction correctness.'] }),
|
|
154
|
+
resolver({ name: 'kotlin-build-resolver', label: 'Kotlin Android', skills: ['kotlin-android'], not: 'UI/coroutine review or project modernization', signals: ['Kotlin/AGP/Gradle/JDK compatibility.', 'KSP/KAPT/generated source failures.', 'Manifest/resource merge versus compiler diagnostics.'], should: ['Repair an Android Gradle build.'], shouldNot: ['Review Compose state.'] }),
|
|
155
|
+
resolver({ name: 'flutter-build-resolver', label: 'Flutter', skills: ['flutter'], not: 'widget review or package modernization', signals: ['Dart analyzer/compile errors versus generated-code drift.', 'CocoaPods/Xcode signing versus Android Gradle failures.', 'Plugin platform constraints versus app code failures.'], should: ['Repair a Flutter build failure.'], shouldNot: ['Review widget lifecycle.'] }),
|
|
156
|
+
resolver({ name: 'react-build-resolver', label: 'React and Next.js', skills: ['react', 'nextjs'], not: 'UI review or broad package upgrades', signals: ['Type/lint failure versus bundler/module resolution.', 'Server/client boundary or static rendering diagnostics.', 'Environment/config presence versus application logic.'], should: ['Repair a Next.js or React production build.'], shouldNot: ['Review hydration correctness.'] }),
|
|
157
|
+
resolver({ name: 'rust-build-resolver', label: 'Rust', skills: ['rust'], not: 'unsafe-code review or dependency modernization', signals: ['Borrow/type diagnostics versus feature unification.', 'Build-script/native library/linker failures.', 'Toolchain/edition mismatch versus source error.'], should: ['Repair a Cargo build failure.'], shouldNot: ['Review unsafe invariants.'] }),
|
|
158
|
+
resolver({ name: 'swift-build-resolver', label: 'Swift iOS', skills: ['swift-ios'], not: 'concurrency review or signing-policy changes', signals: ['Swift compiler/generic diagnostics versus package resolution.', 'Linker/module map/generated interface failure.', 'Simulator/device availability versus signing/provisioning.'], should: ['Repair an Xcode or SwiftPM build.'], shouldNot: ['Review actor isolation.'] }),
|
|
159
|
+
resolver({ name: 'go-build-resolver', label: 'Go', skills: ['go'], not: 'concurrency review or module-wide upgrades', signals: ['Compiler/type error versus generated code drift.', 'Module/version/sum failure versus private module credentials.', 'CGO/toolchain/linker failure versus test failure.'], should: ['Repair a go build failure.'], shouldNot: ['Review goroutine lifetime.'] }),
|
|
160
|
+
resolver({ name: 'cpp-build-resolver', label: 'C++', skills: ['cpp'], not: 'ownership review or build-system modernization', signals: ['Compiler diagnostic versus template instantiation cause.', 'CMake/configuration/generator failure versus source failure.', 'Undefined reference/ABI/library order versus runtime test failure.'], should: ['Repair a CMake/compiler/linker failure.'], shouldNot: ['Review C++ lifetime safety.'] }),
|
|
161
|
+
resolver({ name: 'pytorch-build-resolver', label: 'PyTorch', skills: ['machine-learning'], not: 'model-quality review or training redesign', signals: ['Python package/import mismatch versus compiled extension ABI.', 'CUDA/driver/toolkit/architecture incompatibility.', 'Distributed launcher/environment failure versus model code failure.'], should: ['Repair a PyTorch environment or extension build.'], shouldNot: ['Review data leakage or model promotion.'] }),
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
const SPEC_MINER = {
|
|
165
|
+
name: 'spec-miner',
|
|
166
|
+
role: 'spec-miner',
|
|
167
|
+
tier: 'heavy',
|
|
168
|
+
tools: ['read', 'search', 'edit'],
|
|
169
|
+
skills: [],
|
|
170
|
+
desc: 'Brownfield spec miner: extracts an anchored SDD draft from existing code under a bounded read budget. NOT feature planning or overwriting an approved spec.',
|
|
171
|
+
routing: {
|
|
172
|
+
should: ['Extract an initial SDD spec from an existing repository with no specs.'],
|
|
173
|
+
shouldNot: ['Design a new feature or amend an approved spec in place.'],
|
|
174
|
+
},
|
|
175
|
+
body: `You are the brownfield spec miner. Produce an **extracted draft**, never a claim that undocumented intent is approved.
|
|
176
|
+
|
|
177
|
+
Read at most 40 files or 20,000 lines. Start from entry points, public interfaces, tests, schemas, migrations, configuration, and user-facing documentation. For every inferred invariant, cite a stable repository anchor (symbol plus path; line only as a convenience). Separate observed behavior, inferred requirement, contradiction, and unknown.
|
|
178
|
+
|
|
179
|
+
Write a new SDD-shaped draft with problem, users, observable behavior, invariants, non-goals, acceptance criteria, source anchors, and open points. Mark status extracted-draft. If the intended slug already has an approved spec, do not edit it: propose a new slug or a revision artifact. Do not plan implementation or invent product intent absent from evidence.
|
|
180
|
+
|
|
181
|
+
Return the read budget used, artifacts inspected, confidence per requirement, and the draft path.`,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const STACK_AGENTS = Object.freeze([...REVIEWERS, ...RESOLVERS, SPEC_MINER]);
|
|
185
|
+
|
|
186
|
+
export const STACK_AGENT_BY_SKILL = Object.freeze(Object.fromEntries(
|
|
187
|
+
['django', 'java', 'spring-boot', 'kotlin-android', 'flutter', 'react', 'nextjs', 'rust', 'swift-ios', 'go', 'cpp', 'vue-nuxt', 'csharp-dotnet', 'laravel', 'php', 'typescript', 'fastapi', 'python', 'postgresdb', 'rag', 'machine-learning']
|
|
188
|
+
.map((skill) => [skill, STACK_AGENTS.filter((agent) => agent.skills.includes(skill)).map((agent) => agent.name)]),
|
|
189
|
+
));
|
|
190
|
+
|
|
191
|
+
export const stackAgents = () => STACK_AGENTS.map((agent) => ({ ...agent, tools: [...agent.tools], skills: [...agent.skills], routing: { should: [...agent.routing.should], shouldNot: [...agent.routing.shouldNot] } }));
|
|
192
|
+
export const stackAgentNames = () => STACK_AGENTS.map((agent) => agent.name);
|
|
193
|
+
export const stackAgentByName = (name) => STACK_AGENTS.find((agent) => agent.name === name);
|
|
194
|
+
|
|
195
|
+
export function resolveStackAgentNames(skillIds = [], explicitAgentIds = []) {
|
|
196
|
+
const names = new Set();
|
|
197
|
+
for (const skill of skillIds) for (const name of STACK_AGENT_BY_SKILL[skill] || []) names.add(name);
|
|
198
|
+
for (const name of explicitAgentIds) if (stackAgentByName(name)) names.add(name);
|
|
199
|
+
return [...names].sort();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function validateAgentCatalog(definitions = STACK_AGENTS) {
|
|
203
|
+
const errors = [];
|
|
204
|
+
const seen = new Set();
|
|
205
|
+
const allowedRoles = new Set(['reviewer', 'build-resolver', 'spec-miner']);
|
|
206
|
+
for (const agent of definitions) {
|
|
207
|
+
const prefix = agent?.name || '<unnamed>';
|
|
208
|
+
if (!agent?.name || seen.has(agent.name)) errors.push(`${prefix}: unique name required`);
|
|
209
|
+
seen.add(agent?.name);
|
|
210
|
+
if (!agent?.desc?.includes('NOT ')) errors.push(`${prefix}: description requires a NOT boundary`);
|
|
211
|
+
if (!allowedRoles.has(agent?.role)) errors.push(`${prefix}: invalid role`);
|
|
212
|
+
if (!['balanced', 'heavy'].includes(agent?.tier)) errors.push(`${prefix}: tier must be balanced or heavy`);
|
|
213
|
+
if (!agent?.routing?.should?.length || !agent?.routing?.shouldNot?.length) errors.push(`${prefix}: routing cases required`);
|
|
214
|
+
if (Buffer.byteLength(agent?.body || '', 'utf8') > 6500) errors.push(`${prefix}: body exceeds 6500 bytes`);
|
|
215
|
+
if (/everything[- ]claude|affaan|worldflow|\.claude\/plugins\/ecc/iu.test(`${agent?.desc || ''}\n${agent?.body || ''}`)) errors.push(`${prefix}: foreign project vocabulary`);
|
|
216
|
+
if (agent?.role === 'reviewer' && JSON.stringify(agent.tools) !== JSON.stringify(REVIEW_TOOLS)) errors.push(`${prefix}: reviewer tools must be read-only`);
|
|
217
|
+
if (agent?.role === 'build-resolver' && JSON.stringify(agent.tools) !== JSON.stringify(RESOLVER_TOOLS)) errors.push(`${prefix}: resolver tools must be bounded`);
|
|
218
|
+
}
|
|
219
|
+
return errors;
|
|
220
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"agent": "go-reviewer",
|
|
4
|
+
"researchedAt": "2026-09-02",
|
|
5
|
+
"sources": [
|
|
6
|
+
{
|
|
7
|
+
"url": "https://go.dev/wiki/CodeReviewComments",
|
|
8
|
+
"coverage": ["error identity and wrapping", "goroutine lifetime", "interface values", "receiver and API review"]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"url": "https://go.dev/doc/comment",
|
|
12
|
+
"coverage": ["public API comments", "package and symbol contracts"]
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"url": "https://go.dev/doc/contribute",
|
|
16
|
+
"coverage": ["review evidence and change scope"]
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"agent": "cpp-reviewer",
|
|
22
|
+
"researchedAt": "2026-09-02",
|
|
23
|
+
"sources": [
|
|
24
|
+
{
|
|
25
|
+
"url": "https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines.html",
|
|
26
|
+
"coverage": ["RAII and ownership", "lifetime and bounds safety", "concurrency", "exception guarantees"]
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"agent": "python-reviewer",
|
|
32
|
+
"researchedAt": "2026-09-02",
|
|
33
|
+
"sources": [
|
|
34
|
+
{
|
|
35
|
+
"url": "https://docs.python.org/3.14/library/asyncio-dev.html",
|
|
36
|
+
"coverage": ["un-awaited coroutines", "blocking async code", "thread boundaries"]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"url": "https://docs.python.org/3.14/library/asyncio-task.html",
|
|
40
|
+
"coverage": ["TaskGroup failure propagation", "cancellation", "task lifetime"]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"url": "https://docs.python.org/3.14/reference/datamodel.html",
|
|
44
|
+
"coverage": ["descriptors and protocols", "object and iterator lifetime"]
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
]
|