@ericrisco/rsc 2.0.0 → 2.0.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/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.0.0",
2
+ "version": "2.0.1",
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.0",
3
+ "version": "2.0.1",
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": {
package/scripts/doctor.js CHANGED
@@ -445,6 +445,18 @@ export function contextBudget({ target, home = homedir(), cwd = process.cwd() }
445
445
  });
446
446
  }
447
447
  const drift = divergence({ cwd, target, home });
448
+ // A separate finding, not a fourth clause of the one below: that one says this checkout
449
+ // is behind its own team, and the fix is to align with the declaration. This one says the
450
+ // declaration itself is behind the catalog, so aligning with it changes nothing. Folded
451
+ // together, the report would name the right skill and the wrong reason.
452
+ if (drift.floorMissing?.length) {
453
+ findings.push({
454
+ id: 'floor-missing',
455
+ severity: 'high',
456
+ summary: `.rsc.json predates a skill the catalog now requires: ${drift.floorMissing.join(', ')}. The always-on body routes work to it, so that route currently answers nowhere.`,
457
+ action: 'Run `npx @ericrisco/rsc sync` — it adds what the catalog requires and touches nothing else.',
458
+ });
459
+ }
448
460
  if (drift.missing.length || drift.extra.length || drift.ownMissing.length) {
449
461
  // The day-two case: a teammate changed the harness and this checkout has not caught
450
462
  // up. Reported always, even after someone declines to align — the divergence does not
@@ -10,6 +10,7 @@ import {
10
10
  resolveAgentNames, agentByName, allAgentNames, readDeveloperTier,
11
11
  } from '../targets/agents.js';
12
12
  import { readState, writeState } from './lib/state.js';
13
+ import { withDefaultSkillFloor } from './lib/default-skill-floor.js';
13
14
  import { readManifest, writeManifest } from './lib/manifest-file.js';
14
15
  import { createBackup } from './lib/backups.js';
15
16
  import {
@@ -534,9 +535,16 @@ export async function syncInstalled({ target, home, cwd = process.cwd(), dryRun
534
535
  // then rebuilt from a governed list that predated it and pruned the difference, recursively.
535
536
  // Reported by a user and reproduced 2026-09-17. The receipt stays untouched, because it is
536
537
  // hash-checked against what the user accepted and is not a log of what happened since.
537
- const declared = governedSkills
538
+ const declaredRaw = governedSkills
538
539
  ? [...new Set([...governedSkills, ...(manifest?.skills || [])])].sort()
539
540
  : (ids.length ? ids : (manifest?.skills || []));
541
+ // A declaration is frozen at the moment it was written, so a skill the catalog later makes
542
+ // mandatory never reaches an existing harness: `add` and `install` apply the floor, sync did
543
+ // not, and sync is the only one an upgrade runs. 2.0.0 shipped the three-lane decisor inside
544
+ // `suggest` — already declared, so already upgraded — routing ordinary work to an `ftd` that
545
+ // sync had no reason to fetch. Applied only to a harness that already declares something: a
546
+ // directory declaring nothing is not a harness missing its floor.
547
+ const declared = declaredRaw.length ? withDefaultSkillFloor(declaredRaw) : declaredRaw;
540
548
  const declaredAgents = state.explicitAgents?.length ? state.explicitAgents : (manifest?.agents || []);
541
549
  if (!declared.length && !declaredAgents.length) return dryRun ? { dryRun: true, synced: [], syncedAgents: [], paths: [] } : { synced: [], syncedAgents: [], backup: null };
542
550
  if (dryRun) {
@@ -4,7 +4,12 @@
4
4
  // `orient` and `suggest` are runtime always-on. `bro` is installed by default
5
5
  // but remains conditionally loaded: its description routes human-language asks
6
6
  // without paying for its body on unrelated turns.
7
- export const DEFAULT_SKILL_FLOOR = Object.freeze(['orient', 'suggest', 'bro']);
7
+ //
8
+ // `ftd` joined in 2.0.1 by the same criterion as `suggest`: the always-on decisor
9
+ // routes ordinary work to `../ftd/SKILL.md`, so a harness without it has a default
10
+ // lane that points at nothing. Membership of every profile was not enough — a
11
+ // declaration written before the skill existed never gains it (see `syncInstalled`).
12
+ export const DEFAULT_SKILL_FLOOR = Object.freeze(['orient', 'suggest', 'bro', 'ftd']);
8
13
 
9
14
  export function withDefaultSkillFloor(skillIds = []) {
10
15
  return [...new Set([...DEFAULT_SKILL_FLOOR, ...skillIds])];
@@ -2,6 +2,7 @@ import { readManifest } from './manifest-file.js';
2
2
  import { readState } from './state.js';
3
3
  import { targetPaths } from '../../targets/index.js';
4
4
  import { existsSync } from 'node:fs';
5
+ import { DEFAULT_SKILL_FLOOR } from './default-skill-floor.js';
5
6
 
6
7
  // What the team declared versus what is on this machine.
7
8
  //
@@ -15,10 +16,13 @@ import { existsSync } from 'node:fs';
15
16
  // ownMissing — declared team skill, absent from the repo. Aligning says so and writes
16
17
  // nothing: it comes from the repo, not from us.
17
18
  // extra — installed BY RSC, no longer declared.
19
+ // floorMissing — a skill the catalog makes mandatory that this declaration predates.
20
+ // Not drift between teammates: drift between the harness and the catalog,
21
+ // and the only bucket that survives everyone running the same `git pull`.
18
22
  // A hand-written skill that nobody declared is in none of them. Not declaring is a valid
19
23
  // way to keep things of your own, not an oversight to correct.
20
24
  export function divergence({ cwd = process.cwd(), target, home } = {}) {
21
- const empty = { missing: [], extra: [], ownMissing: [] };
25
+ const empty = { missing: [], extra: [], ownMissing: [], floorMissing: [] };
22
26
  const manifest = readManifest(cwd);
23
27
  if (!manifest) return empty;
24
28
 
@@ -29,5 +33,8 @@ export function divergence({ cwd = process.cwd(), target, home } = {}) {
29
33
  missing: (manifest.skills || []).filter((id) => !installed.has(id)),
30
34
  extra: [...installed].filter((id) => !(manifest.skills || []).includes(id)),
31
35
  ownMissing: (manifest.ownSkills || []).filter((name) => !existsSync(paths.skillDir(name))),
36
+ // Read off the declaration, not off the disk: a skill installed by hand into a harness
37
+ // that still does not declare it is one sync away from being pruned again.
38
+ floorMissing: DEFAULT_SKILL_FLOOR.filter((id) => !(manifest.skills || []).includes(id)),
32
39
  };
33
40
  }