@ericrisco/rsc 1.2.3 → 1.3.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/scripts/rsc.js CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { loadManifest, skillsForProfile } from './lib/manifest.js';
3
- import { detectTarget, resolveTargets, TARGETS } from '../targets/index.js';
3
+ import { detectTarget, installedTargets, resolveTargets, TARGETS } from '../targets/index.js';
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
7
  import { applyInstall, listInstalled, listInstalledAgents, listInstalledCommands, uninstall, syncInstalled, purge, collisions } from './install-apply.js';
8
8
  import { stackAgentNames } from '../targets/agents.js';
9
9
  import { doctor } from './doctor.js';
10
- import { say, select, pickFrom, banner, confirm, isInteractive } from './lib/ui.js';
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';
@@ -15,9 +15,32 @@ import { listBackups, restoreBackup } 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';
18
+ import { readManifest, writeManifest } from './lib/manifest-file.js';
19
+ import {
20
+ normalizeOnboarding, missingOnboardingFields, scanProject,
21
+ buildOnboardingPlan, decodeGoal, encodeGoal, identifyPlan, recommendDeferredComponents,
22
+ } from './lib/onboarding.js';
23
+ import { applyAcceptedOnboarding, verifyOnboarding } from './lib/onboarding-apply.js';
18
24
 
19
- const argv = process.argv.slice(2);
20
- const cmd = argv[0];
25
+ const rawArgv = process.argv.slice(2);
26
+ const GLOBAL_VALUE_FLAGS = new Set([
27
+ '--target', '--technical-level', '--accompaniment', '--project-kind', '--goal', '--goal-base64', '--software-scope', '--accept-plan',
28
+ ]);
29
+ const COMMANDS = new Set(['onboard', 'reassess', 'add', 'install', 'consult', 'catalog', 'audit', 'list', 'doctor', 'memory', 'sync', 'backups', 'restore', 'upgrade', 'registry', 'worktrees', 'capabilities', 'sello', 'repair', 'uninstall', 'purge']);
30
+ function positionalTokens(input) {
31
+ const out = [];
32
+ for (let i = 0; i < input.length; i++) {
33
+ if (GLOBAL_VALUE_FLAGS.has(input[i]) || input[i] === '--profile' || input[i] === '--without') { i++; continue; }
34
+ if (input[i].startsWith('--')) continue;
35
+ out.push(input[i]);
36
+ }
37
+ return out;
38
+ }
39
+ const candidate = positionalTokens(rawArgv)[0];
40
+ const cmd = candidate || undefined;
41
+ const argv = cmd && COMMANDS.has(cmd)
42
+ ? [cmd, ...rawArgv.filter((_, index) => index !== rawArgv.indexOf(cmd))]
43
+ : rawArgv;
21
44
 
22
45
  const targetLabel = (id) => TARGETS.find((t) => t.id === id)?.label || id;
23
46
 
@@ -32,6 +55,184 @@ function requestedIds(start = 1) {
32
55
  return ids;
33
56
  }
34
57
 
