@damiangilgonzalez/harness 1.3.12 → 1.3.14

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 (73) hide show
  1. package/README.md +36 -11
  2. package/manifest.json +10 -2
  3. package/package.json +3 -2
  4. package/scripts/doctor.js +122 -6
  5. package/scripts/install-apply.js +195 -14
  6. package/scripts/lib/backups.js +13 -0
  7. package/scripts/lib/capabilities.js +19 -2
  8. package/scripts/lib/divergence.js +8 -1
  9. package/scripts/lib/onboarding-apply.js +91 -13
  10. package/scripts/lib/onboarding.js +35 -2
  11. package/scripts/lib/repair.js +8 -4
  12. package/scripts/lib/review-findings.js +175 -0
  13. package/scripts/lib/versions.js +31 -0
  14. package/scripts/rsc.js +117 -19
  15. package/skills/animar/SKILL.md +1 -0
  16. package/skills/claude-project-setup/SKILL.md +1 -0
  17. package/skills/code-review/SKILL.md +18 -1
  18. package/skills/diseno-apple/SKILL.md +1 -0
  19. package/skills/diseno-landing/SKILL.md +1 -0
  20. package/skills/domain-modeling/SKILL.md +1 -0
  21. package/skills/flujo-interfaz/SKILL.md +1 -0
  22. package/skills/grill-me/SKILL.md +1 -0
  23. package/skills/grill-with-docs/SKILL.md +1 -0
  24. package/skills/grilling/SKILL.md +1 -0
  25. package/skills/handoff/SKILL.md +1 -0
  26. package/skills/harness/SKILL.md +6 -0
  27. package/skills/harness/references/wiki-protocol.md +5 -0
  28. package/skills/implement/SKILL.md +1 -0
  29. package/skills/ingeniero-diseno-web/SKILL.md +1 -0
  30. package/skills/init/references/accompaniment-and-profile.md +1 -1
  31. package/skills/leyes-de-percepcion/SKILL.md +1 -0
  32. package/skills/leyes-de-retencion/SKILL.md +1 -0
  33. package/skills/mejor-accesibilidad/SKILL.md +1 -0
  34. package/skills/mejor-colores/SKILL.md +1 -0
  35. package/skills/mejor-layout/SKILL.md +1 -0
  36. package/skills/mejor-redaccion/SKILL.md +1 -0
  37. package/skills/mejor-tipografia/SKILL.md +1 -0
  38. package/skills/mejor-ui/SKILL.md +1 -0
  39. package/skills/muscle-memory/SKILL.md +1 -0
  40. package/skills/orient/SKILL.md +9 -1
  41. package/skills/orient/references/orientation-contract.md +1 -1
  42. package/skills/prototype/SKILL.md +1 -0
  43. package/skills/research/SKILL.md +1 -0
  44. package/skills/revision-de-cambios/SKILL.md +1 -0
  45. package/skills/revision-interfaz/SKILL.md +1 -0
  46. package/skills/sitios-calidad-premio/SKILL.md +1 -0
  47. package/skills/suggest/SKILL.md +10 -9
  48. package/skills/tastemaker/SKILL.md +1 -0
  49. package/skills/teach/SKILL.md +1 -0
  50. package/skills/to-questionnaire/SKILL.md +1 -0
  51. package/skills/to-spec/SKILL.md +1 -0
  52. package/skills/to-tickets/SKILL.md +1 -0
  53. package/skills/video-a-superprompt/SKILL.md +1 -0
  54. package/skills/vocabulario-animacion/SKILL.md +1 -0
  55. package/skills/wait-what/SKILL.md +1 -0
  56. package/skills/wayfinder/SKILL.md +1 -0
  57. package/skills/write-adr/SKILL.md +1 -0
  58. package/skills/writing-for-agents/SKILL.md +1 -0
  59. package/targets/_md-block.js +8 -1
  60. package/targets/agents-md-shadow.js +153 -0
  61. package/targets/agents.js +46 -3
  62. package/targets/claude.js +76 -26
  63. package/targets/clone-bootstrap.mjs +454 -0
  64. package/targets/gitmoji-guard.mjs +30 -3
  65. package/targets/hook-once.mjs +1 -0
  66. package/targets/memory.js +1 -1
  67. package/targets/opt-outs.js +40 -0
  68. package/targets/sello.mjs +2 -2
  69. package/targets/session-memory-adapter.mjs +70 -14
  70. package/targets/session-memory-core.mjs +40 -8
  71. package/targets/session-start.mjs +65 -12
  72. package/targets/ship-guard.mjs +1 -1
  73. package/targets/worktree-reaper.mjs +175 -11
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') },
@@ -68,7 +68,15 @@ ${REFUTER_FOUR_INPUTS}
68
68
  3. Did you inspect the relevant **caller, import, and relevant test**?
