@lifeaitools/rdc-skills 0.24.41 → 0.25.0
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/.claude/settings.json +15 -15
- package/.claude-plugin/marketplace.json +21 -21
- package/.claude-plugin/plugin.json +1518 -1371
- package/.github/workflows/publish.yml +34 -34
- package/.github/workflows/self-test.yml +58 -58
- package/CHANGELOG.md +310 -310
- package/LICENSE +21 -21
- package/MANIFEST.md +221 -221
- package/README.md +375 -376
- package/README.sandbox.md +3 -3
- package/assets/watcher/viewer.html +164 -164
- package/bin/rdc-skills-mcp.mjs +316 -316
- package/commands/build.md +183 -183
- package/commands/collab.md +180 -180
- package/commands/deploy.md +152 -152
- package/commands/design.md +31 -31
- package/commands/edit.md +28 -28
- package/commands/fixit.md +150 -124
- package/commands/handoff.md +173 -173
- package/commands/help.md +95 -95
- package/commands/overnight.md +220 -220
- package/commands/plan.md +158 -158
- package/commands/preplan.md +131 -131
- package/commands/prototype.md +145 -145
- package/commands/release.md +49 -49
- package/commands/report.md +99 -99
- package/commands/review.md +120 -120
- package/commands/self-test.md +113 -113
- package/commands/status.md +86 -86
- package/commands/watch.md +98 -98
- package/commands/workitems.md +137 -137
- package/git-sha.json +1 -1
- package/guides/agent-bootstrap.md +295 -295
- package/guides/agents/backend.md +104 -104
- package/guides/agents/content.md +94 -94
- package/guides/agents/cs2.md +56 -56
- package/guides/agents/data.md +87 -87
- package/guides/agents/design.md +77 -77
- package/guides/agents/frontend.md +92 -92
- package/guides/agents/infrastructure.md +81 -81
- package/guides/agents/setup.md +281 -281
- package/guides/agents/verify.md +151 -151
- package/guides/agents/viz.md +106 -106
- package/guides/backend.md +146 -146
- package/guides/content.md +147 -147
- package/guides/cs2.md +190 -190
- package/guides/data.md +123 -123
- package/guides/design.md +116 -116
- package/guides/engineering-behavior.md +43 -43
- package/guides/escalation-protocol.md +125 -125
- package/guides/frontend.md +151 -151
- package/guides/history-md-spec.md +297 -297
- package/guides/infrastructure.md +179 -179
- package/guides/lessons-learned-spec.md +145 -151
- package/guides/output-contract.md +108 -108
- package/guides/publish-md-spec.md +289 -289
- package/guides/rdc-skills-startup.md +30 -30
- package/guides/verify.md +11 -11
- package/hooks/check-cwd.js +31 -31
- package/hooks/check-rdc-environment.js +164 -164
- package/hooks/check-services.js +6 -6
- package/hooks/check-stale-work-items.js +19 -19
- package/hooks/foreground-process-gate.js +128 -128
- package/hooks/gate-watchdog-selfcheck.js +257 -257
- package/hooks/hook-logger.js +25 -25
- package/hooks/lib/run-evidence-gate.mjs +241 -241
- package/hooks/no-stop-open-epics.js +127 -127
- package/hooks/post-tool-batch-gate.js +203 -203
- package/hooks/post-work-check.js +21 -21
- package/hooks/postcompact-log.js +13 -13
- package/hooks/precompact-log.js +13 -13
- package/hooks/rate-limit-retry.js +46 -46
- package/hooks/rdc-invocation-marker.js +157 -157
- package/hooks/rdc-output-contract-gate.js +94 -94
- package/hooks/require-work-item-on-commit.js +294 -294
- package/hooks/restart-brief.js +19 -19
- package/hooks/run-hidden-hook.ps1 +47 -47
- package/hooks/task-completed-gate.js +274 -274
- package/hooks/work-item-exit-gate.js +944 -944
- package/lib/catalog.mjs +236 -236
- package/lib/cloud-rewrite.mjs +155 -155
- package/package.json +57 -57
- package/rules/work-items-rpc.md +520 -520
- package/scaffold/templates/HISTORY.md.template +39 -39
- package/scaffold/templates/PUBLISH.md.template +21 -21
- package/scaffold/templates/brochure-studio-default.html +70 -70
- package/scripts/acceptance.mjs +502 -502
- package/scripts/fixtures/guides/bad-guide.md +15 -15
- package/scripts/fixtures/guides-clean/good-guide.md +16 -16
- package/scripts/install-rdc-skills.js +1289 -1289
- package/scripts/install.ps1 +202 -202
- package/scripts/install.sh +132 -132
- package/scripts/lib/assertions.mjs +287 -287
- package/scripts/lib/manifest-schema.mjs +754 -754
- package/scripts/lib/runner.mjs +465 -465
- package/scripts/lib/sandbox.mjs +435 -435
- package/scripts/prepack.mjs +32 -32
- package/scripts/rdc-brochure.mjs +482 -482
- package/scripts/rdc-design-cli.mjs +134 -134
- package/scripts/rebuild-mcp.mjs +107 -107
- package/scripts/self-test.mjs +1460 -1460
- package/scripts/stamp-git-sha.mjs +29 -29
- package/scripts/test-guide-validator.mjs +196 -196
- package/scripts/test-rdc-hooks.mjs +145 -145
- package/scripts/uninstall.ps1 +77 -77
- package/scripts/uninstall.sh +69 -69
- package/scripts/update.ps1 +43 -43
- package/scripts/update.sh +43 -43
- package/scripts/validate-place-histories.js +461 -461
- package/scripts/validate-publish-manifests.js +502 -424
- package/scripts/watch-init.mjs +100 -100
- package/skills/brochure/SKILL.md +107 -107
- package/skills/build/SKILL.md +563 -563
- package/skills/channel-formatter/SKILL.md +538 -533
- package/skills/co-develop/SKILL.md +196 -196
- package/skills/collab/SKILL.md +239 -239
- package/skills/convert/SKILL.md +167 -140
- package/skills/deploy/SKILL.md +541 -541
- package/skills/design/SKILL.md +211 -211
- package/skills/design/reference/ownership.md +16 -16
- package/skills/design/reference/rampa.md +92 -92
- package/skills/design/reference/studio-model.md +153 -153
- package/skills/edit/SKILL.md +98 -98
- package/skills/fixit/SKILL.md +203 -165
- package/skills/fs-mcp/SKILL.md +148 -148
- package/skills/handoff/SKILL.md +236 -236
- package/skills/help/SKILL.md +143 -143
- package/skills/housekeeping/SKILL.md +160 -219
- package/skills/lifeai-brochure-author/SKILL.md +340 -340
- package/skills/onramp/SKILL.md +248 -0
- package/skills/overnight/SKILL.md +251 -251
- package/skills/plan/SKILL.md +345 -345
- package/skills/preplan/SKILL.md +90 -90
- package/skills/prototype/SKILL.md +150 -150
- package/skills/rdc-brochurify/SKILL.md +245 -245
- package/skills/rdc-extract-verifier-rules/SKILL.md +191 -191
- package/skills/release/SKILL.md +140 -140
- package/skills/report/SKILL.md +100 -100
- package/skills/review/SKILL.md +152 -152
- package/skills/rpms-filemap/SKILL.cloud.md +111 -111
- package/skills/rpms-filemap/SKILL.md +111 -111
- package/skills/self-test/SKILL.md +132 -132
- package/skills/status/SKILL.md +99 -99
- package/skills/terminal-config/SKILL.md +62 -62
- package/skills/tests/MATRIX.md +54 -54
- package/skills/tests/README.md +47 -47
- package/skills/tests/onramp.test.json +87 -0
- package/skills/tests/rdc-brochure.test.json +34 -34
- package/skills/tests/rdc-build.test.json +36 -36
- package/skills/tests/rdc-channel-formatter.test.json +45 -45
- package/skills/tests/rdc-co-develop.test.json +29 -29
- package/skills/tests/rdc-collab.test.json +29 -29
- package/skills/tests/rdc-convert.test.json +35 -35
- package/skills/tests/rdc-deploy.test.json +30 -30
- package/skills/tests/rdc-design.test.json +27 -27
- package/skills/tests/rdc-edit.test.json +29 -29
- package/skills/tests/rdc-fixit.test.json +36 -36
- package/skills/tests/rdc-fs-mcp.test.json +36 -36
- package/skills/tests/rdc-handoff.test.json +28 -28
- package/skills/tests/rdc-help.test.json +29 -29
- package/skills/tests/rdc-housekeeping.test.json +28 -32
- package/skills/tests/rdc-lifeai-brochure-author.test.json +35 -35
- package/skills/tests/rdc-overnight.test.json +37 -37
- package/skills/tests/rdc-plan.test.json +27 -27
- package/skills/tests/rdc-preplan.test.json +31 -31
- package/skills/tests/rdc-prototype.test.json +28 -28
- package/skills/tests/rdc-rdc-brochurify.test.json +23 -23
- package/skills/tests/rdc-rdc-extract-verifier-rules.test.json +34 -34
- package/skills/tests/rdc-release.test.json +29 -29
- package/skills/tests/rdc-report.test.json +28 -28
- package/skills/tests/rdc-review.test.json +29 -29
- package/skills/tests/rdc-rpms-filemap.test.json +28 -28
- package/skills/tests/rdc-self-test.test.json +24 -24
- package/skills/tests/rdc-status.test.json +29 -29
- package/skills/tests/rdc-terminal-config.test.json +29 -29
- package/skills/tests/rdc-watch.test.json +24 -24
- package/skills/tests/rdc-workitems.test.json +27 -27
- package/skills/watch/SKILL.md +97 -97
- package/skills/workitems/SKILL.md +151 -151
- package/tests/acceptance.test.mjs +59 -59
- package/tests/channel-formatter.contract.test.mjs +251 -251
- package/tests/curl-surface.test.mjs +289 -289
- package/tests/harness-gates.test.mjs +325 -325
- package/tests/help-surface.test.mjs +61 -61
- package/tests/install-rdc-skills.test.mjs +49 -49
- package/tests/manifest-contract-fields.test.mjs +78 -78
- package/tests/mcp.test.mjs +271 -271
- package/tests/rdc-brochure.test.mjs +125 -125
- package/tests/require-work-item-on-commit.test.mjs +162 -162
- package/tests/run-evidence-gate.test.mjs +82 -82
- package/tests/skill-test-matrix.test.mjs +66 -66
- package/tests/validate-skills.js +27 -27
- package/tests/work-item-exit-gate-l2.test.mjs +368 -368
- package/tests/work-item-exit-gate-l3.test.mjs +197 -197
- package/RELEASE.md +0 -42
- package/tests/housekeeping-lessons-triage.test.mjs +0 -49
- package/tests/lessons-pipeline-contract.test.mjs +0 -26
- package/tests/release-contract.test.mjs +0 -16
|
@@ -1,257 +1,257 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* gate-watchdog-selfcheck.js — Truth Gate 3.0 Layer 6 (gate watchdog).
|
|
4
|
-
*
|
|
5
|
-
* Registered on `SessionStart`. Per the Claude Code hooks reference,
|
|
6
|
-
* `SessionStart` CANNOT block — so this watchdog is ADVISORY: it prints a loud
|
|
7
|
-
* STOP-banner (like the existing agent-startup guard) when a Truth-Gate
|
|
8
|
-
* component is missing or unregistered, but it always exits 0. A disabled or
|
|
9
|
-
* absent gate is the ultimate bypass (R4); this watchdog surfaces that the
|
|
10
|
-
* moment a session starts, before any work trusts the gate chain.
|
|
11
|
-
*
|
|
12
|
-
* It asserts three things are present + registered:
|
|
13
|
-
* 1. work-item-exit-gate.js — exists in the installed rdc-skills hooks AND is
|
|
14
|
-
* referenced by a registered hook command (PreToolUse).
|
|
15
|
-
* 2. truth-gate.mjs — exists in the repo's .claude/hooks AND is
|
|
16
|
-
* registered on Stop and SubagentStop in .claude/settings.json.
|
|
17
|
-
* 3. the LIVE hookify plugin hooks.json — the plugin's OWN hooks manifest under
|
|
18
|
-
* ~/.claude/plugins/cache/.../hookify/<hash>/hooks/hooks.json.
|
|
19
|
-
* ⛔ It deliberately does NOT trust the dead ~/.claude/hooks/hookify-*.js
|
|
20
|
-
* wrappers — those point at an ORPHANED cache hash and are not the live
|
|
21
|
-
* enforcement path. The watchdog resolves the live manifest and (when both
|
|
22
|
-
* exist) warns if the dead wrappers point at a cache hash that differs from
|
|
23
|
-
* the live one (the exact drift that silently disables hookify).
|
|
24
|
-
*
|
|
25
|
-
* Test seam: the pure decision is `evaluateWatchdog(facts)` where `facts` is a
|
|
26
|
-
* plain object of booleans/paths gathered by the side-effecting probes. The
|
|
27
|
-
* banner text is produced by `renderBanner(findings)`. Both are exported and
|
|
28
|
-
* have no filesystem/process dependency, so the missing-gate banner and the
|
|
29
|
-
* all-present silent path are provable offline.
|
|
30
|
-
*/
|
|
31
|
-
'use strict';
|
|
32
|
-
|
|
33
|
-
const fs = require('fs');
|
|
34
|
-
const os = require('os');
|
|
35
|
-
const path = require('path');
|
|
36
|
-
|
|
37
|
-
const REPO_ROOT = process.env.RDC_TRUTH_GATE_REPO || 'C:/Dev/regen-root';
|
|
38
|
-
const HOOKS_DIR = __dirname; // installed rdc-skills hooks dir (where this file lives)
|
|
39
|
-
|
|
40
|
-
function readJsonSafe(p) {
|
|
41
|
-
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) { return null; }
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Serialize all hook command strings in a settings.json hooks object. */
|
|
45
|
-
function settingsCommandText(settings) {
|
|
46
|
-
if (!settings || !settings.hooks || typeof settings.hooks !== 'object') return '';
|
|
47
|
-
const parts = [];
|
|
48
|
-
for (const event of Object.keys(settings.hooks)) {
|
|
49
|
-
const groups = Array.isArray(settings.hooks[event]) ? settings.hooks[event] : [];
|
|
50
|
-
for (const g of groups) {
|
|
51
|
-
for (const h of Array.isArray(g.hooks) ? g.hooks : []) {
|
|
52
|
-
if (h && typeof h.command === 'string') parts.push(`${event}::${h.command}`);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return parts.join('\n');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Is a hook command referenced for the given event in the serialized text? */
|
|
60
|
-
function registeredForEvent(commandText, event, needle) {
|
|
61
|
-
return commandText.split('\n').some((line) => line.startsWith(`${event}::`) && line.includes(needle));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Locate the LIVE hookify plugin hooks.json under the plugins cache. Returns the
|
|
66
|
-
* resolved manifest path (the plugin's OWN hooks/hooks.json), or null. NEVER
|
|
67
|
-
* returns one of the dead ~/.claude/hooks/hookify-*.js wrappers.
|
|
68
|
-
*/
|
|
69
|
-
function findLiveHookifyManifest(home) {
|
|
70
|
-
const cacheRoot = path.join(home, '.claude', 'plugins', 'cache');
|
|
71
|
-
const found = [];
|
|
72
|
-
const walk = (dir, depth) => {
|
|
73
|
-
if (depth > 6) return;
|
|
74
|
-
let entries;
|
|
75
|
-
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
|
|
76
|
-
for (const e of entries) {
|
|
77
|
-
const full = path.join(dir, e.name);
|
|
78
|
-
if (e.isDirectory()) {
|
|
79
|
-
walk(full, depth + 1);
|
|
80
|
-
} else if (e.isFile() && e.name === 'hooks.json' && /hookify/i.test(full)) {
|
|
81
|
-
found.push(full);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
walk(cacheRoot, 0);
|
|
86
|
-
return found.length > 0 ? found[0] : null;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** Read the cache-hash a dead hookify wrapper points at (its PLUGIN_ROOT). null if none. */
|
|
90
|
-
function deadWrapperPluginRoot(home) {
|
|
91
|
-
const wrapper = path.join(home, '.claude', 'hooks', 'hookify-stop.js');
|
|
92
|
-
try {
|
|
93
|
-
const src = fs.readFileSync(wrapper, 'utf8');
|
|
94
|
-
const m = src.match(/PLUGIN_ROOT\s*=\s*['"]([^'"]+)['"]/);
|
|
95
|
-
return m ? m[1] : null;
|
|
96
|
-
} catch (_) {
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Gather the watchdog facts via side-effecting probes. Returns a plain facts
|
|
103
|
-
* object consumed by the pure evaluator. Kept separate from evaluateWatchdog so
|
|
104
|
-
* the decision logic is unit-testable without a filesystem.
|
|
105
|
-
*/
|
|
106
|
-
function gatherFacts({ home = os.homedir(), repoRoot = REPO_ROOT, hooksDir = HOOKS_DIR } = {}) {
|
|
107
|
-
const exitGatePath = path.join(hooksDir, 'work-item-exit-gate.js');
|
|
108
|
-
const truthGatePath = path.join(repoRoot, '.claude', 'hooks', 'truth-gate.mjs');
|
|
109
|
-
// Claude Code enforces the MERGED user+project hook set. A gate registered in
|
|
110
|
-
// EITHER ~/.claude/settings.json (user) OR <repo>/.claude/settings.json (project)
|
|
111
|
-
// is live. Reading only the project file false-positives on user-registered
|
|
112
|
-
// gates — e.g. work-item-exit-gate.js is registered in user settings, so a
|
|
113
|
-
// project-only read wrongly reported it "unregistered" every session.
|
|
114
|
-
const projectSettingsPath = path.join(repoRoot, '.claude', 'settings.json');
|
|
115
|
-
const userSettingsPath = path.join(home, '.claude', 'settings.json');
|
|
116
|
-
const projectSettings = readJsonSafe(projectSettingsPath);
|
|
117
|
-
const userSettings = readJsonSafe(userSettingsPath);
|
|
118
|
-
const commandText = [settingsCommandText(userSettings), settingsCommandText(projectSettings)]
|
|
119
|
-
.filter(Boolean)
|
|
120
|
-
.join('\n');
|
|
121
|
-
const settingsPath = projectSettingsPath; // retained for banner/log compatibility
|
|
122
|
-
|
|
123
|
-
const liveHookifyManifest = findLiveHookifyManifest(home);
|
|
124
|
-
const deadRoot = deadWrapperPluginRoot(home);
|
|
125
|
-
// The live manifest's plugin-root dir (the parent of /hooks/hooks.json).
|
|
126
|
-
const liveRoot = liveHookifyManifest
|
|
127
|
-
? path.dirname(path.dirname(liveHookifyManifest))
|
|
128
|
-
: null;
|
|
129
|
-
|
|
130
|
-
return {
|
|
131
|
-
exitGateFileExists: fs.existsSync(exitGatePath),
|
|
132
|
-
exitGateRegistered: registeredForEvent(commandText, 'PreToolUse', 'work-item-exit-gate.js'),
|
|
133
|
-
truthGateFileExists: fs.existsSync(truthGatePath),
|
|
134
|
-
truthGateOnStop: registeredForEvent(commandText, 'Stop', 'truth-gate.mjs'),
|
|
135
|
-
truthGateOnSubagentStop: registeredForEvent(commandText, 'SubagentStop', 'truth-gate.mjs'),
|
|
136
|
-
liveHookifyManifestExists: Boolean(liveHookifyManifest),
|
|
137
|
-
liveHookifyManifestPath: liveHookifyManifest,
|
|
138
|
-
// Drift: a dead wrapper that points at a different cache hash than the live one.
|
|
139
|
-
deadWrapperRoot: deadRoot,
|
|
140
|
-
liveHookifyRoot: liveRoot,
|
|
141
|
-
deadWrapperPointsAtStaleCache: Boolean(
|
|
142
|
-
deadRoot && liveRoot &&
|
|
143
|
-
path.normalize(deadRoot).toLowerCase() !== path.normalize(liveRoot).toLowerCase(),
|
|
144
|
-
),
|
|
145
|
-
// paths for the banner / log
|
|
146
|
-
exitGatePath,
|
|
147
|
-
truthGatePath,
|
|
148
|
-
settingsPath,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Pure decision over the gathered facts. Returns an array of finding strings
|
|
154
|
-
* (empty = all gates healthy). The live hookify check is explicitly the PLUGIN
|
|
155
|
-
* manifest, not the dead wrappers.
|
|
156
|
-
*/
|
|
157
|
-
function evaluateWatchdog(facts = {}) {
|
|
158
|
-
const findings = [];
|
|
159
|
-
if (!facts.exitGateFileExists) {
|
|
160
|
-
findings.push(`work-item-exit-gate.js is MISSING from the installed hooks (${facts.exitGatePath || '?'}).`);
|
|
161
|
-
}
|
|
162
|
-
if (!facts.exitGateRegistered) {
|
|
163
|
-
findings.push('work-item-exit-gate.js is NOT registered on a PreToolUse hook in .claude/settings.json.');
|
|
164
|
-
}
|
|
165
|
-
if (!facts.truthGateFileExists) {
|
|
166
|
-
findings.push(`truth-gate.mjs is MISSING from the repo (${facts.truthGatePath || '?'}).`);
|
|
167
|
-
}
|
|
168
|
-
if (!facts.truthGateOnStop) {
|
|
169
|
-
findings.push('truth-gate.mjs is NOT registered on the Stop event in .claude/settings.json.');
|
|
170
|
-
}
|
|
171
|
-
if (!facts.truthGateOnSubagentStop) {
|
|
172
|
-
findings.push('truth-gate.mjs is NOT registered on the SubagentStop event in .claude/settings.json.');
|
|
173
|
-
}
|
|
174
|
-
if (!facts.liveHookifyManifestExists) {
|
|
175
|
-
findings.push(
|
|
176
|
-
'The LIVE hookify plugin hooks.json could not be found under ~/.claude/plugins/cache/**/hookify/**/hooks/hooks.json. ' +
|
|
177
|
-
'Hookify enforcement may be disabled. (The dead ~/.claude/hooks/hookify-*.js wrappers are NOT the live path and are not trusted here.)',
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
if (facts.deadWrapperPointsAtStaleCache) {
|
|
181
|
-
findings.push(
|
|
182
|
-
`The dead ~/.claude/hooks/hookify-*.js wrappers point at an ORPHANED cache hash (${facts.deadWrapperRoot}) ` +
|
|
183
|
-
`that differs from the live plugin (${facts.liveHookifyRoot}). The wrappers are stale; the live plugin manifest is authoritative.`,
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
return findings;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/** Render the loud STOP-banner from findings. Empty findings → empty string. */
|
|
190
|
-
function renderBanner(findings) {
|
|
191
|
-
if (!Array.isArray(findings) || findings.length === 0) return '';
|
|
192
|
-
const lines = findings.map((f) => ` ⛔ ${f}`).join('\n');
|
|
193
|
-
return (
|
|
194
|
-
'\n' +
|
|
195
|
-
'════════════════════════════════════════════════════════════════════════\n' +
|
|
196
|
-
'⛔⛔ TRUTH GATE WATCHDOG — A GATE IS MISSING OR UNREGISTERED ⛔⛔\n' +
|
|
197
|
-
'════════════════════════════════════════════════════════════════════════\n' +
|
|
198
|
-
'The Truth Gate enforcement chain is INCOMPLETE for this session. A disabled\n' +
|
|
199
|
-
'or unregistered gate is the ultimate closure bypass. Findings:\n\n' +
|
|
200
|
-
lines + '\n\n' +
|
|
201
|
-
'This watchdog evaluates the MERGED user+project hook set; a gate registered in\n' +
|
|
202
|
-
'~/.claude/settings.json (user) counts as registered. Repair by gate:\n' +
|
|
203
|
-
' - work-item-exit-gate.js (user-level, installer-managed):\n' +
|
|
204
|
-
' node C:/Dev/rdc-skills/scripts/install-rdc-skills.js (reinstalls user hooks)\n' +
|
|
205
|
-
' - truth-gate.mjs (repo-local, NOT installer-managed): register it on Stop+SubagentStop\n' +
|
|
206
|
-
' in <repo>/.claude/settings.json by hand — the installer does not wire this hook.\n' +
|
|
207
|
-
'════════════════════════════════════════════════════════════════════════\n'
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function main() {
|
|
212
|
-
// Drain stdin if present (SessionStart delivers JSON); we don't need its body.
|
|
213
|
-
try {
|
|
214
|
-
const chunks = [];
|
|
215
|
-
process.stdin.on('data', (c) => chunks.push(c));
|
|
216
|
-
process.stdin.on('end', () => finish());
|
|
217
|
-
process.stdin.resume();
|
|
218
|
-
// If no stdin arrives quickly, finish anyway (SessionStart may pass nothing).
|
|
219
|
-
setTimeout(finish, 1500);
|
|
220
|
-
} catch (_) {
|
|
221
|
-
finish();
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
let _finished = false;
|
|
226
|
-
function finish() {
|
|
227
|
-
if (_finished) return;
|
|
228
|
-
_finished = true;
|
|
229
|
-
let findings = [];
|
|
230
|
-
try {
|
|
231
|
-
findings = evaluateWatchdog(gatherFacts());
|
|
232
|
-
} catch (e) {
|
|
233
|
-
// Even the watchdog crashing must not block the session — advisory only.
|
|
234
|
-
findings = [`gate-watchdog self-check crashed: ${e && e.message ? e.message : String(e)}`];
|
|
235
|
-
}
|
|
236
|
-
const banner = renderBanner(findings);
|
|
237
|
-
if (banner) {
|
|
238
|
-
// SessionStart context output via systemMessage; also emit to stderr so it
|
|
239
|
-
// is visible like the agent-startup guard. ADVISORY — always exit 0.
|
|
240
|
-
process.stdout.write(JSON.stringify({ systemMessage: banner }));
|
|
241
|
-
process.stderr.write(banner);
|
|
242
|
-
}
|
|
243
|
-
process.exit(0); // SessionStart cannot block — never exit non-zero.
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (require.main === module) {
|
|
247
|
-
main();
|
|
248
|
-
} else {
|
|
249
|
-
module.exports = {
|
|
250
|
-
evaluateWatchdog,
|
|
251
|
-
renderBanner,
|
|
252
|
-
gatherFacts,
|
|
253
|
-
findLiveHookifyManifest,
|
|
254
|
-
settingsCommandText,
|
|
255
|
-
registeredForEvent,
|
|
256
|
-
};
|
|
257
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* gate-watchdog-selfcheck.js — Truth Gate 3.0 Layer 6 (gate watchdog).
|
|
4
|
+
*
|
|
5
|
+
* Registered on `SessionStart`. Per the Claude Code hooks reference,
|
|
6
|
+
* `SessionStart` CANNOT block — so this watchdog is ADVISORY: it prints a loud
|
|
7
|
+
* STOP-banner (like the existing agent-startup guard) when a Truth-Gate
|
|
8
|
+
* component is missing or unregistered, but it always exits 0. A disabled or
|
|
9
|
+
* absent gate is the ultimate bypass (R4); this watchdog surfaces that the
|
|
10
|
+
* moment a session starts, before any work trusts the gate chain.
|
|
11
|
+
*
|
|
12
|
+
* It asserts three things are present + registered:
|
|
13
|
+
* 1. work-item-exit-gate.js — exists in the installed rdc-skills hooks AND is
|
|
14
|
+
* referenced by a registered hook command (PreToolUse).
|
|
15
|
+
* 2. truth-gate.mjs — exists in the repo's .claude/hooks AND is
|
|
16
|
+
* registered on Stop and SubagentStop in .claude/settings.json.
|
|
17
|
+
* 3. the LIVE hookify plugin hooks.json — the plugin's OWN hooks manifest under
|
|
18
|
+
* ~/.claude/plugins/cache/.../hookify/<hash>/hooks/hooks.json.
|
|
19
|
+
* ⛔ It deliberately does NOT trust the dead ~/.claude/hooks/hookify-*.js
|
|
20
|
+
* wrappers — those point at an ORPHANED cache hash and are not the live
|
|
21
|
+
* enforcement path. The watchdog resolves the live manifest and (when both
|
|
22
|
+
* exist) warns if the dead wrappers point at a cache hash that differs from
|
|
23
|
+
* the live one (the exact drift that silently disables hookify).
|
|
24
|
+
*
|
|
25
|
+
* Test seam: the pure decision is `evaluateWatchdog(facts)` where `facts` is a
|
|
26
|
+
* plain object of booleans/paths gathered by the side-effecting probes. The
|
|
27
|
+
* banner text is produced by `renderBanner(findings)`. Both are exported and
|
|
28
|
+
* have no filesystem/process dependency, so the missing-gate banner and the
|
|
29
|
+
* all-present silent path are provable offline.
|
|
30
|
+
*/
|
|
31
|
+
'use strict';
|
|
32
|
+
|
|
33
|
+
const fs = require('fs');
|
|
34
|
+
const os = require('os');
|
|
35
|
+
const path = require('path');
|
|
36
|
+
|
|
37
|
+
const REPO_ROOT = process.env.RDC_TRUTH_GATE_REPO || 'C:/Dev/regen-root';
|
|
38
|
+
const HOOKS_DIR = __dirname; // installed rdc-skills hooks dir (where this file lives)
|
|
39
|
+
|
|
40
|
+
function readJsonSafe(p) {
|
|
41
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) { return null; }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Serialize all hook command strings in a settings.json hooks object. */
|
|
45
|
+
function settingsCommandText(settings) {
|
|
46
|
+
if (!settings || !settings.hooks || typeof settings.hooks !== 'object') return '';
|
|
47
|
+
const parts = [];
|
|
48
|
+
for (const event of Object.keys(settings.hooks)) {
|
|
49
|
+
const groups = Array.isArray(settings.hooks[event]) ? settings.hooks[event] : [];
|
|
50
|
+
for (const g of groups) {
|
|
51
|
+
for (const h of Array.isArray(g.hooks) ? g.hooks : []) {
|
|
52
|
+
if (h && typeof h.command === 'string') parts.push(`${event}::${h.command}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return parts.join('\n');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Is a hook command referenced for the given event in the serialized text? */
|
|
60
|
+
function registeredForEvent(commandText, event, needle) {
|
|
61
|
+
return commandText.split('\n').some((line) => line.startsWith(`${event}::`) && line.includes(needle));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Locate the LIVE hookify plugin hooks.json under the plugins cache. Returns the
|
|
66
|
+
* resolved manifest path (the plugin's OWN hooks/hooks.json), or null. NEVER
|
|
67
|
+
* returns one of the dead ~/.claude/hooks/hookify-*.js wrappers.
|
|
68
|
+
*/
|
|
69
|
+
function findLiveHookifyManifest(home) {
|
|
70
|
+
const cacheRoot = path.join(home, '.claude', 'plugins', 'cache');
|
|
71
|
+
const found = [];
|
|
72
|
+
const walk = (dir, depth) => {
|
|
73
|
+
if (depth > 6) return;
|
|
74
|
+
let entries;
|
|
75
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
|
|
76
|
+
for (const e of entries) {
|
|
77
|
+
const full = path.join(dir, e.name);
|
|
78
|
+
if (e.isDirectory()) {
|
|
79
|
+
walk(full, depth + 1);
|
|
80
|
+
} else if (e.isFile() && e.name === 'hooks.json' && /hookify/i.test(full)) {
|
|
81
|
+
found.push(full);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
walk(cacheRoot, 0);
|
|
86
|
+
return found.length > 0 ? found[0] : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Read the cache-hash a dead hookify wrapper points at (its PLUGIN_ROOT). null if none. */
|
|
90
|
+
function deadWrapperPluginRoot(home) {
|
|
91
|
+
const wrapper = path.join(home, '.claude', 'hooks', 'hookify-stop.js');
|
|
92
|
+
try {
|
|
93
|
+
const src = fs.readFileSync(wrapper, 'utf8');
|
|
94
|
+
const m = src.match(/PLUGIN_ROOT\s*=\s*['"]([^'"]+)['"]/);
|
|
95
|
+
return m ? m[1] : null;
|
|
96
|
+
} catch (_) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Gather the watchdog facts via side-effecting probes. Returns a plain facts
|
|
103
|
+
* object consumed by the pure evaluator. Kept separate from evaluateWatchdog so
|
|
104
|
+
* the decision logic is unit-testable without a filesystem.
|
|
105
|
+
*/
|
|
106
|
+
function gatherFacts({ home = os.homedir(), repoRoot = REPO_ROOT, hooksDir = HOOKS_DIR } = {}) {
|
|
107
|
+
const exitGatePath = path.join(hooksDir, 'work-item-exit-gate.js');
|
|
108
|
+
const truthGatePath = path.join(repoRoot, '.claude', 'hooks', 'truth-gate.mjs');
|
|
109
|
+
// Claude Code enforces the MERGED user+project hook set. A gate registered in
|
|
110
|
+
// EITHER ~/.claude/settings.json (user) OR <repo>/.claude/settings.json (project)
|
|
111
|
+
// is live. Reading only the project file false-positives on user-registered
|
|
112
|
+
// gates — e.g. work-item-exit-gate.js is registered in user settings, so a
|
|
113
|
+
// project-only read wrongly reported it "unregistered" every session.
|
|
114
|
+
const projectSettingsPath = path.join(repoRoot, '.claude', 'settings.json');
|
|
115
|
+
const userSettingsPath = path.join(home, '.claude', 'settings.json');
|
|
116
|
+
const projectSettings = readJsonSafe(projectSettingsPath);
|
|
117
|
+
const userSettings = readJsonSafe(userSettingsPath);
|
|
118
|
+
const commandText = [settingsCommandText(userSettings), settingsCommandText(projectSettings)]
|
|
119
|
+
.filter(Boolean)
|
|
120
|
+
.join('\n');
|
|
121
|
+
const settingsPath = projectSettingsPath; // retained for banner/log compatibility
|
|
122
|
+
|
|
123
|
+
const liveHookifyManifest = findLiveHookifyManifest(home);
|
|
124
|
+
const deadRoot = deadWrapperPluginRoot(home);
|
|
125
|
+
// The live manifest's plugin-root dir (the parent of /hooks/hooks.json).
|
|
126
|
+
const liveRoot = liveHookifyManifest
|
|
127
|
+
? path.dirname(path.dirname(liveHookifyManifest))
|
|
128
|
+
: null;
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
exitGateFileExists: fs.existsSync(exitGatePath),
|
|
132
|
+
exitGateRegistered: registeredForEvent(commandText, 'PreToolUse', 'work-item-exit-gate.js'),
|
|
133
|
+
truthGateFileExists: fs.existsSync(truthGatePath),
|
|
134
|
+
truthGateOnStop: registeredForEvent(commandText, 'Stop', 'truth-gate.mjs'),
|
|
135
|
+
truthGateOnSubagentStop: registeredForEvent(commandText, 'SubagentStop', 'truth-gate.mjs'),
|
|
136
|
+
liveHookifyManifestExists: Boolean(liveHookifyManifest),
|
|
137
|
+
liveHookifyManifestPath: liveHookifyManifest,
|
|
138
|
+
// Drift: a dead wrapper that points at a different cache hash than the live one.
|
|
139
|
+
deadWrapperRoot: deadRoot,
|
|
140
|
+
liveHookifyRoot: liveRoot,
|
|
141
|
+
deadWrapperPointsAtStaleCache: Boolean(
|
|
142
|
+
deadRoot && liveRoot &&
|
|
143
|
+
path.normalize(deadRoot).toLowerCase() !== path.normalize(liveRoot).toLowerCase(),
|
|
144
|
+
),
|
|
145
|
+
// paths for the banner / log
|
|
146
|
+
exitGatePath,
|
|
147
|
+
truthGatePath,
|
|
148
|
+
settingsPath,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Pure decision over the gathered facts. Returns an array of finding strings
|
|
154
|
+
* (empty = all gates healthy). The live hookify check is explicitly the PLUGIN
|
|
155
|
+
* manifest, not the dead wrappers.
|
|
156
|
+
*/
|
|
157
|
+
function evaluateWatchdog(facts = {}) {
|
|
158
|
+
const findings = [];
|
|
159
|
+
if (!facts.exitGateFileExists) {
|
|
160
|
+
findings.push(`work-item-exit-gate.js is MISSING from the installed hooks (${facts.exitGatePath || '?'}).`);
|
|
161
|
+
}
|
|
162
|
+
if (!facts.exitGateRegistered) {
|
|
163
|
+
findings.push('work-item-exit-gate.js is NOT registered on a PreToolUse hook in .claude/settings.json.');
|
|
164
|
+
}
|
|
165
|
+
if (!facts.truthGateFileExists) {
|
|
166
|
+
findings.push(`truth-gate.mjs is MISSING from the repo (${facts.truthGatePath || '?'}).`);
|
|
167
|
+
}
|
|
168
|
+
if (!facts.truthGateOnStop) {
|
|
169
|
+
findings.push('truth-gate.mjs is NOT registered on the Stop event in .claude/settings.json.');
|
|
170
|
+
}
|
|
171
|
+
if (!facts.truthGateOnSubagentStop) {
|
|
172
|
+
findings.push('truth-gate.mjs is NOT registered on the SubagentStop event in .claude/settings.json.');
|
|
173
|
+
}
|
|
174
|
+
if (!facts.liveHookifyManifestExists) {
|
|
175
|
+
findings.push(
|
|
176
|
+
'The LIVE hookify plugin hooks.json could not be found under ~/.claude/plugins/cache/**/hookify/**/hooks/hooks.json. ' +
|
|
177
|
+
'Hookify enforcement may be disabled. (The dead ~/.claude/hooks/hookify-*.js wrappers are NOT the live path and are not trusted here.)',
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
if (facts.deadWrapperPointsAtStaleCache) {
|
|
181
|
+
findings.push(
|
|
182
|
+
`The dead ~/.claude/hooks/hookify-*.js wrappers point at an ORPHANED cache hash (${facts.deadWrapperRoot}) ` +
|
|
183
|
+
`that differs from the live plugin (${facts.liveHookifyRoot}). The wrappers are stale; the live plugin manifest is authoritative.`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
return findings;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Render the loud STOP-banner from findings. Empty findings → empty string. */
|
|
190
|
+
function renderBanner(findings) {
|
|
191
|
+
if (!Array.isArray(findings) || findings.length === 0) return '';
|
|
192
|
+
const lines = findings.map((f) => ` ⛔ ${f}`).join('\n');
|
|
193
|
+
return (
|
|
194
|
+
'\n' +
|
|
195
|
+
'════════════════════════════════════════════════════════════════════════\n' +
|
|
196
|
+
'⛔⛔ TRUTH GATE WATCHDOG — A GATE IS MISSING OR UNREGISTERED ⛔⛔\n' +
|
|
197
|
+
'════════════════════════════════════════════════════════════════════════\n' +
|
|
198
|
+
'The Truth Gate enforcement chain is INCOMPLETE for this session. A disabled\n' +
|
|
199
|
+
'or unregistered gate is the ultimate closure bypass. Findings:\n\n' +
|
|
200
|
+
lines + '\n\n' +
|
|
201
|
+
'This watchdog evaluates the MERGED user+project hook set; a gate registered in\n' +
|
|
202
|
+
'~/.claude/settings.json (user) counts as registered. Repair by gate:\n' +
|
|
203
|
+
' - work-item-exit-gate.js (user-level, installer-managed):\n' +
|
|
204
|
+
' node C:/Dev/rdc-skills/scripts/install-rdc-skills.js (reinstalls user hooks)\n' +
|
|
205
|
+
' - truth-gate.mjs (repo-local, NOT installer-managed): register it on Stop+SubagentStop\n' +
|
|
206
|
+
' in <repo>/.claude/settings.json by hand — the installer does not wire this hook.\n' +
|
|
207
|
+
'════════════════════════════════════════════════════════════════════════\n'
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function main() {
|
|
212
|
+
// Drain stdin if present (SessionStart delivers JSON); we don't need its body.
|
|
213
|
+
try {
|
|
214
|
+
const chunks = [];
|
|
215
|
+
process.stdin.on('data', (c) => chunks.push(c));
|
|
216
|
+
process.stdin.on('end', () => finish());
|
|
217
|
+
process.stdin.resume();
|
|
218
|
+
// If no stdin arrives quickly, finish anyway (SessionStart may pass nothing).
|
|
219
|
+
setTimeout(finish, 1500);
|
|
220
|
+
} catch (_) {
|
|
221
|
+
finish();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let _finished = false;
|
|
226
|
+
function finish() {
|
|
227
|
+
if (_finished) return;
|
|
228
|
+
_finished = true;
|
|
229
|
+
let findings = [];
|
|
230
|
+
try {
|
|
231
|
+
findings = evaluateWatchdog(gatherFacts());
|
|
232
|
+
} catch (e) {
|
|
233
|
+
// Even the watchdog crashing must not block the session — advisory only.
|
|
234
|
+
findings = [`gate-watchdog self-check crashed: ${e && e.message ? e.message : String(e)}`];
|
|
235
|
+
}
|
|
236
|
+
const banner = renderBanner(findings);
|
|
237
|
+
if (banner) {
|
|
238
|
+
// SessionStart context output via systemMessage; also emit to stderr so it
|
|
239
|
+
// is visible like the agent-startup guard. ADVISORY — always exit 0.
|
|
240
|
+
process.stdout.write(JSON.stringify({ systemMessage: banner }));
|
|
241
|
+
process.stderr.write(banner);
|
|
242
|
+
}
|
|
243
|
+
process.exit(0); // SessionStart cannot block — never exit non-zero.
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (require.main === module) {
|
|
247
|
+
main();
|
|
248
|
+
} else {
|
|
249
|
+
module.exports = {
|
|
250
|
+
evaluateWatchdog,
|
|
251
|
+
renderBanner,
|
|
252
|
+
gatherFacts,
|
|
253
|
+
findLiveHookifyManifest,
|
|
254
|
+
settingsCommandText,
|
|
255
|
+
registeredForEvent,
|
|
256
|
+
};
|
|
257
|
+
}
|
package/hooks/hook-logger.js
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const os = require('os');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
|
|
7
|
-
const logPath = path.join(os.homedir(), '.claude', 'hook-events.jsonl');
|
|
8
|
-
|
|
9
|
-
module.exports = function hookLog(hook, event, verdict, details = {}) {
|
|
10
|
-
try {
|
|
11
|
-
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
12
|
-
fs.appendFileSync(
|
|
13
|
-
logPath,
|
|
14
|
-
JSON.stringify({
|
|
15
|
-
ts: new Date().toISOString(),
|
|
16
|
-
hook,
|
|
17
|
-
event,
|
|
18
|
-
verdict,
|
|
19
|
-
details,
|
|
20
|
-
}) + '\n',
|
|
21
|
-
);
|
|
22
|
-
} catch (_) {
|
|
23
|
-
// Hooks should never fail just because telemetry could not be written.
|
|
24
|
-
}
|
|
25
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const logPath = path.join(os.homedir(), '.claude', 'hook-events.jsonl');
|
|
8
|
+
|
|
9
|
+
module.exports = function hookLog(hook, event, verdict, details = {}) {
|
|
10
|
+
try {
|
|
11
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
12
|
+
fs.appendFileSync(
|
|
13
|
+
logPath,
|
|
14
|
+
JSON.stringify({
|
|
15
|
+
ts: new Date().toISOString(),
|
|
16
|
+
hook,
|
|
17
|
+
event,
|
|
18
|
+
verdict,
|
|
19
|
+
details,
|
|
20
|
+
}) + '\n',
|
|
21
|
+
);
|
|
22
|
+
} catch (_) {
|
|
23
|
+
// Hooks should never fail just because telemetry could not be written.
|
|
24
|
+
}
|
|
25
|
+
};
|