58
+ function onboardingInput(targets) {
59
+ const value = (name) => {
60
+ const v = flag(name);
61
+ return typeof v === 'string' && !v.startsWith('--') ? v : undefined;
62
+ };
63
+ return {
64
+ schemaVersion: 1,
65
+ technicalLevel: value('technical-level'),
66
+ accompaniment: value('accompaniment'),
67
+ projectKind: value('project-kind'),
68
+ goal: value('goal') || (value('goal-base64') ? decodeGoal(value('goal-base64')) : undefined),
69
+ softwareScope: value('software-scope'),
70
+ targets,
71
+ };
72
+ }
73
+
74
+ function onboardingRequired(raw, action = 'onboard') {
75
+ const payload = {
76
+ code: 'RSC_ONBOARDING_REQUIRED',
77
+ schemaVersion: 1,
78
+ missing: missingOnboardingFields(raw),
79
+ recovery: `Run npx @ericrisco/rsc@latest ${action} --technical-level <non-technical|mixed|technical> --accompaniment <L0|L1|L2|L3> --project-kind <software|operations|research|content|mixed> --goal "<what you want>" --target <assistant>`,
80
+ };
81
+ console.error(`RSC_ONBOARDING_REQUIRED ${JSON.stringify(payload)}`);
82
+ process.exitCode = 2;
83
+ }
84
+
85
+ function hasDeclaredHarness(manifest = readManifest()) {
86
+ if (!manifest || manifest.version !== 1 || !Array.isArray(manifest.targets) || !manifest.targets.length
87
+ || !manifest.targets.every((target) => TARGETS.some((known) => known.id === target))
88
+ || !Array.isArray(manifest.skills) || !manifest.skills.length) return false;
89
+ const receipt = manifest.onboarding;
90
+ if (receipt) {
91
+ try {
92
+ const verified = receipt.schemaVersion === 1
93
+ && identifyPlan(receipt.plan) === receipt.acceptedPlanId
94
+ && verifyOnboarding(process.cwd(), receipt.plan, receipt.acceptedPlanId).length === 0;
95
+ if (verified) return true;
96
+ if (receipt.maintenanceDrift && installedTargets(process.cwd()).some((target) => manifest.targets.includes(target))) return true;
97
+ } catch { return false; }
98
+ }
99
+ // Backward compatibility is evidence-based: an actually installed pre-onboarding
100
+ // harness keeps its maintenance commands. A hand-written or merely committed
101
+ // declaration with no local state cannot bypass first contact.
102
+ return installedTargets(process.cwd()).some((target) => manifest.targets.includes(target));
103
+ }
104
+
105
+ function markMaintenanceDrift(action) {
106
+ const manifest = readManifest();
107
+ if (!manifest?.onboarding) return;
108
+ writeManifest(process.cwd(), {
109
+ ...manifest,
110
+ onboarding: {
111
+ ...manifest.onboarding,
112
+ maintenanceDrift: { action, recordedAt: new Date().toISOString(), requiresReassessment: true },
113
+ },
114
+ });
115
+ }
116
+
117
+ function renderPlan(plan, planId) {
118
+ say('RSC_ONBOARDING_PLAN');
119
+ say(`Plan id: ${planId}`);
120
+ say(`Project: ${plan.record.projectKind}${plan.record.softwareScope ? ` (${plan.record.softwareScope})` : ''}`);
121
+ say(`Targets: ${plan.policy.targets.join(', ')}`);
122
+ say('Selected:');
123
+ for (const decision of plan.decisions.filter((d) => d.state === 'selected')) say(` + ${decision.kind}/${decision.id} — ${decision.reason}`);
124
+ say('Deferred:');
125
+ for (const decision of plan.decisions.filter((d) => d.state === 'deferred')) say(` - ${decision.kind}/${decision.id} — ${decision.reason} Reevaluate: ${decision.reevaluateWhen.join('; ')}`);
126
+ say('Excluded:');
127
+ for (const decision of plan.decisions.filter((d) => d.state === 'excluded')) say(` × ${decision.kind}/${decision.id} — ${decision.reason}`);
128
+ say('Managed paths:');
129
+ for (const path of plan.governedPaths) say(` ${path}`);
130
+ if (plan.evidence.parentHarness) say(`Parent harness detected at ${plan.evidence.parentHarness}; it is not inherited by this plan.`);
131
+ const pieces = [
132
+ 'npx @ericrisco/rsc@latest onboard',
133
+ `--technical-level ${plan.record.technicalLevel}`,
134
+ `--accompaniment ${plan.record.accompaniment}`,
135
+ `--project-kind ${plan.record.projectKind}`,
136
+ `--goal-base64 ${encodeGoal(plan.record.goal)}`,
137
+ ...(plan.record.softwareScope ? [`--software-scope ${plan.record.softwareScope}`] : []),
138
+ `--target ${plan.record.targets.join(',')}`,
139
+ `--accept-plan ${planId}`,
140
+ ];
141
+ say(`Accept exactly this plan: ${pieces.join(' ')}`);
142
+ }
143
+
144
+ async function runOnboarding(targets) {
145
+ const raw = onboardingInput(targets);
146
+ if (isInteractive() && missingOnboardingFields(raw).length) {
147
+ await banner(loadManifest().counts.skills);
148
+ say('Before rsc writes anything, it will learn how to help and show the exact harness plan.');
149
+ raw.technicalLevel ||= await select('How technical should the conversation be?', [
150
+ { key: 'non-technical', label: 'Plain language — explain terms and avoid code jargon' },
151
+ { key: 'mixed', label: 'Mixed — concise explanations with useful technical detail' },
152
+ { key: 'technical', label: 'Technical — assume I am comfortable with code and tooling' },
153
+ ]);
154
+ raw.accompaniment ||= await select('How much accompaniment do you want?', [
155
+ { key: 'L0', label: 'L0 — results only' },
156
+ { key: 'L1', label: 'L1 — brief reasons, questions only when needed' },
157
+ { key: 'L2', label: 'L2 — explain each relevant decision' },
158
+ { key: 'L3', label: 'L3 — guide me through everything' },
159
+ ]);
160
+ raw.projectKind ||= await select('What are you building or running?', [
161
+ { key: 'software', label: 'Software or a website' },
162
+ { key: 'operations', label: 'Operations, a company or a process' },
163
+ { key: 'research', label: 'Research or a knowledge base' },
164
+ { key: 'content', label: 'Content or publishing' },
165
+ { key: 'mixed', label: 'A combination of these' },
166
+ ]);
167
+ raw.goal ||= await ask('What do you want this project to achieve? > ');
168
+ if ((raw.projectKind === 'software' || raw.projectKind === 'mixed') && !raw.softwareScope) {
169
+ raw.softwareScope = await select('How much software work is expected now?', [
170
+ { key: 'small', label: 'Small — one focused tool or a few simple changes' },
171
+ { key: 'growing', label: 'Growing — several related features or integrations' },
172
+ { key: 'complex', label: 'Complex — multiple systems, teams or critical behavior' },
173
+ ]);
174
+ }
175
+ if (!raw.targets?.length) raw.targets = await selectAgents();
176
+ if (!raw.technicalLevel || !raw.accompaniment || !raw.projectKind || !raw.goal || !raw.targets?.length) {
177
+ say('Cancelled — nothing was touched.');
178
+ return;
179
+ }
180
+ }
181
+ if (missingOnboardingFields(raw).length) return onboardingRequired(raw);
182
+ let record;
183
+ try { record = normalizeOnboarding(raw); } catch (error) {
184
+ console.error(`RSC_ONBOARDING_INVALID: ${error.message}`);
185
+ process.exitCode = 2;
186
+ return;
187
+ }
188
+ const plan = buildOnboardingPlan(record, scanProject(process.cwd()));
189
+ const planId = identifyPlan(plan);
190
+ const accepted = flag('accept-plan');
191
+ if (!accepted) {
192
+ renderPlan(plan, planId);
193
+ if (!isInteractive()) return;
194
+ if (!(await confirm('Accept this exact harness plan?'))) return void say('Cancelled — nothing was touched.');
195
+ }
196
+ if (accepted && accepted !== planId) {
197
+ console.error(`RSC_PLAN_CHANGED: accepted ${accepted}, current plan is ${planId}. Regenerate the plan and ask the user to accept the new id.`);
198
+ process.exitCode = 3;
199
+ return;
200
+ }
201
+ try {
202
+ await applyAcceptedOnboarding({ cwd: process.cwd(), plan, planId });
203
+ say(`RSC_ONBOARDING_READY ${planId}`);
204
+ } catch (error) {
205
+ console.error(error.message);
206
+ process.exitCode = 4;
207
+ }
208
+ }
209
+
210
+ function runReassessment() {
211
+ const manifest = readManifest();
212
+ const plan = manifest?.onboarding?.plan;
213
+ if (!plan) {
214
+ console.error('RSC_ONBOARDING_REQUIRED: no accepted onboarding receipt. Run npx @ericrisco/rsc@latest onboard');
215
+ process.exitCode = 2;
216
+ return;
217
+ }
218
+ const recommendations = recommendDeferredComponents(plan, scanProject(process.cwd()));
219
+ if (!recommendations.length) return void say('RSC_REASSESSMENT_NO_CHANGE');
220
+ say('RSC_REASSESSMENT_RECOMMENDED');
221
+ for (const item of recommendations) say(` ${item.kind}/${item.id}: ${item.explanation}`);
222
+ const record = recommendations[0].suggestedRecord || plan.record;
223
+ say('Review a new plan; nothing has been installed:');
224
+ say([
225
+ 'npx @ericrisco/rsc@latest onboard',
226
+ `--technical-level ${record.technicalLevel}`,
227
+ `--accompaniment ${record.accompaniment}`,
228
+ `--project-kind ${record.projectKind}`,
229
+ `--goal-base64 ${encodeGoal(record.goal)}`,
230
+ ...(record.softwareScope ? [`--software-scope ${record.softwareScope}`] : []),
231
+ `--target ${record.targets.join(',')}`,
232
+ ].join(' '));
233
+ say('Show the new plan and ask the user to accept its id before applying it.');
234
+ }
235
+
35
236
  function editDistance(a, b) {
36
237
  const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
37
238
  for (let i = 1; i <= a.length; i++) {
@@ -73,7 +274,7 @@ function reportUnknown(ids) {
73
274
  // flag says which: `purge` sweeps every target by design, and the catalog/consult
74
275
  // side barely touches one, so blocking them would be a regression, not a safeguard.
75
276
  const NEEDS_TARGET = new Set([
76
- 'add', 'install', 'list', 'doctor', 'sync', 'uninstall', 'capabilities', 'catalog', 'registry',
277
+ 'onboard', 'add', 'install', 'list', 'doctor', 'sync', 'uninstall', 'capabilities', 'catalog', 'registry',
77
278
  ]);
78
279
 
79
280
  function flag(name) {
@@ -317,24 +518,38 @@ async function main() {
317
518
  const target = targets[0];
318
519
  switch (cmd) {
319
520
  case undefined:
320
- return wizard(f ? targets : null);
521
+ return hasDeclaredHarness() ? wizard(f ? targets : null) : runOnboarding(f ? targets : []);
522
+ case 'onboard':
523
+ return runOnboarding(f ? targets : []);
524
+ case 'reassess':
525
+ return runReassessment();
321
526
  case 'add': {
527
+ if (!hasDeclaredHarness()) return onboardingRequired(onboardingInput(targets));
322
528
  const requested = requestedIds();
323
529
  const selected = classifyRequested(requested);
324
530
  if (reportUnknown(selected.unknown)) return;
325
531
  const ids = withDefaultSkillFloor(selected.skills);
326
532
  if (!argv.includes('--force') && !(await guardCollisions(targets, ids))) return;
327
- for (const t of targets) await applyInstall({ skillIds: ids, agentIds: selected.agents, target: t });
533
+ const currentManifest = readManifest();
534
+ const receipt = currentManifest?.onboarding;
535
+ const maintainedIds = receipt ? [...new Set([...(currentManifest.skills || []), ...(receipt.plan.policy.skills || []), ...ids])].sort() : ids;
536
+ const policy = receipt ? { ...receipt.plan.policy, skills: maintainedIds } : undefined;
537
+ for (const t of targets) await applyInstall({ skillIds: maintainedIds, agentIds: selected.agents, target: t, policy });
538
+ markMaintenanceDrift(`add ${requested.join(',')}`);
328
539
  say(`✅ Installed for ${targets.join(', ')}: ${requested.join(', ')}`);
329
540
  return void say(' ↻ Reload/restart your assistant so the new skill activates.');
330
541
  }
331
542
  case 'install': {
543
+ if (!hasDeclaredHarness()) return onboardingRequired(onboardingInput(targets));
332
544
  const profile = flag('profile') || 'minimal';
333
545
  const without = argv.filter((a, i) => argv[i - 1] === '--without');
334
546
  let ids = skillsForProfile(loadManifest(), profile);
335
547
  ids = withDefaultSkillFloor(ids).filter((id) => !without.includes(id));
336
548
  if (!argv.includes('--force') && !(await guardCollisions(targets, ids))) return;
337
- for (const t of targets) await applyInstall({ skillIds: ids, target: t });
549
+ const receipt = readManifest()?.onboarding;
550
+ const policy = receipt ? { ...receipt.plan.policy, skills: ids } : undefined;
551
+ for (const t of targets) await applyInstall({ skillIds: ids, target: t, policy });
552
+ markMaintenanceDrift(`install ${profile}`);
338
553
  say(`✅ Profile '${profile}' installed for ${targets.join(', ')} (${ids.length} skills)`);
339
554
  printAgentHandoff();
340
555
  return;
@@ -806,7 +1021,7 @@ async function main() {
806
1021
  return void (await runPurge(argv.includes('--dry-run'), argv.includes('--with-docs')));
807
1022
  default:
808
1023
  say(`rsc: unknown command '${cmd}'.`);
809
- say('Use: npx @ericrisco/rsc | add <id...> | install --profile <p> | consult "<text>" | list | capabilities [--full|gap-log] | audit | registry refresh | doctor | sync | memory <on|off|status|save|resume|learn|metrics> | sello <on|off|status|…> | worktrees [reap [path] [--confirm]] | backups | restore <id|latest> | upgrade | repair | uninstall <id> | purge');
1024
+ say('Use: npx @ericrisco/rsc onboard | reassess | add <id...> | install --profile <p> | consult "<text>" | list | capabilities [--full|gap-log] | audit | registry refresh | doctor | sync | memory <on|off|status|save|resume|learn|metrics> | sello <on|off|status|…> | worktrees [reap [path] [--confirm]] | backups | restore <id|latest> | upgrade | repair | uninstall <id> | purge');
810
1025
  say('Any command takes --target <claude|codex|cursor|copilot|gemini|…> (comma-separate for several)');
811
1026
  say(' → without it, rsc uses the assistant already installed here; if two are, it asks instead of guessing.');
812
1027
  process.exitCode = 1;
@@ -91,7 +91,10 @@ search() {
91
91
  # only exercising whichever one happens to be installed.
92
92
  if have rg && [ -z "${VERIFY_FORCE_GREP:-}" ]; then
93
93
  # rg already ignores .gitignore'd paths (node_modules, .next, dist); also skip self.
94
- rg -n --no-heading --glob "!$SELF_NAME" "$@"
94
+ # Always name the search root. run_tell_table feeds its registry through stdin;
95
+ # without an explicit path ripgrep treats that stdin as the search corpus and
96
+ # silently checks the registry text instead of the project files.
97
+ rg -n --no-heading --glob "!$SELF_NAME" "$@" .
95
98
  else
96
99
  # last arg is the pattern; everything before are paths/globs we ignore for grep.
97
100
  # `${*: -1}` (last positional) is supported on bash 3.2; the space before -1 is required.
@@ -42,6 +42,9 @@ request. If the user engaged **SDD autopilot**, that one consent covers the whol
42
42
  through the phases without re-asking.
43
43
 
44
44
  If `specify` / `sdd` are not installed, offer to add them (§2) before routing.
45
+ When `.rsc.json` records SDD as deferred, first run `npx @ericrisco/rsc@latest reassess`.
46
+ Stay silent on `RSC_REASSESSMENT_NO_CHANGE`. If it reports new evidence, explain what changed and
47
+ show the new plan command; SDD still needs a newly accepted plan id and is never added silently.
45
48
 
46
49
  Method, phase map and full decision table: `../sdd/SKILL.md`. On Claude Code this rule also arrives
47
50
  as a per-turn hook; the brevity here is deduplication, not relaxation.
@@ -17,8 +17,11 @@ export function writeSkill(id, fromDir, toPath) {
17
17
  return linkOrCopy(fromDir, toPath);
18
18
  }
19
19
 
20
- export function wireHook(paths, sourceMd) {
21
- const body = stripFrontmatter(readFileSync(sourceMd, 'utf8'));
20
+ export function wireHook(paths, sourceMd, policy = {}) {
21
+ const full = stripFrontmatter(readFileSync(sourceMd, 'utf8'));
22
+ const body = policy.codeHooks === false
23
+ ? `# rsc-suggest — always-on operations layer\n\nRead \`02-DOCS/wiki/harness/user-profile.md\` before acting. Use \`orient\` to keep the user situated and \`suggest\` to offer a missing skill only when the current task needs it. Close with the configured orientation block.\n`
24
+ : full;
22
25
  const block = `${MARK_START}\n${body}\n${MARK_END}`;
23
26
  let doc = existsSync(paths.hookTarget) ? readFileSync(paths.hookTarget, 'utf8') : '';
24
27
  if (doc.includes(MARK_START)) {
package/targets/claude.js CHANGED
@@ -79,7 +79,7 @@ export function unwireHook(paths) {
79
79
  const P = '${CLAUDE_PROJECT_DIR}';
80
80
  const at = (...seg) => `${P}/${seg.join('/')}`;
81
81
 
82
- export function wireHook(paths) {
82
+ export function wireHook(paths, sourceMd, policy = {}) {
83
83
  const scriptDest = join(paths.projectRoot, '.rsc', 'session-start.mjs');
84
84
  mkdirSync(dirname(scriptDest), { recursive: true });
85
85
  // Shared by session-start + userprompt-gate. Hooks are materialized file by file, so a module
@@ -90,8 +90,12 @@ export function wireHook(paths) {
90
90
  copyFileSync(join(HERE, 'worktree-reaper.mjs'), join(paths.projectRoot, '.rsc', 'worktree-reaper.mjs'));
91
91
  copyFileSync(join(HERE, 'session-start.mjs'), scriptDest);
92
92
 
93
- const suggestMd = `${paths.skillDir('suggest')}/SKILL.md`;
94
- const suggestRel = at(relative(paths.projectRoot, paths.skillDir('suggest')).split(sep).join('/'), 'SKILL.md');
93
+ let suggestRel = at(relative(paths.projectRoot, paths.skillDir('suggest')).split(sep).join('/'), 'SKILL.md');
94
+ const operationsSuggest = join(paths.projectRoot, '.rsc', 'suggest-always-on.md');
95
+ if (policy.codeHooks === false) {
96
+ writeFileSync(operationsSuggest, '# rsc-suggest — always-on operations layer\n\nRead `02-DOCS/wiki/harness/user-profile.md` before acting. Use `orient` to keep the user situated and `suggest` to offer a missing skill only when the current task needs it. Close with the configured orientation block.\n');
97
+ suggestRel = at('.rsc', 'suggest-always-on.md');
98
+ } else if (existsSync(operationsSuggest)) rmSync(operationsSuggest, { force: true });
95
99
  const cmd = `node "${at('.rsc', 'session-start.mjs')}" "${suggestRel}" "${P}"`;
96
100
 
97
101
  const file = paths.hookTarget;
@@ -111,6 +115,7 @@ export function wireHook(paths) {
111
115
  // the workspace has no harness wiki. Registered idempotently on both events, with
112
116
  // any prior rsc worklog-checkpoint entry (.sh or .mjs) dropped first.
113
117
  const wlDest = join(paths.projectRoot, '.rsc', 'worklog-checkpoint.mjs');
118
+ const written = [paths.hookTarget, scriptDest, wlDest];
114
119
  copyFileSync(join(HERE, 'worklog-checkpoint.mjs'), wlDest);
115
120
  const wlCmd = `node "${at('.rsc', 'worklog-checkpoint.mjs')}" "${P}"`;
116
121
  for (const event of ['PreCompact', 'SessionEnd']) {
@@ -121,6 +126,7 @@ export function wireHook(paths) {
121
126
  settings.hooks[event].push({ hooks: [{ type: 'command', command: wlCmd }] });
122
127
  }
123
128
 
129
+ if (policy.codeHooks !== false) {
124
130
  // Ship guard: a PreToolUse(Bash) hook that DENIES switching to / merging the trunk
125
131
  // while the current feature branch has uncommitted or unpushed work — forcing the
126
132
  // commit → push → PR close (the `ship` skill). Materialized + node-run (Windows-safe),
@@ -182,8 +188,24 @@ export function wireHook(paths) {
182
188
  (e) => !hookWiringOf(e).includes('.rsc/userprompt-gate.'),
183
189
  );
184
190
  settings.hooks.UserPromptSubmit.push({ hooks: [{ type: 'command', command: fgCmd }] });
191
+ written.push(sgDest, dgDest, gmDest, fgDest, join(paths.projectRoot, '.rsc', 'sello.mjs'));
192
+ } else {
193
+ for (const event of ['PreToolUse', 'UserPromptSubmit']) {
194
+ if (!settings.hooks[event]) continue;
195
+ settings.hooks[event] = settings.hooks[event].filter((entry) => {
196
+ const body = hookWiringOf(entry);
197
+ return !body.includes('.rsc/ship-guard.') && !body.includes('.rsc/danger-guard.') &&
198
+ !body.includes('.rsc/gitmoji-guard.') && !body.includes('.rsc/userprompt-gate.');
199
+ });
200
+ if (!settings.hooks[event].length) delete settings.hooks[event];
201
+ }
202
+ for (const name of ['ship-guard.mjs', 'danger-guard.mjs', 'gitmoji-guard.mjs', 'userprompt-gate.mjs', 'sello.mjs']) {
203
+ rmSync(join(paths.projectRoot, '.rsc', name), { force: true });
204
+ }
205
+ written.push(operationsSuggest);
206
+ }
185
207
 
186
208
  mkdirSync(dirname(file), { recursive: true });
187
209
  writeFileSync(file, JSON.stringify(settings, null, 2) + '\n');
188
- return [file, scriptDest, wlDest, sgDest, dgDest, gmDest, fgDest];
210
+ return written;
189
211
  }
package/targets/cursor.js CHANGED
@@ -8,8 +8,11 @@ export function writeSkill(id, fromDir, toPath) {
8
8
  return [toPath];
9
9
  }
10
10
 
11
- export function wireHook(paths, sourceMd) {
12
- const body = stripFrontmatter(readFileSync(sourceMd, 'utf8'));
11
+ export function wireHook(paths, sourceMd, policy = {}) {
12
+ const full = stripFrontmatter(readFileSync(sourceMd, 'utf8'));
13
+ const body = policy.codeHooks === false
14
+ ? `# rsc-suggest — always-on operations layer\n\nRead \`02-DOCS/wiki/harness/user-profile.md\` before acting. Use \`orient\` and offer missing skills through \`suggest\` only when needed.\n`
15
+ : full;
13
16
  mkdirSync(dirname(paths.hookTarget), { recursive: true });
14
17
  writeFileSync(paths.hookTarget, `---\ndescription: rsc auto-suggest\nalwaysApply: true\n---\n${body}`);
15
18
  return [paths.hookTarget];
package/targets/index.js CHANGED
@@ -169,8 +169,8 @@ export function writeSkill(target, id, fromDir, toPath) {
169
169
  return ADAPTER[SPEC[target].adapter].writeSkill(id, fromDir, toPath);
170
170
  }
171
171
 
172
- export function wireHook(target, paths, sourceMd) {
173
- return ADAPTER[SPEC[target].adapter].wireHook(paths, sourceMd);
172
+ export function wireHook(target, paths, sourceMd, policy) {
173
+ return ADAPTER[SPEC[target].adapter].wireHook(paths, sourceMd, policy);
174
174
  }
175
175
 
176
176
  // Inverse of wireHook — remove rsc's always-on surface for a target (settings.json