69
69
  4. Can the severity survive the **existing guards** you verified?
70
70
 
71
- If an answer is no, lower the severity or omit the finding. Every **HIGH or CRITICAL** needs the line and failure mode in the report. **Zero findings with an attack list is valid.**
71
+ If an answer is no, lower the severity or omit the finding. Every **blocker or should-fix** needs the line and failure mode in the report. **Zero findings with an attack list is valid.**
72
+
73
+ **Report findings in one shape, so the panel can be consolidated** — a fenced ${'`json findings`'} block holding an array (empty when you found nothing):
74
+
75
+ ${'```'}json findings
76
+ [{ "lens": "<your lens>", "severity": "blocker|should-fix|nit|question", "file": "path/from/repo/root", "line": 42, "claim": "one sentence: what is wrong", "failure_scenario": "concrete input + state -> wrong result", "evidence": "the exact changed line, quoted" }]
77
+ ${'```'}
78
+
79
+ ${'`rsc review consolidate`'} merges what several lenses reported about the same place and sends each unique blocker or should-fix to ${'`finding-verifier`'}, whose job is to prove it false. A blocker or should-fix with no ${'`file`'}, ${'`line`'} or ${'`failure_scenario`'} is downgraded to a question there, mechanically.
72
80
 
73
81
  **Common false positives to reject:** an equivalent mutant with no diverging input; a documented dummy value that never reaches a sink; a deliberate boundary already enforced by a caller; generated/vendor code outside the change; style preference presented as correctness; and a theoretical race with no shared state or overlapping lifetime.
74
82
 
@@ -126,8 +134,36 @@ ${REFUTER_CONTRACT}
126
134
 
