@ericrisco/rsc 2.0.17 → 2.0.18

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
 
package/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.0.17",
2
+ "version": "2.0.18",
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.18",
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,10 @@
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…)"
100
104
  }
101
105
  ]
102
106
  }
@@ -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