@ericrisco/rsc 2.0.18 → 2.0.20

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
@@ -276,7 +276,7 @@ rsc consult "I want to launch a SaaS" # recommend only, no install
276
276
  rsc registry refresh # write .rsc/skill-registry.{json,md}
277
277
  rsc list # installed skills, agents and commands
278
278
  rsc capabilities # installed/available surfaces + memory mode
279
- rsc doctor # health, missing backing, hooks and local memory
279
+ rsc doctor # health, missing backing, hooks and local memory; exits 1 when unhealthy
280
280
  rsc memory status # full / assisted / unsupported / degraded
281
281
  rsc memory save --session handoff # force a deterministic local checkpoint
282
282
  rsc memory resume # print this branch/worktree continuation
package/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.0.18",
2
+ "version": "2.0.20",
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.18",
3
+ "version": "2.0.20",
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": {
@@ -101,6 +101,14 @@
101
101
  {
102
102
  "fixedIn": "2.0.18",
103
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"
108
+ },
109
+ {
110
+ "fixedIn": "2.0.20",
111
+ "symptom": "In a project whose path contains a space, rsc doctor reported every hook script as missing and exited 1, though the scripts were there and the hooks ran"
104
112
  }
105
113
  ]
106
114
  }
package/scripts/doctor.js CHANGED
@@ -65,6 +65,10 @@ function selloStatus(root) {
65
65
  //
66
66
  // Hookless targets get an empty list by construction: their always-on surface is a
67
67
  // markdown block with no script behind it, so there is nothing that can go missing.
68
+ // A quoted span counts as a path only if it starts like one and holds no shell operator: a whole
69
+ // `sh -c '…'` snippet is not a path, and reading it as one reports a present script missing.
70
+ const SCRIPT_RE =
71
+ /"((?:[A-Za-z]:)?[\\/~][^"&;|]*[\\/]\.rsc[\\/][^"\s]+?\.mjs)"|'((?:[A-Za-z]:)?[\\/~][^'&;|]*[\\/]\.rsc[\\/][^'\s]+?\.mjs)'|([^"'\s]*[\\/]\.rsc[\\/][^"'\s]+\.mjs)/g;
68
72
  export function missingHookScripts({ target, home = homedir(), cwd = process.cwd() } = {}) {
69
73
  if (HOOKLESS_TARGETS.has(target)) return [];
70
74
  const file = targetPaths(target, home, cwd).hookTarget;
@@ -84,8 +88,10 @@ export function missingHookScripts({ target, home = homedir(), cwd = process.cwd
84
88
  const expand = (c) => c.split('${CLAUDE_PROJECT_DIR}').join(cwd);
85
89
  for (const raw of commands) {
86
90
  const cmd = expand(raw);
87
- for (const m of cmd.matchAll(/["']?([^"'\s]*[\\/]\.rsc[\\/][^"'\s]+\.mjs)["']?/g)) {
88
- const script = m[1];
91
+ // Quoted forms first: a project path may hold a space, and stopping at whitespace kept only
92
+ // the tail (`entreno/.rsc/x.mjs`), which then resolved against the cwd and read as missing (#283).
93
+ for (const m of cmd.matchAll(SCRIPT_RE)) {
94
+ const script = m[1] ?? m[2] ?? m[3];
89
95
  if (!existsSync(script)) seen.add(script);
90
96
  }
91
97
  }
@@ -201,7 +207,7 @@ export function doctor({ target, home, cwd }) {
201
207
  const missingAgents = (state.agents || []).filter((id) => {
202
208
  const path = agentPath(target, root, id);
203
209
  return path && !existsSync(path);
204
- }).map((id) => ({ id, action: 'Run `npx @ericrisco/rsc sync` to restore this managed agent.' }));
210
+ }).map((id) => ({ id, path: agentPath(target, root, id), action: 'Run `npx @ericrisco/rsc sync` to restore this managed agent.' }));
205
211
  const report = {
206
212
  target,
207
213
  installed: Object.keys(state.skills),
@@ -251,9 +257,12 @@ export function doctor({ target, home, cwd }) {
251
257
  commandOrphans,
252
258
  commandCollisions: state.commandCollisions || [],
253
259
  };
260
+ // #277 — the three "missing" lists share one shape, { id, path, action }, so whoever builds on top
261
+ // of doctor reads them the same way; and `healthy` is what the exit code says.
254
262
  for (const [id, e] of Object.entries(state.skills)) {
255
- for (const f of e.files) if (!existsSync(f)) report.missing.push(`${id}:${f}`);
263
+ 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
264
  }
265
+ report.healthy = report.hookWired && !report.missing.length && !missingAgents.length && !missingCommands.length;
257
266
  return report;
258
267
  }
259
268
 
package/scripts/rsc.js CHANGED
@@ -758,6 +758,8 @@ async function main() {
758
758
  }
759
759
  case 'doctor': {
760
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;
761
763
  if (argv.includes('--json')) return void say(JSON.stringify(report, null, 2));
762
764
  printContextBudget(report.contextBudget);
763
765
  return void say(JSON.stringify({ ...report, contextBudget: undefined }, null, 2));