@ericrisco/rsc 1.1.6 → 1.2.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ericrisco/rsc",
3
- "version": "1.1.6",
3
+ "version": "1.2.1",
4
4
  "description": "Eric Risco's agent-skills catalog as a granular, self-recommending CLI installer.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,10 +19,11 @@
19
19
  "knowledge:doctor": "node scripts/knowledge-doctor.js",
20
20
  "spec:gate": "node scripts/spec-gate.js",
21
21
  "routing:audit": "node scripts/routing-audit.js",
22
+ "originality:check": "node scripts/originality-check.js",
22
23
  "test": "node --test",
23
24
  "coverage": "c8 --reporter=text node --test",
24
25
  "version": "npm run manifest && git add manifest.json",
25
- "prepublishOnly": "npm run manifest && npm run validate && npm run drift:check"
26
+ "prepublishOnly": "npm run manifest && npm run validate && npm run drift:check && npm run originality:check"
26
27
  },
27
28
  "dependencies": {
28
29
  "@iarna/toml": "^2.2.5",
@@ -0,0 +1,16 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "source": "https://github.com/affaan-m/everything-claude-code",
4
+ "revision": "90430ab3a716e12a9c6770802efa352098735f24",
5
+ "generatedAt": "2026-09-02",
6
+ "included": [
7
+ "agents/**",
8
+ "commands/**"
9
+ ],
10
+ "ngramSize": 8,
11
+ "normalization": "NFKC lowercase; Unicode letters/numbers plus _+.#/-; whitespace joined",
12
+ "fingerprintFormat": "first 64 bits of SHA-256, sorted big-endian; collisions fail closed",
13
+ "fingerprintCount": 93337,
14
+ "dataSha256": "2025038e308e097c1d478c6eeeabf34dd35e6affa7abb20c3b31cf0fdde63463",
15
+ "exemptions": []
16
+ }
Binary file
@@ -4,6 +4,8 @@ import { fileURLToPath } from 'node:url';
4
4
  import Ajv from 'ajv';
5
5
  import { parseFrontmatter } from './lib/frontmatter.js';
6
6
  import { fenceBalance } from './lib/skill-lint.js';
7
+ import { allAgentNames, agentByName } from '../targets/agents.js';
8
+ import { fixedCommands, resolveCommands } from '../targets/commands.js';
7
9
 
8
10
  const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
9
11
  const SKILLS = join(ROOT, 'skills');
@@ -32,7 +34,45 @@ export function buildManifest() {
32
34
  profiles: fm.profiles || [],
33
35
  };
34
36
  }).sort((a, b) => a.id.localeCompare(b.id));
35
- return { version, counts: { skills: skills.length }, skills };
37
+ const agents = allAgentNames().map((id) => {
38
+ const agent = agentByName(id);
39
+ return {
40
+ id,
41
+ description: agent.desc,
42
+ role: agent.role || (id === 'developer' ? 'developer' : 'refuter'),
43
+ tier: agent.tier || 'balanced',
44
+ tools: agent.tools || null,
45
+ skills: agent.skills || [],
46
+ };
47
+ }).sort((a, b) => a.id.localeCompare(b.id));
48
+ // Cursor exposes every command kind, so resolving the complete catalog through
49
+ // that target gives the target-neutral public inventory: fixed entries plus all
50
+ // stack aliases, without introducing a second alias table.
51
+ const commands = resolveCommands({
52
+ target: 'cursor',
53
+ skills: ids,
54
+ agents: allAgentNames(),
55
+ memoryMode: 'full',
56
+ }).map((command) => ({
57
+ id: command.name,
58
+ description: command.description,
59
+ kind: command.kind,
60
+ backing: command.backing,
61
+ }));
62
+ const sourceReceipts = JSON.parse(readFileSync(join(ROOT, 'targets', 'agent-sources.json'), 'utf8'));
63
+ return {
64
+ version,
65
+ counts: {
66
+ skills: skills.length,
67
+ agents: agents.length,
68
+ commands: commands.length,
69
+ fixedCommands: fixedCommands().length,
70
+ },
71
+ skills,
72
+ agents,
73
+ commands,
74
+ sourceReceipts,
75
+ };
36
76
  }
