@damiangilgonzalez/harness 1.3.11 → 1.3.13

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.
Files changed (61) hide show
  1. package/README.md +31 -9
  2. package/manifest.json +1 -1
  3. package/package.json +3 -2
  4. package/scripts/doctor.js +109 -2
  5. package/scripts/install-apply.js +127 -7
  6. package/scripts/lib/capabilities.js +19 -2
  7. package/scripts/lib/divergence.js +8 -1
  8. package/scripts/lib/onboarding-apply.js +18 -1
  9. package/scripts/lib/onboarding.js +27 -2
  10. package/scripts/lib/versions.js +31 -0
  11. package/scripts/rsc.js +66 -16
  12. package/skills/animar/SKILL.md +1 -0
  13. package/skills/claude-project-setup/SKILL.md +1 -0
  14. package/skills/code-review/SKILL.md +1 -0
  15. package/skills/diseno-apple/SKILL.md +1 -0
  16. package/skills/diseno-landing/SKILL.md +1 -0
  17. package/skills/domain-modeling/SKILL.md +1 -0
  18. package/skills/flujo-interfaz/SKILL.md +1 -0
  19. package/skills/grill-me/SKILL.md +1 -0
  20. package/skills/grill-with-docs/SKILL.md +1 -0
  21. package/skills/grilling/SKILL.md +1 -0
  22. package/skills/handoff/SKILL.md +1 -0
  23. package/skills/implement/SKILL.md +1 -0
  24. package/skills/ingeniero-diseno-web/SKILL.md +1 -0
  25. package/skills/leyes-de-percepcion/SKILL.md +1 -0
  26. package/skills/leyes-de-retencion/SKILL.md +1 -0
  27. package/skills/mejor-accesibilidad/SKILL.md +1 -0
  28. package/skills/mejor-colores/SKILL.md +1 -0
  29. package/skills/mejor-layout/SKILL.md +1 -0
  30. package/skills/mejor-redaccion/SKILL.md +1 -0
  31. package/skills/mejor-tipografia/SKILL.md +1 -0
  32. package/skills/mejor-ui/SKILL.md +1 -0
  33. package/skills/muscle-memory/SKILL.md +1 -0
  34. package/skills/prototype/SKILL.md +1 -0
  35. package/skills/research/SKILL.md +1 -0
  36. package/skills/revision-de-cambios/SKILL.md +1 -0
  37. package/skills/revision-interfaz/SKILL.md +1 -0
  38. package/skills/sitios-calidad-premio/SKILL.md +1 -0
  39. package/skills/suggest/SKILL.md +10 -9
  40. package/skills/tastemaker/SKILL.md +1 -0
  41. package/skills/teach/SKILL.md +1 -0
  42. package/skills/to-questionnaire/SKILL.md +1 -0
  43. package/skills/to-spec/SKILL.md +1 -0
  44. package/skills/to-tickets/SKILL.md +1 -0
  45. package/skills/video-a-superprompt/SKILL.md +1 -0
  46. package/skills/vocabulario-animacion/SKILL.md +1 -0
  47. package/skills/wait-what/SKILL.md +1 -0
  48. package/skills/wayfinder/SKILL.md +1 -0
  49. package/skills/write-adr/SKILL.md +1 -0
  50. package/skills/writing-for-agents/SKILL.md +1 -0
  51. package/targets/_md-block.js +8 -1
  52. package/targets/agents-md-shadow.js +153 -0
  53. package/targets/agents.js +9 -2
  54. package/targets/claude.js +52 -11
  55. package/targets/clone-bootstrap.mjs +454 -0
  56. package/targets/gitmoji-guard.mjs +30 -3
  57. package/targets/opt-outs.js +40 -0
  58. package/targets/session-memory-adapter.mjs +70 -14
  59. package/targets/session-memory-core.mjs +45 -7
  60. package/targets/session-start.mjs +63 -10
  61. package/targets/worktree-reaper.mjs +174 -10
