@lifeaitools/rdc-skills 0.24.42 → 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.
Files changed (198) hide show
  1. package/.claude/settings.json +15 -15
  2. package/.claude-plugin/marketplace.json +21 -21
  3. package/.claude-plugin/plugin.json +1518 -1371
  4. package/.github/workflows/publish.yml +34 -34
  5. package/.github/workflows/self-test.yml +58 -58
  6. package/CHANGELOG.md +310 -310
  7. package/LICENSE +21 -21
  8. package/MANIFEST.md +221 -221
  9. package/README.md +375 -376
  10. package/README.sandbox.md +3 -3
  11. package/assets/watcher/viewer.html +164 -164
  12. package/bin/rdc-skills-mcp.mjs +316 -316
  13. package/commands/build.md +183 -183
  14. package/commands/collab.md +180 -180
  15. package/commands/deploy.md +152 -152
  16. package/commands/design.md +31 -31
  17. package/commands/edit.md +28 -28
  18. package/commands/fixit.md +150 -124
  19. package/commands/handoff.md +173 -173
  20. package/commands/help.md +95 -95
  21. package/commands/overnight.md +220 -220
  22. package/commands/plan.md +158 -158
  23. package/commands/preplan.md +131 -131
  24. package/commands/prototype.md +145 -145
  25. package/commands/release.md +49 -49
  26. package/commands/report.md +99 -99
  27. package/commands/review.md +120 -120
  28. package/commands/self-test.md +113 -113
  29. package/commands/status.md +86 -86
  30. package/commands/watch.md +98 -98
  31. package/commands/workitems.md +137 -137
  32. package/git-sha.json +1 -1
  33. package/guides/agent-bootstrap.md +295 -295
  34. package/guides/agents/backend.md +104 -104
  35. package/guides/agents/content.md +94 -94
  36. package/guides/agents/cs2.md +56 -56
  37. package/guides/agents/data.md +87 -87
  38. package/guides/agents/design.md +77 -77
  39. package/guides/agents/frontend.md +92 -92
  40. package/guides/agents/infrastructure.md +81 -81
  41. package/guides/agents/setup.md +281 -281
  42. package/guides/agents/verify.md +151 -151
  43. package/guides/agents/viz.md +106 -106
  44. package/guides/backend.md +146 -146
  45. package/guides/content.md +147 -147
  46. package/guides/cs2.md +190 -190
  47. package/guides/data.md +123 -123
  48. package/guides/design.md +116 -116
  49. package/guides/engineering-behavior.md +43 -43
  50. package/guides/escalation-protocol.md +125 -125
  51. package/guides/frontend.md +151 -151
  52. package/guides/history-md-spec.md +297 -297
  53. package/guides/infrastructure.md +179 -179
  54. package/guides/lessons-learned-spec.md +145 -151
  55. package/guides/output-contract.md +108 -108
  56. package/guides/publish-md-spec.md +289 -289
  57. package/guides/rdc-skills-startup.md +30 -30
  58. package/guides/verify.md +11 -11
  59. package/hooks/check-cwd.js +31 -31
  60. package/hooks/check-rdc-environment.js +164 -164
  61. package/hooks/check-services.js +6 -6
  62. package/hooks/check-stale-work-items.js +19 -19
  63. package/hooks/foreground-process-gate.js +128 -128
  64. package/hooks/gate-watchdog-selfcheck.js +257 -257
  65. package/hooks/hook-logger.js +25 -25
  66. package/hooks/lib/run-evidence-gate.mjs +241 -241
  67. package/hooks/no-stop-open-epics.js +127 -127
  68. package/hooks/post-tool-batch-gate.js +203 -203
  69. package/hooks/post-work-check.js +21 -21
  70. package/hooks/postcompact-log.js +13 -13
  71. package/hooks/precompact-log.js +13 -13
  72. package/hooks/rate-limit-retry.js +46 -46
  73. package/hooks/rdc-invocation-marker.js +157 -157
  74. package/hooks/rdc-output-contract-gate.js +94 -94
  75. package/hooks/require-work-item-on-commit.js +294 -294
  76. package/hooks/restart-brief.js +19 -19
  77. package/hooks/run-hidden-hook.ps1 +47 -47
  78. package/hooks/task-completed-gate.js +274 -274
  79. package/hooks/work-item-exit-gate.js +944 -944
  80. package/lib/catalog.mjs +236 -236
  81. package/lib/cloud-rewrite.mjs +155 -155
  82. package/package.json +57 -57
  83. package/rules/work-items-rpc.md +520 -520
  84. package/scaffold/templates/HISTORY.md.template +39 -39
  85. package/scaffold/templates/PUBLISH.md.template +21 -21
  86. package/scaffold/templates/brochure-studio-default.html +70 -70
  87. package/scripts/acceptance.mjs +502 -502
  88. package/scripts/fixtures/guides/bad-guide.md +15 -15
  89. package/scripts/fixtures/guides-clean/good-guide.md +16 -16
  90. package/scripts/install-rdc-skills.js +1289 -1289
  91. package/scripts/install.ps1 +202 -202
  92. package/scripts/install.sh +132 -132
  93. package/scripts/lib/assertions.mjs +287 -287
  94. package/scripts/lib/manifest-schema.mjs +754 -754
  95. package/scripts/lib/runner.mjs +465 -465
  96. package/scripts/lib/sandbox.mjs +435 -435
  97. package/scripts/prepack.mjs +32 -32
  98. package/scripts/rdc-brochure.mjs +482 -482
  99. package/scripts/rdc-design-cli.mjs +134 -134
  100. package/scripts/rebuild-mcp.mjs +107 -107
  101. package/scripts/self-test.mjs +1460 -1460
  102. package/scripts/stamp-git-sha.mjs +29 -29
  103. package/scripts/test-guide-validator.mjs +196 -196
  104. package/scripts/test-rdc-hooks.mjs +145 -145
  105. package/scripts/uninstall.ps1 +77 -77
  106. package/scripts/uninstall.sh +69 -69
  107. package/scripts/update.ps1 +43 -43
  108. package/scripts/update.sh +43 -43
  109. package/scripts/validate-place-histories.js +461 -461
  110. package/scripts/validate-publish-manifests.js +502 -424
  111. package/scripts/watch-init.mjs +100 -100
  112. package/skills/brochure/SKILL.md +107 -107
  113. package/skills/build/SKILL.md +563 -563
  114. package/skills/channel-formatter/SKILL.md +538 -533
  115. package/skills/co-develop/SKILL.md +196 -196
  116. package/skills/collab/SKILL.md +239 -239
  117. package/skills/convert/SKILL.md +167 -140
  118. package/skills/deploy/SKILL.md +541 -541
  119. package/skills/design/SKILL.md +211 -211
  120. package/skills/design/reference/ownership.md +16 -16
  121. package/skills/design/reference/rampa.md +92 -92
  122. package/skills/design/reference/studio-model.md +153 -153
  123. package/skills/edit/SKILL.md +98 -98
  124. package/skills/fixit/SKILL.md +203 -165
  125. package/skills/fs-mcp/SKILL.md +148 -148
  126. package/skills/handoff/SKILL.md +236 -236
  127. package/skills/help/SKILL.md +143 -143
  128. package/skills/housekeeping/SKILL.md +160 -219
  129. package/skills/lifeai-brochure-author/SKILL.md +340 -340
  130. package/skills/onramp/SKILL.md +248 -0
  131. package/skills/overnight/SKILL.md +251 -251
  132. package/skills/plan/SKILL.md +345 -345
  133. package/skills/preplan/SKILL.md +90 -90
  134. package/skills/prototype/SKILL.md +150 -150
  135. package/skills/rdc-brochurify/SKILL.md +245 -245
  136. package/skills/rdc-extract-verifier-rules/SKILL.md +191 -191
  137. package/skills/release/SKILL.md +140 -140
  138. package/skills/report/SKILL.md +100 -100
  139. package/skills/review/SKILL.md +152 -152
  140. package/skills/rpms-filemap/SKILL.cloud.md +111 -111
  141. package/skills/rpms-filemap/SKILL.md +111 -111
  142. package/skills/self-test/SKILL.md +132 -132
  143. package/skills/status/SKILL.md +99 -99
  144. package/skills/terminal-config/SKILL.md +62 -62
  145. package/skills/tests/MATRIX.md +54 -54
  146. package/skills/tests/README.md +47 -47
  147. package/skills/tests/onramp.test.json +87 -0
  148. package/skills/tests/rdc-brochure.test.json +34 -34
  149. package/skills/tests/rdc-build.test.json +36 -36
  150. package/skills/tests/rdc-channel-formatter.test.json +45 -45
  151. package/skills/tests/rdc-co-develop.test.json +29 -29
  152. package/skills/tests/rdc-collab.test.json +29 -29
  153. package/skills/tests/rdc-convert.test.json +35 -35
  154. package/skills/tests/rdc-deploy.test.json +30 -30
  155. package/skills/tests/rdc-design.test.json +27 -27
  156. package/skills/tests/rdc-edit.test.json +29 -29
  157. package/skills/tests/rdc-fixit.test.json +36 -36
  158. package/skills/tests/rdc-fs-mcp.test.json +36 -36
  159. package/skills/tests/rdc-handoff.test.json +28 -28
  160. package/skills/tests/rdc-help.test.json +29 -29
  161. package/skills/tests/rdc-housekeeping.test.json +28 -32
  162. package/skills/tests/rdc-lifeai-brochure-author.test.json +35 -35
  163. package/skills/tests/rdc-overnight.test.json +37 -37
  164. package/skills/tests/rdc-plan.test.json +27 -27
  165. package/skills/tests/rdc-preplan.test.json +31 -31
  166. package/skills/tests/rdc-prototype.test.json +28 -28
  167. package/skills/tests/rdc-rdc-brochurify.test.json +23 -23
  168. package/skills/tests/rdc-rdc-extract-verifier-rules.test.json +34 -34
  169. package/skills/tests/rdc-release.test.json +29 -29
  170. package/skills/tests/rdc-report.test.json +28 -28
  171. package/skills/tests/rdc-review.test.json +29 -29
  172. package/skills/tests/rdc-rpms-filemap.test.json +28 -28
  173. package/skills/tests/rdc-self-test.test.json +24 -24
  174. package/skills/tests/rdc-status.test.json +29 -29
  175. package/skills/tests/rdc-terminal-config.test.json +29 -29
  176. package/skills/tests/rdc-watch.test.json +24 -24
  177. package/skills/tests/rdc-workitems.test.json +27 -27
  178. package/skills/watch/SKILL.md +97 -97
  179. package/skills/workitems/SKILL.md +151 -151
  180. package/tests/acceptance.test.mjs +59 -59
  181. package/tests/channel-formatter.contract.test.mjs +251 -251
  182. package/tests/curl-surface.test.mjs +289 -289
  183. package/tests/harness-gates.test.mjs +325 -325
  184. package/tests/help-surface.test.mjs +61 -61
  185. package/tests/install-rdc-skills.test.mjs +49 -49
  186. package/tests/manifest-contract-fields.test.mjs +78 -78
  187. package/tests/mcp.test.mjs +271 -271
  188. package/tests/rdc-brochure.test.mjs +125 -125
  189. package/tests/require-work-item-on-commit.test.mjs +162 -162
  190. package/tests/run-evidence-gate.test.mjs +82 -82
  191. package/tests/skill-test-matrix.test.mjs +66 -66
  192. package/tests/validate-skills.js +27 -27
  193. package/tests/work-item-exit-gate-l2.test.mjs +368 -368
  194. package/tests/work-item-exit-gate-l3.test.mjs +197 -197
  195. package/RELEASE.md +0 -42
  196. package/tests/housekeeping-lessons-triage.test.mjs +0 -49
  197. package/tests/lessons-pipeline-contract.test.mjs +0 -27
  198. 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
+ }
@@ -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
+ };