@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.
- package/README.md +31 -9
- package/manifest.json +1 -1
- package/package.json +3 -2
- package/scripts/doctor.js +109 -2
- package/scripts/install-apply.js +127 -7
- package/scripts/lib/capabilities.js +19 -2
- package/scripts/lib/divergence.js +8 -1
- package/scripts/lib/onboarding-apply.js +18 -1
- package/scripts/lib/onboarding.js +27 -2
- package/scripts/lib/versions.js +31 -0
- package/scripts/rsc.js +66 -16
- package/skills/animar/SKILL.md +1 -0
- package/skills/claude-project-setup/SKILL.md +1 -0
- package/skills/code-review/SKILL.md +1 -0
- 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/implement/SKILL.md +1 -0
- package/skills/ingeniero-diseno-web/SKILL.md +1 -0
- 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/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 +9 -2
- package/targets/claude.js +52 -11
- package/targets/clone-bootstrap.mjs +454 -0
- package/targets/gitmoji-guard.mjs +30 -3
- package/targets/opt-outs.js +40 -0
- package/targets/session-memory-adapter.mjs +70 -14
- package/targets/session-memory-core.mjs +45 -7
- package/targets/session-start.mjs +63 -10
- 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)
|
|
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 =
|
|
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,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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
}
|