127
135
  **Check the mapping both ways:** every acceptance criterion needs a falsification procedure that can be made to fail, and every test should trace to something someone asked for.`,
128
136
  },
137
+ // The second half of ECC's orch-review, adapted: after `rsc review consolidate` collapses the panel's
138
+ // duplicates, every unique blocker/should-fix gets ONE more fresh context whose default is that the
139
+ // finding is false. A refuter attacks the diff; the verifier attacks the finding. Same asymmetry,
140
+ // pointed the other way — which is what stops an exaggerated lens from reaching the author as a blocker.
141
+ {
142
+ name: 'finding-verifier',
143
+ desc: 'Adversarial verifier of review findings: takes ONE consolidated finding and tries to prove it false against the code. Default stance: the finding is false; only a reproduced or traced failure is CONFIRMED. Fresh context, edits nothing.',
144
+ body: `You are the **finding verifier** for this project. ${'`code-review`'} hands you **one** finding that survived ${'`rsc review consolidate`'} — a blocker or should-fix some refuter lens reported against a diff. Your job is to **prove it false**. Your starting position is that it is false; the finding has to beat you.
145
+
146
+ **Your inputs:** the finding (location, claim, failure scenario, evidence) plus the same four the lenses had —
147
+
148
+ ${REFUTER_FOUR_INPUTS}
149
+
150
+ **You do NOT get** the refuter's reasoning or the builder's defence. Judge the claim against the code, not against either story.
151
+
152
+ **How to try to kill it:**
153
+ 1. Read the cited line **and** its callers, guards and tests — the check that makes it impossible often lives two functions up, or in a caller that never passes that input.
154
+ 2. Try the failure scenario for real when you can: run the test, execute the command, or write a throwaway check in a temp directory outside the repo. An observed failure outranks any argument.
155
+ 3. Ask whether the change **caused** it. A defect that was already there is real but does not block this delivery.
156
+
157
+ **Verdict — exactly one, with its proof:**
158
+ - **CONFIRMED** — you reproduced it, or traced a reachable path from a concrete input to the wrong result. Quote the output or the path. Only this verdict may block.
159
+ - **REFUTED** — you found what makes it impossible (the guard, the caller, the type, the test). Cite it with file and line.
160
+ - **UNPROVEN** — neither: it becomes a question for the human, and questions never block.
161
+
162
+ Also state whether the severity holds. A CONFIRMED finding whose blast radius is narrow is a should-fix, not a blocker — say so. You fix nothing and edit no file: your output is the verdict, the proof, and the commands you ran.`,
163
+ },
129
164
  ];
130
165
 
166
+
131
167
  export const BASE_AGENT_NAMES = Object.freeze(AGENTS.map((agent) => agent.name));
132
168
  export { stackAgents, stackAgentNames, validateAgentCatalog };
133
169
  export const allAgentNames = () => [...BASE_AGENT_NAMES, ...stackAgentNames()];
@@ -160,7 +196,14 @@ export function writeDeveloperTier(cwd, tier) {
160
196
  function renderMd(spec, model, agent) {
161
197
  const fm = ['---', `name: ${agent.name}`, `description: "${agent.desc}"`, `model: ${model}`];
162
198
  if (spec.mode) fm.push(`mode: ${spec.mode}`);
163
- if (agent.tools) fm.push(`tools: [${agent.tools.join(', ')}]`);
199
+ if (agent.tools) {
200
+ if (spec.toolsFormat === 'map') {
201
+ fm.push('tools:');
202
+ for (const tool of agent.tools) fm.push(` ${tool}: true`);
203
+ } else {
204
+ fm.push(`tools: [${agent.tools.join(', ')}]`);
205
+ }
206
+ }
164
207
  fm.push('---', '');
165
208
  return `${fm.join('\n')}${agent.body}\n`;
166
209
  }
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,26 +175,13 @@ 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.'),
147
182
  );
148
183
  settings.hooks.PreToolUse.push({ matcher: SHELL_TOOLS, hooks: [{ type: 'command', command: sgCmd }] });
149
184
 
150
- // Danger guard: a PreToolUse(Bash) hook that DENIES irreversible foot-gun commands
151
- // (rm -rf, git push --force, DROP/TRUNCATE, DELETE/UPDATE without WHERE, dd to /dev,
152
- // curl|bash, …) when the user-profile says the user is NON-technical (default-safe
153
- // when no profile exists yet; never guards a fully `technical` user). Materialized +
154
- // node-run (Windows-safe), idempotent, fail-open, opt-out via .rsc/.no-danger-guard.
155
- const dgDest = join(paths.projectRoot, '.rsc', 'danger-guard.mjs');
156
- copyFileSync(join(HERE, 'danger-guard.mjs'), dgDest);
157
- const dgCmd = `node "${at('.rsc', 'danger-guard.mjs')}" "${P}"`;
158
- settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
159
- (e) => !hookWiringOf(e).includes('.rsc/danger-guard.'),
160
- );
161
- settings.hooks.PreToolUse.push({ matcher: SHELL_TOOLS, hooks: [{ type: 'command', command: dgCmd }] });
162
-
163
185
  // Gitmoji guard: a PreToolUse(Bash) hook that DENIES a `git commit` whose message
164
186
  // carries no gitmoji (gitmoji.dev) in front of the Conventional Commits header. The
165
187
  // convention lives in `git-workflow`; prose asks, this hook decides — at the one
@@ -169,7 +191,7 @@ export function wireHook(paths, sourceMd, policy = {}) {
169
191
  // .rsc/.no-gitmoji.
170
192
  const gmDest = join(paths.projectRoot, '.rsc', 'gitmoji-guard.mjs');
171
193
  copyFileSync(join(HERE, 'gitmoji-guard.mjs'), gmDest);
172
- const gmCmd = `node "${at('.rsc', 'gitmoji-guard.mjs')}" "${P}"`;
194
+ const gmCmd = viaBootstrap('guard', at('.rsc', 'gitmoji-guard.mjs'), `"${P}"`);
173
195
  settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
174
196
  (e) => !hookWiringOf(e).includes('.rsc/gitmoji-guard.'),
175
197
  );
@@ -185,24 +207,24 @@ export function wireHook(paths, sourceMd, policy = {}) {
185
207
  // (non-rsc) UserPromptSubmit hooks are preserved.
186
208
  const fgDest = join(paths.projectRoot, '.rsc', 'userprompt-gate.mjs');
187
209
  copyFileSync(join(HERE, 'userprompt-gate.mjs'), fgDest);
188
- const fgCmd = `node "${at('.rsc', 'userprompt-gate.mjs')}" "${P}"`;
210
+ const fgCmd = viaBootstrap('quiet', at('.rsc', 'userprompt-gate.mjs'), `"${P}"`);
189
211
  settings.hooks.UserPromptSubmit ||= [];
190
212
  settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter(
191
213
  (e) => !hookWiringOf(e).includes('.rsc/userprompt-gate.'),
192
214
  );
193
215
  settings.hooks.UserPromptSubmit.push({ hooks: [{ type: 'command', command: fgCmd }] });
194
- written.push(sgDest, dgDest, gmDest, fgDest, join(paths.projectRoot, '.rsc', 'sello.mjs'));
216
+ written.push(sgDest, gmDest, fgDest, join(paths.projectRoot, '.rsc', 'sello.mjs'));
195
217
  } else {
196
218
  for (const event of ['PreToolUse', 'UserPromptSubmit']) {
197
219
  if (!settings.hooks[event]) continue;
198
220
  settings.hooks[event] = settings.hooks[event].filter((entry) => {
199
221
  const body = hookWiringOf(entry);
200
- return !body.includes('.rsc/ship-guard.') && !body.includes('.rsc/danger-guard.') &&
222
+ return !body.includes('.rsc/ship-guard.') &&
201
223
  !body.includes('.rsc/gitmoji-guard.') && !body.includes('.rsc/userprompt-gate.');
202
224
  });
203
225
  if (!settings.hooks[event].length) delete settings.hooks[event];
204
226
  }
205
- for (const name of ['ship-guard.mjs', 'danger-guard.mjs', 'gitmoji-guard.mjs', 'userprompt-gate.mjs', 'sello.mjs']) {
227
+ for (const name of ['ship-guard.mjs', 'gitmoji-guard.mjs', 'userprompt-gate.mjs', 'sello.mjs']) {
206
228
  rmSync(join(paths.projectRoot, '.rsc', name), { force: true });
207
229
  }
208
230
  written.push(operationsSuggest);
@@ -213,7 +235,35 @@ export function wireHook(paths, sourceMd, policy = {}) {
213
235
  for (const id of policy.plugins) settings.enabledPlugins[id] = true;
214
236
  }
215
237
 
238
+ // Danger guard: its own decision, not part of the code-hook block above (#273). `init` promises it
239
+ // to non-technical and mixed users whatever the project is, and a plan that predates the field is
240
+ // read as "yes": a non-technical operations install gets the guard it was missing on its next sync,
241
+ // and a technical user gets a guard that stands down at runtime.
242
+ settings.hooks.PreToolUse ||= [];
243
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter((e) => !hookWiringOf(e).includes('.rsc/danger-guard.'));
244
+ if (policy.dangerGuard !== false) {
245
+ // Danger guard: a PreToolUse(Bash) hook that DENIES irreversible foot-gun commands
246
+ // (rm -rf, git push --force, DROP/TRUNCATE, DELETE/UPDATE without WHERE, dd to /dev,
247
+ // curl|bash, …) when the user-profile says the user is NON-technical (default-safe
248
+ // when no profile exists yet; never guards a fully `technical` user). Materialized +
249
+ // node-run (Windows-safe), idempotent, fail-open, opt-out via .rsc/.no-danger-guard.
250
+ const dgDest = join(paths.projectRoot, '.rsc', 'danger-guard.mjs');
251
+ copyFileSync(join(HERE, 'danger-guard.mjs'), dgDest);
252
+ const dgCmd = viaBootstrap('guard', at('.rsc', 'danger-guard.mjs'), `"${P}"`);
253
+ settings.hooks.PreToolUse.push({ matcher: SHELL_TOOLS, hooks: [{ type: 'command', command: dgCmd }] });
254
+ written.push(dgDest);
255
+ } else {
256
+ rmSync(join(paths.projectRoot, '.rsc', 'danger-guard.mjs'), { force: true });
257
+ }
258
+ if (!settings.hooks.PreToolUse.length) delete settings.hooks.PreToolUse;
259
+
216
260
  mkdirSync(dirname(file), { recursive: true });
217
261
  writeFileSync(file, JSON.stringify(settings, null, 2) + '\n');
262
+
263
+ // settings.json now names this project as rsc-wired, which is the predicate the shadow reads —
264
+ // so this call belongs AFTER the write, not before it. No-op unless the root AGENTS.md also
265
+ // carries the always-on body and no CLAUDE.md exists: see targets/agents-md-shadow.js.
266
+ const shadow = ensureShadowClaudeMd(paths.projectRoot);
267
+ if (shadow) written.push(shadow);
218
268
  return written;
219
269
  }