@ericrisco/rsc 2.0.17 → 2.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -71,6 +71,9 @@ accompaniment, project kind and goal **before it writes anything**, reads only t
71
71
  root, and shows every selected or deferred skill, agent and hook with its reason. Relay those
72
72
  questions to the user; never invent or default their answers. The first pass writes nothing and
73
73
  prints a SHA-256 plan id. Only rerun with `--accept-plan <id>` after the user accepts that exact plan.
74
+ If the project already has a skill of its own with a catalog name, the plan lists it under
75
+ **Your own skills this plan replaces** — tell the user before accepting; renaming their folder and
76
+ regenerating the plan keeps theirs.
74
77
 
75
78
  For a non-interactive agent, collect the answers and preview the same plan explicitly:
76
79
 
@@ -273,7 +276,7 @@ rsc consult "I want to launch a SaaS" # recommend only, no install
273
276
  rsc registry refresh # write .rsc/skill-registry.{json,md}
274
277
  rsc list # installed skills, agents and commands
275
278
  rsc capabilities # installed/available surfaces + memory mode
276
- rsc doctor # health, missing backing, hooks and local memory
279
+ rsc doctor # health, missing backing, hooks and local memory; exits 1 when unhealthy
277
280
  rsc memory status # full / assisted / unsupported / degraded
278
281
  rsc memory save --session handoff # force a deterministic local checkpoint
279
282
  rsc memory resume # print this branch/worktree continuation
package/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.0.17",
2
+ "version": "2.0.19",
3
3
  "counts": {
4
4
  "skills": 273,
5
5
  "agents": 33,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ericrisco/rsc",
3
- "version": "2.0.17",
3
+ "version": "2.0.19",
4
4
  "description": "A guided harness builder that turns your goal into a minimal, verified AI-agent setup using strict best practices.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -97,6 +97,14 @@
97
97
  {
98
98
  "fixedIn": "2.0.17",
99
99
  "symptom": "Re-accepting an onboarding plan erased what init had learned about you, reset your explanation level, or dropped an assistant you added later"
100
+ },
101
+ {
102
+ "fixedIn": "2.0.18",
103
+ "symptom": "Onboarding silently replaced a skill you wrote yourself that had the same name as one of rsc's (review, plan, debug…)"
104
+ },
105
+ {
106
+ "fixedIn": "2.0.19",
107
+ "symptom": "rsc doctor reported missing pieces or an unwired hook but still exited with 0, so a CI or an editor extension could not tell"
100
108
  }
101
109
  ]
102
110
  }
package/scripts/doctor.js CHANGED
@@ -201,7 +201,7 @@ export function doctor({ target, home, cwd }) {
201
201
  const missingAgents = (state.agents || []).filter((id) => {
202
202
  const path = agentPath(target, root, id);
203
203
  return path && !existsSync(path);
204
- }).map((id) => ({ id, action: 'Run `npx @ericrisco/rsc sync` to restore this managed agent.' }));
204
+ }).map((id) => ({ id, path: agentPath(target, root, id), action: 'Run `npx @ericrisco/rsc sync` to restore this managed agent.' }));
205
205
  const report = {
206
206
  target,
207
207
  installed: Object.keys(state.skills),
@@ -251,9 +251,12 @@ export function doctor({ target, home, cwd }) {
251
251
  commandOrphans,
252
252
  commandCollisions: state.commandCollisions || [],
253
253
  };
254
+ // #277 — the three "missing" lists share one shape, { id, path, action }, so whoever builds on top
255
+ // of doctor reads them the same way; and `healthy` is what the exit code says.
254
256
  for (const [id, e] of Object.entries(state.skills)) {
255
- for (const f of e.files) if (!existsSync(f)) report.missing.push(`${id}:${f}`);
257
+ for (const f of e.files) if (!existsSync(f)) report.missing.push({ id, path: f, action: 'Run `npx @ericrisco/rsc sync` to restore this managed skill.' });
256
258
  }
259
+ report.healthy = report.hookWired && !report.missing.length && !missingAgents.length && !missingCommands.length;
257
260
  return report;
258
261
  }
259
262
 
@@ -422,6 +422,14 @@ export function collisions({ cwd = process.cwd(), target, home, skillIds = [] })
422
422
  return skillIds.filter((id) => !managed.has(id) && existsSync(paths.skillDir(id)));
423
423
  }
424
424
 
