@lifeaitools/rdc-skills 0.24.38 → 0.24.41

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