@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.
- package/README.md +36 -11
- package/manifest.json +10 -2
- package/package.json +3 -2
- package/scripts/doctor.js +122 -6
- package/scripts/install-apply.js +195 -14
- package/scripts/lib/backups.js +13 -0
- package/scripts/lib/capabilities.js +19 -2
- package/scripts/lib/divergence.js +8 -1
- package/scripts/lib/onboarding-apply.js +91 -13
- package/scripts/lib/onboarding.js +35 -2
- package/scripts/lib/repair.js +8 -4
- package/scripts/lib/review-findings.js +175 -0
- package/scripts/lib/versions.js +31 -0
- package/scripts/rsc.js +117 -19
- package/skills/animar/SKILL.md +1 -0
- package/skills/claude-project-setup/SKILL.md +1 -0
- package/skills/code-review/SKILL.md +18 -1
- package/skills/diseno-apple/SKILL.md +1 -0
- package/skills/diseno-landing/SKILL.md +1 -0
- package/skills/domain-modeling/SKILL.md +1 -0
- package/skills/flujo-interfaz/SKILL.md +1 -0
- package/skills/grill-me/SKILL.md +1 -0
- package/skills/grill-with-docs/SKILL.md +1 -0
- package/skills/grilling/SKILL.md +1 -0
- package/skills/handoff/SKILL.md +1 -0
- package/skills/harness/SKILL.md +6 -0
- package/skills/harness/references/wiki-protocol.md +5 -0
- package/skills/implement/SKILL.md +1 -0
- package/skills/ingeniero-diseno-web/SKILL.md +1 -0
- package/skills/init/references/accompaniment-and-profile.md +1 -1
- package/skills/leyes-de-percepcion/SKILL.md +1 -0
- package/skills/leyes-de-retencion/SKILL.md +1 -0
- package/skills/mejor-accesibilidad/SKILL.md +1 -0
- package/skills/mejor-colores/SKILL.md +1 -0
- package/skills/mejor-layout/SKILL.md +1 -0
- package/skills/mejor-redaccion/SKILL.md +1 -0
- package/skills/mejor-tipografia/SKILL.md +1 -0
- package/skills/mejor-ui/SKILL.md +1 -0
- package/skills/muscle-memory/SKILL.md +1 -0
- package/skills/orient/SKILL.md +9 -1
- package/skills/orient/references/orientation-contract.md +1 -1
- package/skills/prototype/SKILL.md +1 -0
- package/skills/research/SKILL.md +1 -0
- package/skills/revision-de-cambios/SKILL.md +1 -0
- package/skills/revision-interfaz/SKILL.md +1 -0
- package/skills/sitios-calidad-premio/SKILL.md +1 -0
- package/skills/suggest/SKILL.md +10 -9
- package/skills/tastemaker/SKILL.md +1 -0
- package/skills/teach/SKILL.md +1 -0
- package/skills/to-questionnaire/SKILL.md +1 -0
- package/skills/to-spec/SKILL.md +1 -0
- package/skills/to-tickets/SKILL.md +1 -0
- package/skills/video-a-superprompt/SKILL.md +1 -0
- package/skills/vocabulario-animacion/SKILL.md +1 -0
- package/skills/wait-what/SKILL.md +1 -0
- package/skills/wayfinder/SKILL.md +1 -0
- package/skills/write-adr/SKILL.md +1 -0
- package/skills/writing-for-agents/SKILL.md +1 -0
- package/targets/_md-block.js +8 -1
- package/targets/agents-md-shadow.js +153 -0
- package/targets/agents.js +46 -3
- package/targets/claude.js +76 -26
- package/targets/clone-bootstrap.mjs +454 -0
- package/targets/gitmoji-guard.mjs +30 -3
- package/targets/hook-once.mjs +1 -0
- package/targets/memory.js +1 -1
- package/targets/opt-outs.js +40 -0
- package/targets/sello.mjs +2 -2
- package/targets/session-memory-adapter.mjs +70 -14
- package/targets/session-memory-core.mjs +40 -8
- package/targets/session-start.mjs +65 -12
- package/targets/ship-guard.mjs +1 -1
- 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 **
|
|
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)
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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,
|
|
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.') &&
|
|
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', '
|
|
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
|
}
|