@@ -0,0 +1,153 @@
1
+ // agents-md-shadow.js — keep the always-on body from arriving twice in Claude Code.
2
+ //
3
+ // THE DEFECT. rsc delivers its always-on layer through two different surfaces, one per
4
+ // assistant family:
5
+ // - Claude Code → a SessionStart hook (targets/claude.js) that PRINTS suggest's body.
6
+ // - AGENTS.md family (codex, opencode, amp, jules, zed) → the body is written INTO the
7
+ // root AGENTS.md between markers (targets/_md-block.js).
8
+ // Wiring both in one project is an ordinary install — this repo's own workspace does it.
9
+ // Until Claude Code 2.1.277 the two never met, because Claude Code did not read AGENTS.md.
10
+ //
11
+ // From 2.1.277 it does: with NO CLAUDE.md in the working directory or above it, Claude Code
12
+ // reads the root AGENTS.md as the project instructions. In a dual-wired project that makes
13
+ // the same ~7 KB body land twice per session — once printed by the hook, once loaded from
14
+ // the file. Anthropic documents the shape of this exact collision and its remedy ("A
15
+ // SessionStart hook that prints AGENTS.md: remove it. Once Claude reads AGENTS.md directly,
16
+ // the hook adds a second copy to the context").
17
+ //
18
+ // WHY THIS IS NOT SOLVED BY MAKING THE HOOK STAY SILENT. The hook cannot know whether it is
19
+ // talking to a Claude Code that reads AGENTS.md: the SessionStart payload carries session_id,
20
+ // cwd, source and sometimes model — there is no version field, and no env var exposes one.
21
+ // Staying silent on a guess would, on every client older than 2.1.277, swallow the always-on
22
+ // layer completely. hook-once.mjs states the rule this file obeys: "A duplicated block costs
23
+ // context; a swallowed always-on layer costs the session its behaviour."
24
+ //
25
+ // THE FIX, version-free. Claude Code reads AGENTS.md only when no CLAUDE.md exists. So give
26
+ // it one. A CLAUDE.md at the project root makes every Claude Code — old or new — take the
27
+ // hook as the single source of the always-on body, and leaves AGENTS.md to the assistants it
28
+ // was written for. No version sniffing, no guess, same bytes on every client.
29
+ //
30
+ // The shadow deliberately does NOT `@AGENTS.md`-import: the import would expand the rsc block
31
+ // into context and recreate the duplication it exists to prevent.
32
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
33
+ import { dirname, join, parse } from 'node:path';
34
+
35
+ // The marker _md-block.js wraps the always-on body in. Its presence in a root AGENTS.md is
36
+ // what makes that file a second carrier of the body rather than the user's own prose.
37
+ const SUGGEST_MARK = '<!-- rsc-suggest:start -->';
38
+
39
+ // Identifies a CLAUDE.md as ours. Block-level HTML comments are stripped from Claude Code's
40
+ // context before injection, so everything explanatory below costs the user zero tokens.
41
+ export const SHADOW_MARK = '<!-- rsc:claude-md-shadow -->';
42
+
43
+ export const SHADOW_BODY = `${SHADOW_MARK}
44
+ # Project instructions
45
+
46
+ Claude Code reads this file. Every other assistant wired in this project reads \`AGENTS.md\`.
47
+
48
+ Project instructions for Claude Code go below.
49
+
50
+ <!--
51
+ Why rsc created this file
52
+ =========================
53
+ rsc's always-on layer reaches Claude Code through its SessionStart hook, and reaches the
54
+ AGENTS.md-family assistants through the root AGENTS.md. From Claude Code 2.1.277, a project
55
+ with no CLAUDE.md is read through AGENTS.md directly — which would deliver that same body a
56
+ second time, every session. The existence of this file is what prevents it.
57
+
58
+ Do NOT add "@AGENTS.md" here: the import would expand the always-on block into context and
59
+ bring the duplication back.
60
+
61
+ rsc removes this file on uninstall while it is still untouched. Edit it and it is yours —
62
+ rsc then leaves it alone, and it keeps doing its job just as well.
63
+ -->
64
+ `;
65
+
66
+ // The three files whose presence stops Claude Code reading AGENTS.md (per its memory docs).
67
+ // A user's own CLAUDE.md already does the shadow's job, so we never touch a project that has one.
68
+ const CLAUDE_MD_FORMS = ['CLAUDE.md', join('.claude', 'CLAUDE.md'), 'CLAUDE.local.md'];
69
+
70
+ // Claude Code looks for those files in the working directory AND every directory above it, so a
71
+ // CLAUDE.md two levels up already suppresses the AGENTS.md read. Walking up costs a few stats and
72
+ // keeps us from planting a file that was never needed.
73
+ function claudeMdAboveOrAt(root) {
74
+ let dir = root;
75
+ for (;;) {
76
+ for (const form of CLAUDE_MD_FORMS) if (existsSync(join(dir, form))) return join(dir, form);
77
+ const up = dirname(dir);
78
+ if (up === dir || dir === parse(dir).root) return null;
79
+ dir = up;
80
+ }
81
+ }
82
+
83
+ // Is the Claude Code target wired in this project? Same predicate session-start.mjs and doctor
84
+ // use: an rsc command in .claude/settings.json. The backslash normalization is not cosmetic —
85
+ // a Windows install stores the path with separators that JSON escapes as pairs, and reading only
86
+ // for `.rsc/` answered "not wired" on the exact platform where duplication hurt most.
87
+ function claudeWired(root) {
88
+ try {
89
+ return readFileSync(join(root, '.claude', 'settings.json'), 'utf8')
90
+ .replace(/\\\\/g, '/')
91
+ .includes('.rsc/');
92
+ } catch { return false; }
93
+ }
94
+
95
+ // Does the root AGENTS.md carry rsc's always-on block (as opposed to being the user's own prose)?
96
+ function agentsMdCarriesBody(root) {
97
+ try { return readFileSync(join(root, 'AGENTS.md'), 'utf8').includes(SUGGEST_MARK); } catch { return false; }
98
+ }
99
+
100
+ /**
101
+ * Write the shadow CLAUDE.md when — and only when — this project is in the configuration that
102
+ * duplicates: Claude Code wired, the root AGENTS.md carrying rsc's block, and no CLAUDE.md of
103
+ * any form at the root or above it.
104
+ *
105
+ * Called from BOTH adapters' wireHook. Installs run one target per call, so whichever of the two
106
+ * is wired second is the one that sees the full picture; calling from both is what makes the fix
107
+ * independent of the order the user picks their assistants in.
108
+ *
109
+ * @returns the path written, or null when nothing was needed. Never throws: a shadow we could not
110
+ * write costs context, and that must not be the thing that fails an install.
111
+ */
112
+ export function ensureShadowClaudeMd(projectRoot) {
113
+ try {
114
+ const dest = shadowTarget(projectRoot);
115
+ if (!dest) return null;
116
+ mkdirSync(dirname(dest), { recursive: true });
117
+ writeFileSync(dest, SHADOW_BODY);
118
+ return dest;
119
+ } catch { return null; }
120
+ }
121
+
122
+ /**
123
+ * Where the shadow would go, or null when this project does not need one — the decision without
124
+ * the write, so the installer can declare the file BEFORE it exists. A path this returns but the
125
+ * install plan omits is a file no --dry-run mentions and no `rsc restore` can bring back.
126
+ *
127
+ * @param assumeClaudeWired for a claude install in flight: settings.json names the project as
128
+ * wired only after wireHook writes it, so at plan time the answer has to be assumed, not read.
129
+ */
130
+ export function shadowTarget(projectRoot, { assumeClaudeWired = false } = {}) {
131
+ try {
132
+ if (!assumeClaudeWired && !claudeWired(projectRoot)) return null;
133
+ if (!agentsMdCarriesBody(projectRoot)) return null;
134
+ if (claudeMdAboveOrAt(projectRoot)) return null;
135
+ return join(projectRoot, 'CLAUDE.md');
136
+ } catch { return null; }
137
+ }
138
+
139
+ /**
140
+ * Take back exactly what we gave: remove the shadow only while it is byte-identical to what
141
+ * ensureShadowClaudeMd wrote. The moment the user edits it, it is their project instructions —
142
+ * uninstalling rsc must not delete those. Same discipline _md-block.js applies to its block.
143
+ *
144
+ * @returns the path removed, or null.
145
+ */
146
+ export function removeShadowClaudeMd(projectRoot) {
147
+ try {
148
+ const dest = join(projectRoot, 'CLAUDE.md');
149
+ if (readFileSync(dest, 'utf8') !== SHADOW_BODY) return null;
150
+ rmSync(dest, { force: true });
151
+ return dest;
152
+ } catch { return null; }
153
+ }
package/targets/agents.js CHANGED
@@ -32,7 +32,7 @@ const AGENT_TARGETS = {
32
32
  claude: { dir: '.claude/agents', ext: '.md', format: 'md', model: (t) => (t === 'heavy' ? 'opus' : 'sonnet') },
33
33
  junie: { dir: '.junie/agents', ext: '.md', format: 'md', model: (t) => (t === 'heavy' ? 'opus' : 'sonnet') },
34
34
  cursor: { dir: '.cursor/agents', ext: '.md', format: 'md', model: (t) => TIER_MODEL.anthropic[t] },
35
- opencode: { dir: '.opencode/agents', ext: '.md', format: 'md', mode: 'subagent', model: (t) => `anthropic/${TIER_MODEL.anthropic[t]}` },
35
+ opencode: { dir: '.opencode/agents', ext: '.md', format: 'md', mode: 'subagent', toolsFormat: 'map', model: (t) => `anthropic/${TIER_MODEL.anthropic[t]}` },
36
36
  gemini: { dir: '.gemini/agents', ext: '.md', format: 'md', model: (t) => TIER_MODEL.google[t] },
37
37
  copilot: { dir: '.github/agents', ext: '.agent.md', format: 'md', model: (t) => TIER_MODEL.anthropic[t] },
38
38
  kiro: { dir: '.kiro/agents', ext: '.json', format: 'json', model: (t) => (t === 'heavy' ? 'claude-opus-4' : 'claude-sonnet-4') },
@@ -160,7 +160,14 @@ export function writeDeveloperTier(cwd, tier) {
160
160
  function renderMd(spec, model, agent) {
161
161
  const fm = ['---', `name: ${agent.name}`, `description: "${agent.desc}"`, `model: ${model}`];
162
162
  if (spec.mode) fm.push(`mode: ${spec.mode}`);
163
- if (agent.tools) fm.push(`tools: [${agent.tools.join(', ')}]`);
163
+ if (agent.tools) {
164
+ if (spec.toolsFormat === 'map') {
165
+ fm.push('tools:');
166
+ for (const tool of agent.tools) fm.push(` ${tool}: true`);
167
+ } else {
168
+ fm.push(`tools: [${agent.tools.join(', ')}]`);
169
+ }
170
+ }
164
171
  fm.push('---', '');
165
172
  return `${fm.join('\n')}${agent.body}\n`;
166
173
  }
package/targets/claude.js CHANGED
@@ -2,6 +2,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, rmSyn
2
2
  import { dirname, join, relative, sep } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { linkOrCopy } from './index.js';
5
+ import { ensureShadowClaudeMd, removeShadowClaudeMd } from './agents-md-shadow.js';
5
6
 
6
7
  const HERE = dirname(fileURLToPath(import.meta.url));
7
8
  // Shell tools the PreToolUse guards listen on. On Windows the primary shell is the PowerShell tool,
@@ -48,11 +49,23 @@ export function hookWiringOf(entry) {
48
49
  // other settings are preserved. Empty event arrays (and an empty hooks object) are
49
50
  // pruned so we don't leave noise behind.
50
51
  export function unwireHook(paths) {
52
+ // The bootstrap lives in the COMMITTED tree, so leaving it behind does not just orphan a file the
53
+ // way a stray `.rsc/` entry would — it leaves an executable in something the user pushes. Nothing
54
+ // else removes it: uninstall and purge only ever knew about `.rsc/`.
55
+ try {
56
+ rmSync(join(paths.projectRoot, '.claude', 'rsc-bootstrap.mjs'), { force: true });
57
+ } catch { /* never let cleanup be the thing that fails */ }
58
+
59
+ // The shadow CLAUDE.md exists only to keep the hook from being doubled by a natively-read
60
+ // AGENTS.md. Unwiring the hook removes the reason for it. Only ever takes back an untouched one.
61
+ const shadow = removeShadowClaudeMd(paths.projectRoot);
62
+ const removed = shadow ? [shadow] : [];
63
+
51
64
  const file = paths.hookTarget;
52
- if (!existsSync(file)) return [];
65
+ if (!existsSync(file)) return removed;
53
66
  let settings;
54
- try { settings = JSON.parse(readFileSync(file, 'utf8')); } catch { return []; }
55
- if (!settings.hooks) return [];
67
+ try { settings = JSON.parse(readFileSync(file, 'utf8')); } catch { return removed; }
68
+ if (!settings.hooks) return removed;
56
69
  for (const event of Object.keys(settings.hooks)) {
57
70
  settings.hooks[event] = (settings.hooks[event] || []).filter((e) => {
58
71
  const s = hookWiringOf(e);
@@ -62,7 +75,7 @@ export function unwireHook(paths) {
62
75
  }
63
76
  if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
64
77
  writeFileSync(file, JSON.stringify(settings, null, 2) + '\n');
65
- return [file];
78
+ return [...removed, file];
66
79
  }
67
80
 
68
81
  // SessionStart runs a project-local session-start.mjs via `node`: it prints
@@ -82,6 +95,23 @@ export function unwireHook(paths) {
82
95
  const P = '${CLAUDE_PROJECT_DIR}';
83
96
  const at = (...seg) => `${P}/${seg.join('/')}`;
84
97
 
98
+ // Everything below points at `.rsc/`, which git does not carry — so in a clone every one of these
99
+ // commands runs against a file that is not there and the client shows the module loader's stack
100
+ // trace instead of a hook. The fix is not to make each script survive its own absence (it cannot:
101
+ // the guards import only node builtins, so a missing entry file fails before a line of theirs runs).
102
+ // The fix is to point the wiring at a file that IS committed, and let it decide: delegate when the
103
+ // harness is mounted, speak once when it is not.
104
+ //
105
+ // The script's own path stays in the command as an argument, so every dedup/unwire needle
106
+ // (`.rsc/session-start.`, `.rsc/ship-guard.`, …) still matches and idempotency is untouched.
107
+ // Three modes, because a hook's event decides what it is allowed to say. `announce` is the one hook
108
+ // that may open with the offer (one per session, no marker needed). `guard` is a shell guard, and in
109
+ // an unbuilt harness it is the only place that can notice someone is about to do the thing the
110
+ // missing guard protected. `quiet` delegates and never speaks.
111
+ const BOOTSTRAP = '.claude/rsc-bootstrap.mjs';
112
+ const viaBootstrap = (mode, target, ...args) =>
113
+ [`node "${at(...BOOTSTRAP.split('/'))}"`, `"${mode}"`, `"${P}"`, `"${target}"`, ...args].join(' ');
114
+
85
115
  export function wireHook(paths, sourceMd, policy = {}) {
86
116
  const scriptDest = join(paths.projectRoot, '.rsc', 'session-start.mjs');
87
117
  mkdirSync(dirname(scriptDest), { recursive: true });
@@ -92,6 +122,11 @@ export function wireHook(paths, sourceMd, policy = {}) {
92
122
  // sibling too. Missing it does not break startup — the sweep is wrapped — it just goes silent.
93
123
  copyFileSync(join(HERE, 'worktree-reaper.mjs'), join(paths.projectRoot, '.rsc', 'worktree-reaper.mjs'));
94
124
  copyFileSync(join(HERE, 'session-start.mjs'), scriptDest);
125
+ // The one harness file that belongs in the committed tree rather than in `.rsc/`: it is the only
126
+ // thing standing between a clone and seven stack traces, so it has to survive `git clone`.
127
+ const bootstrapDest = join(paths.projectRoot, '.claude', 'rsc-bootstrap.mjs');
128
+ mkdirSync(dirname(bootstrapDest), { recursive: true });
129
+ copyFileSync(join(HERE, 'clone-bootstrap.mjs'), bootstrapDest);
95
130
 
96
131
  let suggestRel = at(relative(paths.projectRoot, paths.skillDir('suggest')).split(sep).join('/'), 'SKILL.md');
97
132
  const operationsSuggest = join(paths.projectRoot, '.rsc', 'suggest-always-on.md');
@@ -99,7 +134,7 @@ export function wireHook(paths, sourceMd, policy = {}) {
99
134
  writeFileSync(operationsSuggest, '# rsc-suggest — always-on operations layer\n\nRead `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');
100
135
  suggestRel = at('.rsc', 'suggest-always-on.md');
101
136
  } else if (existsSync(operationsSuggest)) rmSync(operationsSuggest, { force: true });
102
- const cmd = `node "${at('.rsc', 'session-start.mjs')}" "${suggestRel}" "${P}"`;
137
+ const cmd = viaBootstrap('announce', at('.rsc', 'session-start.mjs'), `"${suggestRel}"`, `"${P}"`);
103
138
 
104
139
  const file = paths.hookTarget;
105
140
  const settings = existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : {};
@@ -118,9 +153,9 @@ export function wireHook(paths, sourceMd, policy = {}) {
118
153
  // the workspace has no harness wiki. Registered idempotently on both events, with
119
154
  // any prior rsc worklog-checkpoint entry (.sh or .mjs) dropped first.
120
155
  const wlDest = join(paths.projectRoot, '.rsc', 'worklog-checkpoint.mjs');
121
- const written = [paths.hookTarget, scriptDest, wlDest];
156
+ const written = [paths.hookTarget, scriptDest, wlDest, bootstrapDest];
122
157
  copyFileSync(join(HERE, 'worklog-checkpoint.mjs'), wlDest);
123
- const wlCmd = `node "${at('.rsc', 'worklog-checkpoint.mjs')}" "${P}"`;
158
+ const wlCmd = viaBootstrap('quiet', at('.rsc', 'worklog-checkpoint.mjs'), `"${P}"`);
124
159
  for (const event of ['PreCompact', 'SessionEnd']) {
125
160
  settings.hooks[event] ||= [];
126
161
  settings.hooks[event] = settings.hooks[event].filter(
@@ -140,7 +175,7 @@ export function wireHook(paths, sourceMd, policy = {}) {
140
175
  // sello.mjs is ship-guard's sibling import (hooks are materialized file-by-file,
141
176
  // so the deterministic sello core must land next to the guard that loads it).
142
177
  copyFileSync(join(HERE, 'sello.mjs'), join(paths.projectRoot, '.rsc', 'sello.mjs'));
143
- const sgCmd = `node "${at('.rsc', 'ship-guard.mjs')}" "${P}"`;
178
+ const sgCmd = viaBootstrap('guard', at('.rsc', 'ship-guard.mjs'), `"${P}"`);
144
179
  settings.hooks.PreToolUse ||= [];
145
180
  settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
146
181
  (e) => !hookWiringOf(e).includes('.rsc/ship-guard.'),
@@ -154,7 +189,7 @@ export function wireHook(paths, sourceMd, policy = {}) {
154
189
  // node-run (Windows-safe), idempotent, fail-open, opt-out via .rsc/.no-danger-guard.
155
190
  const dgDest = join(paths.projectRoot, '.rsc', 'danger-guard.mjs');
156
191
  copyFileSync(join(HERE, 'danger-guard.mjs'), dgDest);
157
- const dgCmd = `node "${at('.rsc', 'danger-guard.mjs')}" "${P}"`;
192
+ const dgCmd = viaBootstrap('guard', at('.rsc', 'danger-guard.mjs'), `"${P}"`);
158
193
  settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
159
194
  (e) => !hookWiringOf(e).includes('.rsc/danger-guard.'),
160
195
  );
@@ -169,7 +204,7 @@ export function wireHook(paths, sourceMd, policy = {}) {
169
204
  // .rsc/.no-gitmoji.
170
205
  const gmDest = join(paths.projectRoot, '.rsc', 'gitmoji-guard.mjs');
171
206
  copyFileSync(join(HERE, 'gitmoji-guard.mjs'), gmDest);
172
- const gmCmd = `node "${at('.rsc', 'gitmoji-guard.mjs')}" "${P}"`;
207
+ const gmCmd = viaBootstrap('guard', at('.rsc', 'gitmoji-guard.mjs'), `"${P}"`);
173
208
  settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
174
209
  (e) => !hookWiringOf(e).includes('.rsc/gitmoji-guard.'),
175
210
  );
@@ -185,7 +220,7 @@ export function wireHook(paths, sourceMd, policy = {}) {
185
220
  // (non-rsc) UserPromptSubmit hooks are preserved.
186
221
  const fgDest = join(paths.projectRoot, '.rsc', 'userprompt-gate.mjs');
187
222
  copyFileSync(join(HERE, 'userprompt-gate.mjs'), fgDest);
188
- const fgCmd = `node "${at('.rsc', 'userprompt-gate.mjs')}" "${P}"`;
223
+ const fgCmd = viaBootstrap('quiet', at('.rsc', 'userprompt-gate.mjs'), `"${P}"`);
189
224
  settings.hooks.UserPromptSubmit ||= [];
190
225
  settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter(
191
226
  (e) => !hookWiringOf(e).includes('.rsc/userprompt-gate.'),
@@ -215,5 +250,11 @@ export function wireHook(paths, sourceMd, policy = {}) {
215
250
 
216
251
  mkdirSync(dirname(file), { recursive: true });
217
252
  writeFileSync(file, JSON.stringify(settings, null, 2) + '\n');
253
+
254
+ // settings.json now names this project as rsc-wired, which is the predicate the shadow reads —
255
+ // so this call belongs AFTER the write, not before it. No-op unless the root AGENTS.md also
256
+ // carries the always-on body and no CLAUDE.md exists: see targets/agents-md-shadow.js.
257
+ const shadow = ensureShadowClaudeMd(paths.projectRoot);
258
+ if (shadow) written.push(shadow);
218
259
  return written;
219
260
  }