425
+ // The same test as `collisions`, across every target of a plan, as the paths the user would recognise.
426
+ // `onboard` puts these in the plan itself, so the id the user accepts is an id that names them.
427
+ export function ownSkillPaths({ cwd = process.cwd(), targets = [], skillIds = [], home }) {
428
+ const found = targets.flatMap((target) => collisions({ cwd, target, home, skillIds })
429
+ .map((id) => relative(cwd, targetPaths(target, home, cwd).skillDir(id)).split(sep).join('/')));
430
+ return [...new Set(found)].sort();
431
+ }
432
+
425
433
  export function listInstalled({ target, home, cwd = process.cwd() }) {
426
434
  const paths = targetPaths(target, home, cwd);
427
435
  return Object.keys(readState(paths.stateFile).skills);
@@ -40,6 +40,19 @@ export function createBackup({ cwd = process.cwd(), operation, target, paths, cl
40
40
  return manifest;
41
41
  }
42
42
 
43
+ // Where the newest snapshot kept a path, relative to the project — what to tell someone whose own work
44
+ // was just replaced, so recovering it is a copy they can find rather than a restore they never heard of.
45
+ // A transaction snapshot is the fallback: it is the install's rollback, not the user's copy.
46
+ export function whereKept({ cwd = process.cwd(), relPath }) {
47
+ const backups = listBackups({ cwd });
48
+ const ordered = [...backups.filter((b) => !/transaction/.test(b.operation)), ...backups.filter((b) => /transaction/.test(b.operation))];
49
+ for (const b of ordered) {
50
+ const kept = join(backupsDir(cwd), b.id, 'files', relPath);
51
+ if (existsSync(kept)) return relative(cwd, kept).split(sep).join('/');
52
+ }
53
+ return null;
54
+ }
55
+
43
56
  export function listBackups({ cwd = process.cwd() } = {}) {
44
57
  const dir = backupsDir(cwd);
45
58
  if (!existsSync(dir)) return [];
package/scripts/rsc.js CHANGED
@@ -4,14 +4,14 @@ import { detectTarget, installedTargets, resolveTargets, TARGETS } from '../targ
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, listInstalledAgents, listInstalledCommands, uninstall, syncInstalled, purge, collisions } from './install-apply.js';
7
+ import { applyInstall, listInstalled, listInstalledAgents, listInstalledCommands, uninstall, syncInstalled, purge, collisions, ownSkillPaths } from './install-apply.js';
8
8
  import { stackAgentNames } from '../targets/agents.js';
9
9
  import { doctor } from './doctor.js';
10
10
  import { ask, say, select, pickFrom, banner, confirm, isInteractive } from './lib/ui.js';
11
11
  import { refreshRegistry, registryStatus } from './lib/registry.js';
12
12
  import { audit, writeAuditReport } from './audit.js';
13
13
  import { DOMAINS } from './lib/domains.js';
14
- import { listBackups, restoreBackup } from './lib/backups.js';
14
+ import { listBackups, restoreBackup, whereKept } from './lib/backups.js';
15
15
  import { runUpgrade } from './lib/upgrade.js';
16
16
  import { diagnose, repair } from './lib/repair.js';
17
17
  import { DEFAULT_SKILL_FLOOR, withDefaultSkillFloor } from './lib/default-skill-floor.js';
@@ -142,6 +142,12 @@ function renderPlan(plan, planId) {
142
142
  for (const decision of plan.decisions.filter((d) => d.state === 'excluded')) say(` × ${decision.kind}/${decision.id} — ${decision.reason}`);
143
143
  say('Managed paths:');
144
144
  for (const path of plan.governedPaths) say(` ${path}`);
145
+ if (plan.replacesUserSkills?.length) {
146
+ say('Your own skills this plan replaces:');
147
+ for (const path of plan.replacesUserSkills) say(` ! ${path} — already here and not put by rsc; the catalog skill of the same name takes its place, and yours is kept in .rsc/backups/`);
148
+ say(' To keep yours, rename its folder (for example review → review-own) and generate the plan again.');
149
+ say(' AGENT: name each of these to the user before accepting, and ask whether to replace or rename.');
150
+ }
145
151
  if (plan.evidence.parentHarness) say(`Parent harness detected at ${plan.evidence.parentHarness}; it is not inherited by this plan.`);
146
152
  const pieces = [
147
153
  'npx @ericrisco/rsc@latest onboard',
@@ -201,6 +207,11 @@ async function runOnboarding(targets) {
201
207
  return;
202
208
  }
203
209
  const plan = buildOnboardingPlan(record, scanProject(process.cwd()));
210
+ // #275 — a folder where a catalog skill goes that rsc did not put is the user's own work. It goes
211
+ // INTO the plan, so the id they accept names it: an id made before that folder existed no longer
212
+ // applies, and accepting is never consent to something the plan did not show.
213
+ const replacesUserSkills = ownSkillPaths({ cwd: process.cwd(), targets: plan.policy.targets, skillIds: plan.policy.skills });
214
+ if (replacesUserSkills.length) plan.replacesUserSkills = replacesUserSkills;
204
215
  const planId = identifyPlan(plan);
205
216
  const accepted = flag('accept-plan');
206
217
  if (!accepted) {
@@ -229,6 +240,10 @@ async function runOnboarding(targets) {
229
240
  } catch (error) {
230
241
  say(`RSC_SKELETON_FAILED ${error.message}`);
231
242
  }
243
+ for (const path of plan.replacesUserSkills || []) {
244
+ const kept = whereKept({ cwd: process.cwd(), relPath: path });
245
+ if (kept) say(`Your previous ${path} is kept in ${kept}`);
246
+ }
232
247
  emitHarnessReadiness(plan, planId);
233
248
  }
234
249
 
@@ -743,6 +758,8 @@ async function main() {
743
758
  }
744
759
  case 'doctor': {
745
760
  const report = doctor({ target });
761
+ // #277 — an automation on top of rsc reads the exit code, not the prose.
762
+ if (!report.healthy) process.exitCode = 1;
746
763
  if (argv.includes('--json')) return void say(JSON.stringify(report, null, 2));
747
764
  printContextBudget(report.contextBudget);
748
765
  return void say(JSON.stringify({ ...report, contextBudget: undefined }, null, 2));