37
77
 
38
78
  export function validateFrontmatter() {
@@ -102,11 +142,11 @@ function main() {
102
142
  let current = '';
103
143
  try { current = readFileSync(out, 'utf8'); } catch { /* missing */ }
104
144
  if (current !== json) { console.error('manifest.json is stale — run `npm run manifest`'); process.exit(1); }
105
- console.log(`manifest OK (${manifest.counts.skills} skills)`);
145
+ console.log(`manifest OK (${manifest.counts.skills} skills, ${manifest.counts.agents} agents, ${manifest.counts.commands} commands)`);
106
146
  return;
107
147
  }
108
148
  writeFileSync(out, json);
109
- console.log(`wrote manifest.json (${manifest.counts.skills} skills)`);
149
+ console.log(`wrote manifest.json (${manifest.counts.skills} skills, ${manifest.counts.agents} agents, ${manifest.counts.commands} commands)`);
110
150
  }
111
151
 
112
152
  if (import.meta.url === `file://${process.argv[1]}`) main();
package/scripts/doctor.js CHANGED
@@ -10,7 +10,11 @@ import { SDD_GATE_TEXT } from '../targets/hook-once.mjs';
10
10
  import { isEnabled, checkSello, readSello, countFindings, readEffectiveConfig, validateRiskConfig } from '../targets/sello.mjs';
11
11
  import { designIdentity } from './lib/design-identity.js';
12
12
  import { startingPointSummary } from './lib/starting-point.js';
13
- import { countGaps } from './lib/capabilities.js';
13
+ import { countGaps, listSkills, listAgents } from './lib/capabilities.js';
14
+ import { resolveCommands, commandPath, targetHasCommands } from '../targets/commands.js';
15
+ import { inspectMemoryWiring } from '../targets/memory.js';
16
+ import { metricsSummary } from '../targets/session-memory-core.mjs';
17
+ import { agentPath } from '../targets/agents.js';
14
18
 
15
19
  // The sello's health, surfaced where the user already looks (spec: non-blocking
16
20
  // findings live in the project and are SUMMARIZED here, never nagged about).
@@ -81,6 +85,42 @@ export function doctor({ target, home, cwd }) {
81
85
  const state = readState(paths.stateFile);
82
86
  const manifest = loadManifest();
83
87
  const backups = listBackups({ cwd: root });
88
+ const memory = inspectMemoryWiring(
89
+ target,
90
+ root,
91
+ state.memory,
92
+ Boolean(Object.keys(state.skills || {}).length || state.agents?.length || state.commands?.length),
93
+ );
94
+ if (state.memory && !['unsupported', 'disabled'].includes(memory.mode)) {
95
+ const hasStore = existsSync(join(root, '.rsc', 'memory', 'sessions'))
96
+ || existsSync(join(root, '02-DOCS', 'raw', 'worklog', '.rsc-memory', 'sessions'));
97
+ try {
98
+ memory.metrics = hasStore
99
+ ? metricsSummary({ cwd: root })
100
+ : { sessions: [], total: { cost: null, toolCalls: null }, knownTotal: { cost: 0, toolCalls: 0 }, unknown: { cost: 0, toolCalls: 0 } };
101
+ } catch { memory.metrics = { sessions: [], total: { cost: null, toolCalls: null }, knownTotal: { cost: 0, toolCalls: 0 }, unknown: { cost: 0, toolCalls: 0 } }; }
102
+ }
103
+ const actualSkills = listSkills({ target, home, cwd: root }).map((entry) => entry.id);
104
+ const actualAgents = listAgents({ target, home, cwd: root }).agents.map((entry) => entry.id);
105
+ const desiredCommands = new Set(resolveCommands({
106
+ target,
107
+ skills: actualSkills,
108
+ agents: actualAgents,
109
+ memoryMode: memory.status === 'ready' ? memory.mode : 'degraded',
110
+ }).map((command) => command.name));
111
+ const missingCommands = [];
112
+ const commandOrphans = [];
113
+ if (targetHasCommands(target)) {
114
+ for (const id of state.commands || []) {
115
+ const path = commandPath(target, root, id);
116
+ if (!path || !existsSync(path)) missingCommands.push({ id, path, action: 'Run `npx @ericrisco/rsc sync` to restore this managed command.' });
117
+ else if (!desiredCommands.has(id)) commandOrphans.push({ id, path, action: 'Restore its backing skill/agent with `rsc add`, or run `rsc sync` to reconcile it.' });
118
+ }
119
+ }
120
+ const missingAgents = (state.agents || []).filter((id) => {
121
+ const path = agentPath(target, root, id);
122
+ return path && !existsSync(path);
123
+ }).map((id) => ({ id, action: 'Run `npx @ericrisco/rsc sync` to restore this managed agent.' }));
84
124
  const report = {
85
125
  target,
86
126
  installed: Object.keys(state.skills),
@@ -108,6 +148,12 @@ export function doctor({ target, home, cwd }) {
108
148
  gitmojiGuard: existsSync(join(root, '.rsc', '.no-gitmoji')) ? 'opted-out' : 'armed',
109
149
  // Counted, never interpreted — by spec, the gap log's reader is the user.
110
150
  automationGaps: countGaps(root),
151
+ memory,
152
+ missingAgents,
153
+ agentCollisions: state.agentCollisions || [],
154
+ missingCommands,
155
+ commandOrphans,
156
+ commandCollisions: state.commandCollisions || [],
111
157
  };
112
158
  for (const [id, e] of Object.entries(state.skills)) {
113
159
  for (const f of e.files) if (!existsSync(f)) report.missing.push(`${id}:${f}`);
@@ -1,13 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  import { rmSync, existsSync, cpSync, mkdirSync, readFileSync, writeFileSync, appendFileSync, readdirSync } from 'node:fs';
3
- import { join, dirname, basename, relative, sep } from 'node:path';
3
+ import { join, dirname, relative, sep } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { planInstall } from './install-plan.js';
6
6
  import { targetPaths, writeSkill, wireHook, unwireHook, baseDir, TARGET_IDS } from '../targets/index.js';
7
- import { targetHasAgents, writeAgents, removeAgents, agentPath, agentNames } from '../targets/agents.js';
7
+ import {
8
+ targetHasAgents, reconcileAgents, agentPath, agentNames,
9
+ resolveAgentNames, agentByName, readDeveloperTier,
10
+ } from '../targets/agents.js';
8
11
  import { readState, writeState } from './lib/state.js';
9
12
  import { readManifest, writeManifest } from './lib/manifest-file.js';
10
13
  import { createBackup } from './lib/backups.js';
14
+ import {
15
+ targetHasCommands, resolveCommands, reconcileCommands, commandPath,
16
+ } from '../targets/commands.js';
17
+ import {
18
+ wireMemory, unwireMemory, memoryManagedPaths, memoryModeFor, memoryEnabledForProject, memoryArtifactsPresent,
19
+ } from '../targets/memory.js';
11
20
 
12
21
  const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
13
22
  const CLI_VERSION = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')).version;
@@ -68,11 +77,29 @@ function generatedHookFiles({ target, cwd }) {
68
77
  ];
69
78
  }
70
79
 
71
- export function managedPathsForInstall({ skillIds, target, home, cwd }) {
80
+ export function managedPathsForInstall({ skillIds, agentIds = [], target, home, cwd }) {
72
81
  const paths = targetPaths(target, home, cwd);
73
82
  const plan = planInstall({ skillIds, target, home, cwd });
74
83
  const out = [paths.stateFile, versionFile(cwd), baseVersionsFile(cwd)];
75
- if (targetHasAgents(target)) out.push(...agentNames().map((n) => agentPath(target, cwd, n)), join(cwd, '.rsc', 'developer.json'));
84
+ out.push(...memoryManagedPaths(target, cwd));
85
+ if (targetHasAgents(target)) {
86
+ const state = readState(paths.stateFile);
87
+ const explicit = [...new Set([...(state.explicitAgents || readManifest(cwd)?.agents || []), ...agentIds])];
88
+ const desired = resolveAgentNames([...Object.keys(state.skills || {}), ...skillIds], explicit);
89
+ out.push(...[...new Set([...(state.agents || []), ...desired])].map((n) => agentPath(target, cwd, n)), join(cwd, '.rsc', 'developer.json'));
90
+ }
91
+ if (targetHasCommands(target)) {
92
+ const state = readState(paths.stateFile);
93
+ const explicit = [...new Set([...(state.explicitAgents || readManifest(cwd)?.agents || []), ...agentIds])];
94
+ const desiredAgents = resolveAgentNames([...Object.keys(state.skills || {}), ...skillIds], explicit);
95
+ const desiredCommands = resolveCommands({
96
+ target,
97
+ skills: [...new Set([...Object.keys(state.skills || {}), ...skillIds])],
98
+ agents: desiredAgents,
99
+ memoryMode: memoryEnabledForProject(cwd) ? memoryModeFor(target) : 'disabled',
100
+ });
101
+ out.push(...[...new Set([...(state.commands || []), ...desiredCommands.map((command) => command.name)])].map((name) => commandPath(target, cwd, name)));
102
+ }
76
103
  for (const step of plan) {
77
104
  if (step.kind === 'skill') {
78
105
  out.push(step.to, baseDir(step.id, cwd));
@@ -102,8 +129,8 @@ function localDecisions(cwd) {
102
129
  // into codex on a machine that already had claude is adding, not replacing. Union, sorted
103
130
  // where order carries no meaning, so the file stays diffable and a merge conflict stays
104
131
  // readable.
105
- export function recordInManifest({ cwd, target, skillIds, catalogVersion = CLI_VERSION, dropTarget }) {
106
- const prev = readManifest(cwd) || { targets: [], skills: [], ownSkills: [], optOuts: [] };
132
+ export function recordInManifest({ cwd, target, skillIds, agentIds = [], catalogVersion = CLI_VERSION, dropTarget }) {
133
+ const prev = readManifest(cwd) || { targets: [], skills: [], agents: [], ownSkills: [], optOuts: [] };
107
134
  const { optOuts, tier } = localDecisions(cwd);
108
135
  const union = (a, b) => [...new Set([...(a || []), ...(b || [])])].sort();
109
136
  return writeManifest(cwd, {
@@ -113,18 +140,20 @@ export function recordInManifest({ cwd, target, skillIds, catalogVersion = CLI_V
113
140
  // have every clone rebuild a harness that was deliberately abandoned.
114
141
  targets: union(prev.targets, [target]).filter((t) => t !== dropTarget),
115
142
  skills: union(prev.skills, skillIds),
143
+ agents: union(prev.agents, agentIds),
116
144
  ownSkills: prev.ownSkills || [],
117
145
  catalogVersion,
118
146
  tier: tier ?? prev.tier ?? null,
119
147
  optOuts: optOuts.length ? optOuts : (prev.optOuts || []),
148
+ memory: prev.memory,
120
149
  });
121
150
  }
122
151
 
123
- export async function applyInstall({ skillIds, target, home, cwd = process.cwd(), operation = 'install', dryRun = false }) {
152
+ export async function applyInstall({ skillIds = [], agentIds = [], target, home, cwd = process.cwd(), operation = 'install', dryRun = false }) {
124
153
  const paths = targetPaths(target, home, cwd);
125
154
  const plan = planInstall({ skillIds, target, home, cwd });
126
- const managedPaths = managedPathsForInstall({ skillIds, target, home, cwd });
127
- if (dryRun) return { dryRun: true, skills: skillIds, paths: managedPaths };
155
+ const managedPaths = managedPathsForInstall({ skillIds, agentIds, target, home, cwd });
156
+ if (dryRun) return { dryRun: true, skills: skillIds, agents: agentIds, paths: managedPaths };
128
157
  const state = readState(paths.stateFile);
129
158
  const backup = createBackup({ cwd, operation, target, paths: managedPaths, cliVersion: CLI_VERSION });
130
159
  // Decide base refresh per skill (see baseVersionsFile): a base is re-materialized when
@@ -150,15 +179,34 @@ export async function applyInstall({ skillIds, target, home, cwd = process.cwd()
150
179
  //
151
180
  // The recorded names come from what was WRITTEN, not from a hardcoded list: a state entry
152
181
  // naming an agent whose file never landed answers "you have it" for something absent.
182
+ const explicit = [...new Set([...(state.explicitAgents || readManifest(cwd)?.agents || []), ...agentIds])].sort();
183
+ const desiredAgents = resolveAgentNames(Object.keys(state.skills || {}), explicit);
184
+ const previousAgents = state.agents || [];
153
185
  if (targetHasAgents(target)) {
154
- const written = writeAgents(target, cwd);
155
- state.agents = written.map((f) => basename(f).split('.')[0]);
186
+ const agentResult = reconcileAgents(target, cwd, readDeveloperTier(cwd), previousAgents, desiredAgents);
187
+ state.agents = agentResult.names;
188
+ state.agentCollisions = agentResult.collisions;
189
+ } else {
190
+ state.agents = [];
191
+ state.agentCollisions = [];
156
192
  }
193
+ state.explicitAgents = explicit;
194
+ const memoryResult = wireMemory(target, cwd);
195
+ state.memory = { mode: memoryResult.mode, reason: memoryResult.reason, paths: memoryResult.paths };
196
+ const desiredCommands = resolveCommands({
197
+ target,
198
+ skills: Object.keys(state.skills || {}),
199
+ agents: state.agents,
200
+ memoryMode: state.memory?.mode || 'unsupported',
201
+ });
202
+ const commandResult = reconcileCommands(target, cwd, state.commands || [], desiredCommands);
203
+ state.commands = commandResult.names;
204
+ state.commandCollisions = commandResult.collisions;
157
205
  state.version = CLI_VERSION;
158
206
  writeState(paths.stateFile, state);
159
207
  mkdirSync(dirname(versionFile(cwd)), { recursive: true });
160
208
  writeFileSync(versionFile(cwd), CLI_VERSION + '\n');
161
- recordInManifest({ cwd, target, skillIds });
209
+ recordInManifest({ cwd, target, skillIds, agentIds });
162
210
  ignoreLocalState(cwd, target);
163
211
  return { ...state, backup };
164
212
  }
@@ -196,7 +244,14 @@ export function ignoreLocalState(cwd = process.cwd(), target) {
196
244
  // repo. The declaration that does travel is .rsc.json, in the root.
197
245
  wanted.push(rel(paths.stateFile));
198
246
  // Subagents are catalog content, re-materialised by every install and sync.
199
- for (const f of agentNames()) wanted.push(rel(agentPath(target, cwd, f)));
247
+ for (const name of readState(paths.stateFile).agents || []) {
248
+ const file = agentPath(target, cwd, name);
249
+ if (file) wanted.push(rel(file));
250
+ }
251
+ for (const name of readState(paths.stateFile).commands || []) {
252
+ const file = commandPath(target, cwd, name);
253
+ if (file) wanted.push(rel(file));
254
+ }
200
255
  }
201
256
  // `.rsc`, `/.rsc` and `.rsc/` are the same rule to git. Normalising both ends is what
202
257
  // keeps this idempotent against a .gitignore a human wrote in their own spelling.
@@ -224,7 +279,26 @@ export function listInstalled({ target, home, cwd = process.cwd() }) {
224
279
  return Object.keys(readState(paths.stateFile).skills);
225
280
  }
226
281
 
227
- export async function uninstall({ skillIds, target, home, cwd = process.cwd(), dryRun }) {
282
+ export function listInstalledAgents({ target, home, cwd = process.cwd() }) {
283
+ const state = readState(targetPaths(target, home, cwd).stateFile);
284
+ const explicit = new Set(state.explicitAgents || []);
285
+ return (state.agents || []).map((id) => {
286
+ const agent = agentByName(id);
287
+ return {
288
+ id,
289
+ source: agentNames().includes(id) ? 'base' : explicit.has(id) ? 'explicit' : 'skill',
290
+ skills: agent?.skills || [],
291
+ };
292
+ });
293
+ }
294
+
295
+ export function listInstalledCommands({ target, home, cwd = process.cwd() }) {
296
+ const state = readState(targetPaths(target, home, cwd).stateFile);
297
+ return (state.commands || []).map((id) => ({ id, path: commandPath(target, cwd, id) }))
298
+ .filter((entry) => entry.path && existsSync(entry.path));
299
+ }
300
+
301
+ export async function uninstall({ skillIds = [], agentIds = [], target, home, cwd = process.cwd(), dryRun }) {
228
302
  const paths = targetPaths(target, home, cwd);
229
303
  const state = readState(paths.stateFile);
230
304
  const removed = [];
@@ -237,9 +311,19 @@ export async function uninstall({ skillIds, target, home, cwd = process.cwd(), d
237
311
  removed.push(f);
238
312
  }
239
313
  }
240
- if (!removed.length) return removed;
241
- if (dryRun) return removed;
242
- createBackup({ cwd, operation: 'uninstall', target, paths: managedPaths, cliVersion: CLI_VERSION });
314
+ const previousExplicit = state.explicitAgents || readManifest(cwd)?.agents || [];
315
+ const nextExplicit = previousExplicit.filter((id) => !agentIds.includes(id));
316
+ const remainingSkillIds = Object.keys(state.skills || {}).filter((id) => !skillIds.includes(id));
317
+ const desiredAgents = resolveAgentNames(remainingSkillIds, nextExplicit);
318
+ const staleAgents = (state.agents || []).filter((id) => !desiredAgents.includes(id));
319
+ for (const id of staleAgents) {
320
+ const file = agentPath(target, cwd, id);
321
+ if (file) { managedPaths.push(file); removed.push(file); }
322
+ }
323
+ const explicitChanged = nextExplicit.length !== previousExplicit.length;
324
+ if (!removed.length && !explicitChanged) return removed;
325
+ if (dryRun) return [...new Set(removed)];
326
+ createBackup({ cwd, operation: 'uninstall', target, paths: [...new Set(managedPaths)], cliVersion: CLI_VERSION });
243
327
  for (const id of skillIds) {
244
328
  const entry = state.skills[id];
245
329
  if (!entry) continue;
@@ -248,8 +332,28 @@ export async function uninstall({ skillIds, target, home, cwd = process.cwd(), d
248
332
  }
249
333
  delete state.skills[id];
250
334
  }
335
+ const agentResult = reconcileAgents(target, cwd, readDeveloperTier(cwd), state.agents || [], desiredAgents);
336
+ state.agents = agentResult.names;
337
+ state.agentCollisions = agentResult.collisions;
338
+ state.explicitAgents = nextExplicit;
339
+ const desiredCommands = resolveCommands({
340
+ target,
341
+ skills: remainingSkillIds,
342
+ agents: state.agents,
343
+ memoryMode: state.memory?.mode || 'unsupported',
344
+ });
345
+ const commandResult = reconcileCommands(target, cwd, state.commands || [], desiredCommands);
346
+ removed.push(...commandResult.removed);
347
+ state.commands = commandResult.names;
348
+ state.commandCollisions = commandResult.collisions;
251
349
  writeState(paths.stateFile, state);
252
- return removed;
350
+ const manifest = readManifest(cwd);
351
+ if (manifest) writeManifest(cwd, {
352
+ ...manifest,
353
+ skills: manifest.skills.filter((id) => !skillIds.includes(id)),
354
+ agents: manifest.agents.filter((id) => !agentIds.includes(id)),
355
+ });
356
+ return [...new Set(removed)];
253
357
  }
254
358
 
255
359
  export async function syncInstalled({ target, home, cwd = process.cwd(), dryRun = false }) {
@@ -260,17 +364,20 @@ export async function syncInstalled({ target, home, cwd = process.cwd(), dryRun
260
364
  // declares a harness and has none. The committed manifest is exactly the declaration
261
365
  // to rebuild from, and it is sitting right there.
262
366
  const ids = Object.keys(state.skills || {});
263
- const declared = ids.length ? ids : (readManifest(cwd)?.skills || []);
264
- if (!declared.length) return dryRun ? { dryRun: true, synced: [], paths: [] } : { synced: [], backup: null };
367
+ const manifest = readManifest(cwd);
368
+ const declared = ids.length ? ids : (manifest?.skills || []);
369
+ const declaredAgents = state.explicitAgents?.length ? state.explicitAgents : (manifest?.agents || []);
370
+ if (!declared.length && !declaredAgents.length) return dryRun ? { dryRun: true, synced: [], syncedAgents: [], paths: [] } : { synced: [], syncedAgents: [], backup: null };
265
371
  if (dryRun) {
266
372
  return {
267
373
  dryRun: true,
268
374
  synced: declared,
269
- paths: managedPathsForInstall({ skillIds: declared, target, home, cwd }),
375
+ syncedAgents: declaredAgents,
376
+ paths: managedPathsForInstall({ skillIds: declared, agentIds: declaredAgents, target, home, cwd }),
270
377
  };
271
378
  }
272
- const nextState = await applyInstall({ skillIds: declared, target, home, cwd, operation: 'sync' });
273
- return { synced: declared, backup: nextState.backup };
379
+ const nextState = await applyInstall({ skillIds: declared, agentIds: declaredAgents, target, home, cwd, operation: 'sync' });
380
+ return { synced: declared, syncedAgents: declaredAgents, backup: nextState.backup };
274
381
  }
275
382
 
276
383
  // Remove EVERYTHING rsc put in this project: installed skills across all targets,
@@ -293,20 +400,30 @@ export async function purge({ home, cwd = process.cwd(), withDocs = false, dryRu
293
400
  for (const id of Object.keys(state.skills || {})) {
294
401
  for (const f of state.skills[id].files || []) drop(f, true);
295
402
  }
403
+ for (const name of state.commands || []) {
404
+ const file = commandPath(target, cwd, name);
405
+ if (file) drop(file);
406
+ }
407
+ for (const name of state.agents || []) {
408
+ const file = agentPath(target, cwd, name);
409
+ if (file) drop(file);
410
+ }
296
411
  drop(paths.stateFile);
297
412
  }
298
- // unwireHook mutates files, so only run it for real (dry runs skip it).
299
- if (!dryRun) removed.push(...unwireHook(target, paths));
300
- // Remove the subagents this catalog installed — and only those. An uninstaller that takes
301
- // an agent the user wrote by hand is worse than one that leaves residue.
302
- for (const n of agentNames()) {
303
- const agentFile = agentPath(target, cwd, n);
304
- if (agentFile) drop(agentFile);
413
+ // Unwiring mutates shared config files, so only run it for real (dry runs report
414
+ // those files without touching them).
415
+ if (!dryRun) {
416
+ removed.push(...unwireHook(target, paths));
417
+ removed.push(...unwireMemory(target, cwd));
418
+ } else {
419
+ removed.push(...memoryArtifactsPresent(target, cwd));
305
420
  }
421
+ // A lost state file also loses proof of ownership. Leave same-named user files
422
+ // behind rather than guessing from a catalog id and deleting their work.
306
423
  }
307
424
  drop(join(cwd, '.rsc'), true);
308
425
  if (withDocs) drop(join(cwd, '02-DOCS'), true);
309
- return removed;
426
+ return [...new Set(removed)];
310
427
  }
311
428
 
312
429
  if (import.meta.url === `file://${process.argv[1]}`) {
@@ -25,6 +25,9 @@ import { homedir } from 'node:os';
25
25
  import { loadManifest } from './manifest.js';
26
26
  import { targetPaths } from '../../targets/index.js';
27
27
  import { developerAgentPath, targetHasAgents } from '../../targets/agents.js';
28
+ import { targetHasCommands, commandPath } from '../../targets/commands.js';
29
+ import { inspectMemoryWiring } from '../../targets/memory.js';
30
+ import { readState } from './state.js';
28
31
 
29
32
  // AGENT_TARGETS is not exported, so derive dir + extension from the one path helper
30
33
  // that owns it — duplicating that table is how two copies drift apart. The extension
@@ -101,6 +104,14 @@ export function listSkills({ target, home, cwd = process.cwd() } = {}) {
101
104
  return out.sort((a, b) => a.id.localeCompare(b.id) || a.scope.localeCompare(b.scope));
102
105
  }
103
106
 
107
+ export function listCommands({ target, cwd = process.cwd() } = {}) {
108
+ if (!targetHasCommands(target)) return { supported: false, commands: [] };
109
+ const state = readState(targetPaths(target, undefined, cwd).stateFile);
110
+ const commands = (state.commands || []).map((id) => ({ id, path: commandPath(target, cwd, id) }))
111
+ .filter((entry) => entry.path && existsSync(entry.path));
112
+ return { supported: true, commands };
113
+ }
114
+
104
115
  // Keep the discriminator: a description's value is its `NOT x (that is y)` boundary,
105
116
  // so never cut it off. Trim the middle instead when the whole thing is long.
106
117
  export function shortDesc(d, limit = 220) {
@@ -124,6 +135,8 @@ export function capabilities({ target, home, cwd = process.cwd(), full = false }
124
135
  const skills = listSkills({ target, home, cwd });
125
136
  const installedIds = new Set(skills.map((s) => s.id));
126
137
  const { supported, agents } = listAgents({ target, home, cwd });
138
+ const commandReport = listCommands({ target, cwd });
139
+ const state = readState(targetPaths(target, home, cwd).stateFile);
127
140
  return {
128
141
  target,
129
142
  agentsSupported: supported,
@@ -133,6 +146,14 @@ export function capabilities({ target, home, cwd = process.cwd(), full = false }
133
146
  .map((s) => (full ? { id: s.id, description: shortDesc(s.description) } : { id: s.id }))
134
147
  .sort((a, b) => a.id.localeCompare(b.id)),
135
148
  agents,
149
+ commandsSupported: commandReport.supported,
150
+ commands: commandReport.commands,
151
+ memory: inspectMemoryWiring(
152
+ target,
153
+ cwd,
154
+ state.memory,
155
+ Boolean(Object.keys(state.skills || {}).length || state.agents?.length || state.commands?.length),
156
+ ),
136
157
  };
137
158
  }
138
159
 
@@ -15,7 +15,7 @@ import { join } from 'node:path';
15
15
  export const MANIFEST = '.rsc.json';
16
16
  export const manifestPath = (cwd = process.cwd()) => join(cwd, MANIFEST);
17
17
 
18
- const KEYS = ['version', 'targets', 'skills', 'ownSkills', 'catalogVersion', 'tier', 'optOuts'];
18
+ const KEYS = ['version', 'targets', 'skills', 'agents', 'ownSkills', 'catalogVersion', 'tier', 'optOuts', 'memory'];
19
19
 
20
20
  export function readManifest(cwd = process.cwd()) {
21
21
  const file = manifestPath(cwd);
@@ -32,10 +32,12 @@ export function readManifest(cwd = process.cwd()) {
32
32
  version: raw.version ?? 1,
33
33
  targets: raw.targets || [],
34
34
  skills: raw.skills || [],
35
+ agents: raw.agents || [],
35
36
  ownSkills: raw.ownSkills || [],
36
37
  catalogVersion: raw.catalogVersion ?? null,
37
38
  tier: raw.tier ?? null,
38
39
  optOuts: raw.optOuts || [],
40
+ memory: raw.memory ?? undefined,
39
41
  };
40
42
  }
41
43
 
@@ -45,6 +47,7 @@ export function writeManifest(cwd, manifest) {
45
47
  const out = {};
46
48
  for (const k of KEYS) if (manifest[k] !== undefined) out[k] = manifest[k];
47
49
  out.version ??= 1;
50
+ out.agents ??= [];
48
51
  const ordered = {};
49
52
  for (const k of KEYS) if (out[k] !== undefined) ordered[k] = out[k];
50
53
  const file = manifestPath(cwd);