ruvnet-brain 3.9.134-dev → 4.0.2

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 (218) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/README.md +5 -5
  3. package/bin/install.mjs +382 -36
  4. package/console/CONTRACT.md +172 -0
  5. package/console/activity.js +753 -0
  6. package/console/app.js +4189 -0
  7. package/console/architecture.html +1221 -0
  8. package/console/assets/depth-1.webp +0 -0
  9. package/console/assets/depth-2.webp +0 -0
  10. package/console/assets/depth-3.webp +0 -0
  11. package/console/assets/harness-vs-plain.svg +259 -0
  12. package/console/assets/hero.webp +0 -0
  13. package/console/assets/memory.webp +0 -0
  14. package/console/assets/metaharness.svg +247 -0
  15. package/console/index.html +777 -0
  16. package/console/install-architecture.html +162 -0
  17. package/console/install-mockup.html +543 -0
  18. package/console/style.css +2144 -0
  19. package/console/tips.css +926 -0
  20. package/console/tips.html +858 -0
  21. package/console/tips.js +128 -0
  22. package/docs/RELEASE-NOTES-4.0.md +88 -0
  23. package/kb/model-requirements.mjs +37 -6
  24. package/kb/zip-extract.mjs +53 -14
  25. package/keys/ruvnet-brain-signing.pub.pem +3 -0
  26. package/package.json +14 -22
  27. package/plugin/.claude-plugin/marketplace.json +14 -0
  28. package/plugin/.claude-plugin/plugin.json +22 -0
  29. package/plugin/.codex-plugin/plugin.json +21 -0
  30. package/plugin/.mcp.json +8 -0
  31. package/plugin/commands/brain-console.md +16 -0
  32. package/plugin/commands/configure.md +33 -0
  33. package/plugin/commands/rvbc.md +79 -0
  34. package/plugin/commands/rvcb.md +16 -0
  35. package/plugin/commands/whats-new.md +57 -0
  36. package/plugin/hooks/codex-hooks.json +160 -0
  37. package/plugin/hooks/hook-contracts.json +77 -0
  38. package/plugin/hooks/hooks.json +202 -0
  39. package/plugin/mcp/managed-cli-interface.mjs +47 -4
  40. package/plugin/mcp/server.mjs +56 -6
  41. package/plugin/scripts/anticipate.sh +534 -0
  42. package/plugin/scripts/codex-hook-adapter.mjs +96 -0
  43. package/plugin/scripts/continuation-gate.mjs +267 -0
  44. package/plugin/scripts/design-wall.sh +137 -0
  45. package/plugin/scripts/detach.mjs +182 -0
  46. package/plugin/scripts/first-session-worker.mjs +38 -0
  47. package/plugin/scripts/gate-receipt.sh +35 -0
  48. package/plugin/scripts/ground-before-write.sh +199 -0
  49. package/plugin/scripts/ground-ruvnet.sh +517 -0
  50. package/plugin/scripts/grounding-stamp.sh +113 -0
  51. package/plugin/scripts/grounding-substance.mjs +595 -0
  52. package/plugin/scripts/hijack-ruvnet.sh +81 -0
  53. package/plugin/scripts/hook-input.mjs +558 -0
  54. package/plugin/scripts/hook-shim-bash.mjs +55 -0
  55. package/plugin/scripts/hook-shim.mjs +303 -0
  56. package/plugin/scripts/host-update.mjs +58 -0
  57. package/plugin/scripts/kling-preflight.sh +146 -0
  58. package/plugin/scripts/learn-capture.sh +173 -0
  59. package/plugin/scripts/learn-flush.mjs +155 -0
  60. package/plugin/scripts/lesson-hooks.sh +213 -0
  61. package/plugin/scripts/md-stamp.mjs +219 -0
  62. package/plugin/scripts/protect-brain-state.sh +84 -0
  63. package/plugin/scripts/route-dispatch.sh +147 -0
  64. package/plugin/scripts/routing-outcome-capture.mjs +89 -0
  65. package/plugin/scripts/runtime-preferences.mjs +269 -0
  66. package/plugin/scripts/session-start-core.mjs +477 -0
  67. package/plugin/scripts/session-start.sh +13 -0
  68. package/plugin/scripts/signal-watch.mjs +193 -0
  69. package/plugin/scripts/unprompted-runtime.mjs +377 -0
  70. package/plugin/scripts/update-apply.mjs +419 -0
  71. package/plugin/scripts/verify-interface.sh +53 -0
  72. package/plugin/scripts/version-bump-gate.sh +112 -0
  73. package/plugin/skills/brain-build/SKILL.md +123 -0
  74. package/plugin/skills/brain-console/SKILL.md +22 -0
  75. package/plugin/skills/brain-prompt/SKILL.md +83 -0
  76. package/plugin/skills/brain-score/SKILL.md +101 -0
  77. package/plugin/skills/release-proof/SKILL.md +81 -0
  78. package/plugin/skills/release-proof/agents/openai.yaml +4 -0
  79. package/plugin/skills/release-proof/references/receipt-contract.md +38 -0
  80. package/plugin/skills/release-proof/scripts/release-proof.mjs +210 -0
  81. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +121 -0
  82. package/plugin/skills/ruvnet-brain/SKILL.md +234 -0
  83. package/plugin/skills/rvbc/SKILL.md +23 -0
  84. package/plugin/skills/savings/SKILL.md +46 -0
  85. package/plugin/skills/whats-new/SKILL.md +22 -0
  86. package/scripts/adr-backfill.mjs +107 -0
  87. package/scripts/advocacy-outcomes.mjs +808 -0
  88. package/scripts/agentdb-context.mjs +216 -0
  89. package/scripts/agentdb-fleet-doctor.mjs +101 -0
  90. package/scripts/ascii-drift.mjs +236 -0
  91. package/scripts/behavioral-l1-l4.mjs +210 -0
  92. package/scripts/brain-capability-check.mjs +72 -0
  93. package/scripts/brain-grade-groundtruth.mjs +100 -0
  94. package/scripts/brain-latency-50.mjs +227 -0
  95. package/scripts/brain-novice-50.mjs +189 -0
  96. package/scripts/brain-stamp.mjs +94 -0
  97. package/scripts/brain-state.mjs +212 -0
  98. package/scripts/build-bundle.mjs +522 -0
  99. package/scripts/build-concepts.mjs +132 -0
  100. package/scripts/build-l2.mjs +71 -0
  101. package/scripts/build-primer.mjs +73 -0
  102. package/scripts/build-symbols.mjs +68 -0
  103. package/scripts/calibrate-router.mjs +97 -0
  104. package/scripts/capability-audit.mjs +321 -0
  105. package/scripts/capability-registry.mjs +876 -0
  106. package/scripts/check-indexation.mjs +108 -0
  107. package/scripts/check-legibility.mjs +189 -0
  108. package/scripts/ci/build-fixture-kb.mjs +67 -0
  109. package/scripts/ci/learning-replay-codex-adapter.mjs +62 -0
  110. package/scripts/ci/learning-replay-recorder.mjs +59 -0
  111. package/scripts/ci/mutate-hook-timeout.mjs +70 -0
  112. package/scripts/ci/stranger-fixture-stage.mjs +17 -0
  113. package/scripts/ci/stranger-scenario.mjs +228 -0
  114. package/scripts/ci/stranger-timeout.mjs +25 -0
  115. package/scripts/ci-verdict.mjs +29 -0
  116. package/scripts/claims-verify.mjs +710 -0
  117. package/scripts/clear-claude-tmp.sh +31 -0
  118. package/scripts/console-engine.mjs +434 -0
  119. package/scripts/console-engine.test.mjs +125 -0
  120. package/scripts/corpus-qa.mjs +250 -0
  121. package/scripts/correction-detect-embed.mjs +346 -0
  122. package/scripts/correction-detect-measure.mjs +270 -0
  123. package/scripts/correction-detect.mjs +686 -0
  124. package/scripts/count-chunks.mjs +54 -0
  125. package/scripts/described-questions.json +30 -0
  126. package/scripts/design-grade.mjs +58 -0
  127. package/scripts/dev-plugin-link.sh +105 -0
  128. package/scripts/distill-project.mjs +200 -0
  129. package/scripts/doc-currency.mjs +801 -0
  130. package/scripts/eval-brain.mjs +244 -0
  131. package/scripts/fix-metaharness-memretrieve.mjs +121 -0
  132. package/scripts/full-hints.mjs +87 -0
  133. package/scripts/gate.sh +39 -0
  134. package/scripts/gates.mjs +146 -0
  135. package/scripts/gen-console-images.mjs +54 -0
  136. package/scripts/gen-images.mjs +47 -0
  137. package/scripts/git-clone-refresh.mjs +52 -0
  138. package/scripts/git-hooks/pre-push +126 -0
  139. package/scripts/goal-match.mjs +398 -0
  140. package/scripts/goldie-research.mjs +223 -0
  141. package/scripts/goldie-weekly.sh +67 -0
  142. package/scripts/health-repair.mjs +250 -0
  143. package/scripts/helix-scenario-questions.json +10 -0
  144. package/scripts/ingest-gists.mjs +230 -0
  145. package/scripts/ingest-meeting.mjs +115 -0
  146. package/scripts/ingest-repo.mjs +79 -0
  147. package/scripts/install-npx-witness.sh +49 -0
  148. package/scripts/issue-fix.mjs +639 -0
  149. package/scripts/issue-watch.mjs +276 -0
  150. package/scripts/issue4-close-note.md +31 -0
  151. package/scripts/key-canary.mjs +91 -0
  152. package/scripts/latency-to-surface.mjs +233 -0
  153. package/scripts/learning-enable.mjs +380 -0
  154. package/scripts/learning-replay.mjs +1570 -0
  155. package/scripts/learnings.mjs +62 -0
  156. package/scripts/lesson-gate.mjs +680 -0
  157. package/scripts/lesson-lifecycle.mjs +449 -0
  158. package/scripts/lesson-promote.mjs +262 -0
  159. package/scripts/lesson-ratify.mjs +98 -0
  160. package/scripts/lesson-seed.mjs +252 -0
  161. package/scripts/lesson-store.mjs +447 -0
  162. package/scripts/loop-checkpoint.mjs +86 -0
  163. package/scripts/memdb-health.sh +14 -0
  164. package/scripts/memory-doctor.mjs +271 -0
  165. package/scripts/model-catalog.mjs +79 -0
  166. package/scripts/nightly-controller.mjs +66 -0
  167. package/scripts/nightly-gists.sh +72 -0
  168. package/scripts/nightly-wrapper.sh +180 -0
  169. package/scripts/notify.sh +12 -0
  170. package/scripts/npx-witness.sh +56 -0
  171. package/scripts/onboarding-console.mjs +2749 -0
  172. package/scripts/private-fence.mjs +69 -0
  173. package/scripts/proactivity-metrics.mjs +118 -0
  174. package/scripts/proof-questions.json +56 -0
  175. package/scripts/prove.mjs +95 -0
  176. package/scripts/proxy/claude-proxied.sh +57 -0
  177. package/scripts/proxy/proxy-revert.sh +59 -0
  178. package/scripts/proxy/proxy-up.sh +60 -0
  179. package/scripts/proxy/proxy-verify.mjs +142 -0
  180. package/scripts/published-surface-probe.mjs +241 -0
  181. package/scripts/qe/card-lane-gate.mjs +162 -0
  182. package/scripts/qe/session-start-gate.mjs +229 -0
  183. package/scripts/qe/ux-suite.mjs +323 -0
  184. package/scripts/reconcile-project.mjs +0 -0
  185. package/scripts/record-lesson.mjs +113 -0
  186. package/scripts/refresh-model-catalog.mjs +99 -0
  187. package/scripts/release-proof.mjs +9 -0
  188. package/scripts/release-vector.mjs +281 -0
  189. package/scripts/release.mjs +395 -0
  190. package/scripts/remedy-registry.mjs +247 -0
  191. package/scripts/rerank-cap-eval.mjs +265 -0
  192. package/scripts/rerank-cap-warm-ab.mjs +129 -0
  193. package/scripts/route-cheap.mjs +20 -15
  194. package/scripts/router-utilization.mjs +182 -0
  195. package/scripts/routing-flywheel.mjs +596 -0
  196. package/scripts/rvf-generation.mjs +104 -0
  197. package/scripts/rvf-index-audit.mjs +138 -0
  198. package/scripts/self-update.mjs +508 -0
  199. package/scripts/selfcheck.mjs +7 -1
  200. package/scripts/sign-bundle.mjs +69 -0
  201. package/scripts/signal-watch.mjs +171 -0
  202. package/scripts/stack-sync.mjs +469 -0
  203. package/scripts/stamp-existing-rvf-generations.mjs +53 -0
  204. package/scripts/stamp-sweep.mjs +144 -0
  205. package/scripts/status-honesty.mjs +102 -0
  206. package/scripts/sync-version.mjs +217 -0
  207. package/scripts/token-report.mjs +102 -0
  208. package/scripts/top100-benchmark.mjs +479 -0
  209. package/scripts/top100-corpus.mjs +112 -0
  210. package/scripts/top100-semantic-assertions.mjs +449 -0
  211. package/scripts/update-apply.mjs +9 -0
  212. package/scripts/upgrade-notice.mjs +14 -0
  213. package/scripts/verify-bundle.mjs +51 -0
  214. package/scripts/verify-channels.mjs +184 -0
  215. package/scripts/verify-model-catalog.mjs +104 -0
  216. package/scripts/verify-nightly-close-issue4.sh +31 -0
  217. package/scripts/version.mjs +40 -0
  218. package/scripts/wired-check.mjs +864 -0
@@ -0,0 +1,55 @@
1
+ // hook-shim-bash.mjs — bash resolution for hook-shim.mjs (issue #38).
2
+ //
3
+ // Pulled out of hook-shim.mjs (which runs CLI dispatch with process.exit at import time,
4
+ // making it awkward to unit-test directly) so resolveBash()/skipNoBash() can be exercised
5
+ // in isolation with a mocked platform/fs/spawnSync. hook-shim.mjs imports this module; its
6
+ // own subprocess tests are unaffected.
7
+ //
8
+ // Based on the tested patch contributed by @tkmeownow in #38.
9
+
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import { spawnSync } from 'node:child_process';
13
+
14
+ /** Locate a usable bash. POSIX: /bin/bash, as ever. Windows: Git for Windows bash — env override,
15
+ * standard install locations, then PATH via where.exe. WSL's System32 bash.exe is excluded: it
16
+ * boots a Linux VM whose filesystem view does not contain this plugin's Windows paths.
17
+ * `env`/`platform`/`deps` are injectable so this can be unit-tested for win32 behavior from any
18
+ * host OS; production callers use the defaults (real process.env/platform/fs/spawnSync). */
19
+ export function resolveBash(env = process.env, platform = process.platform, deps = {}) {
20
+ const existsSync = deps.existsSync || fs.existsSync;
21
+ const spawn = deps.spawnSync || spawnSync;
22
+ if (platform !== 'win32') return '/bin/bash';
23
+ const candidates = [
24
+ env.RUVNET_BRAIN_BASH,
25
+ env.CLAUDE_CODE_GIT_BASH_PATH,
26
+ path.win32.join(env.ProgramFiles || 'C:\\Program Files', 'Git', 'bin', 'bash.exe'),
27
+ path.win32.join(env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'Git', 'bin', 'bash.exe'),
28
+ env.LOCALAPPDATA ? path.win32.join(env.LOCALAPPDATA, 'Programs', 'Git', 'bin', 'bash.exe') : null,
29
+ ].filter(Boolean);
30
+ for (const c of candidates) {
31
+ try { if (existsSync(c)) return c; } catch { /* keep looking */ }
32
+ }
33
+ try {
34
+ const r = spawn('where.exe', ['bash.exe'], { encoding: 'utf8' });
35
+ for (const line of String(r.stdout || '').split(/\r?\n/)) {
36
+ const p = line.trim();
37
+ if (p && !/\\system32\\/i.test(p) && existsSync(p)) return p;
38
+ }
39
+ } catch { /* no where.exe result */ }
40
+ return null;
41
+ }
42
+
43
+ /** No usable bash on this machine. Skip the hook — an unsupported platform must not turn every
44
+ * tool call into an error. Notice is emitted once (marker file), not per invocation. */
45
+ export function skipNoBash(brainHome) {
46
+ const marker = path.join(brainHome, '.no-bash-notice-shown');
47
+ if (!fs.existsSync(marker)) {
48
+ try {
49
+ fs.mkdirSync(brainHome, { recursive: true });
50
+ fs.writeFileSync(marker, new Date().toISOString() + '\n');
51
+ } catch { /* best effort */ }
52
+ process.stderr.write('[hook-shim] no bash found — bash-based hooks are disabled on this machine (install Git for Windows, or set RUVNET_BRAIN_BASH to a bash.exe, to enable them)\n');
53
+ }
54
+ return 0;
55
+ }
@@ -0,0 +1,303 @@
1
+ #!/usr/bin/env node
2
+ // hook-shim.mjs — the Stable Spine's hook dispatcher (ADR-023, docs/INTELLIGENT-UPDATING.md §3).
3
+ //
4
+ // THE ONE JOB: make hook BEHAVIOR update without a Claude Code restart. CC freezes
5
+ // ${CLAUDE_PLUGIN_ROOT} at boot (a version-named cache dir), so any hook command pointing there is
6
+ // trapped at the boot-time version. This shim is the only thing hooks.json points at; it resolves
7
+ // the ACTIVE generation from ~/.cache/ruvnet-brain/active.json ONCE per invocation and executes the
8
+ // hook body from that immutable versions/<v>/ tree. Update = rewrite active.json (atomic, by
9
+ // scripts/update-apply.mjs) → the very next hook fire runs the new code. No restart.
10
+ //
11
+ // SHELL CONTRACT (this file is boot-frozen — treat as near-frozen ABI, per DDD-0003):
12
+ // • Self-contained: node:fs/path/os/child_process/url builtins, plus the sibling
13
+ // hook-shim-bash.mjs (bash-interpreter resolution, issue #38) — colocated in this same
14
+ // boot-frozen scripts/ dir, never resolved from the spine. No imports from the hook BODY.
15
+ // • Typed dispatch table (red-team findings 15/16/30): each hook declares its file, interpreter,
16
+ // and mode. `blocking` hooks propagate their exact exit code (route-dispatch's deliberate
17
+ // exit-2 wall survives by CONTRACT); `advisory` hooks can never block a turn — any failure,
18
+ // including a missing file, exits 0.
19
+ // • Containment (finding 13): a codeRoot is honored ONLY if it resolves under
20
+ // ~/.cache/ruvnet-brain/versions/ — or is the explicit dev-mode checkout declared in
21
+ // ~/.cache/ruvnet-brain/dev.json (finding 24).
22
+ // • Loud fallback (finding 25): no spine / broken spine / missing body file → run the sibling in
23
+ // ${CLAUDE_PLUGIN_ROOT} AND emit one stderr line naming the frozen fallback, so a broken spine
24
+ // can never masquerade as health. First-install (spine never seeded) stays quiet.
25
+ // • Per-invocation consistency (finding 14, accepted middle): active.json is read once; the whole
26
+ // invocation runs from one immutable tree — a hook never straddles two generations.
27
+ // • Brain OFF is a CONTRACT PER HOOK, not one early-exit (ADR-054 §3): each table entry declares
28
+ // `offBehavior`, resolved ONCE per invocation against the sentinel (see BRAIN_OFF below).
29
+
30
+ import fs from 'node:fs';
31
+ import path from 'node:path';
32
+ import os from 'node:os';
33
+ import { spawnSync } from 'node:child_process';
34
+ import { fileURLToPath } from 'node:url';
35
+ import { resolveBash, skipNoBash } from './hook-shim-bash.mjs';
36
+
37
+ const BRAIN_HOME = process.env.RUVNET_BRAIN_HOME || path.join(os.homedir(), '.cache', 'ruvnet-brain');
38
+ const ACTIVE = path.join(BRAIN_HOME, 'active.json');
39
+ const DEV = path.join(BRAIN_HOME, 'dev.json');
40
+ const VERSIONS = path.join(BRAIN_HOME, 'versions');
41
+ // fileURLToPath, not URL.pathname: on Windows, pathname yields "/C:/…" which path.resolve mangles
42
+ // into "C:\C:\…" — the fallback silently pointed at a nonexistent tree (issue #38).
43
+ const PLUGIN_ROOT = process.env.CLAUDE_PLUGIN_ROOT || path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
44
+
45
+ // ── BRAIN OFF (ADR-054 §2/§4) — resolved ONCE, here, for the whole invocation. ──────────────────
46
+ //
47
+ // A bare existsSync on the sentinel, on purpose and by contract. This file is BOOT-FROZEN, so it may
48
+ // not import scripts/brain-state.mjs (that module lives in the spine, outside this frozen tree, and
49
+ // an import of it would reintroduce exactly the version-straddling this shim exists to prevent).
50
+ // The duplication is deliberate and is held by test, not by comment: tests/unit/brain-off.test.mjs
51
+ // drives the REAL shim against the REAL sentinel path, so a drift between the two copies goes red.
52
+ //
53
+ // The read happens once and is carried through the rest of the invocation (ADR-054 §4, per-operation
54
+ // snapshot) — a hook must never straddle a mid-flight flip any more than it straddles two generations.
55
+ // statSync, not existsSync: existsSync does not throw on EACCES, it returns false — so an unreadable
56
+ // state directory reported "no sentinel" and silently switched a user's brain back on. Only genuine
57
+ // absence (ENOENT/ENOTDIR) means ON; every other error means we cannot tell, and "cannot tell" must
58
+ // not override a choice the user already made. Same rule, same codes, as scripts/brain-state.mjs.
59
+ const BRAIN_STATE_DIR = process.env.RUVNET_BRAIN_STATE_DIR || path.join(os.homedir(), '.config', 'ruvnet-brain');
60
+ let BRAIN_OFF = false;
61
+ try { fs.statSync(path.join(BRAIN_STATE_DIR, 'brain-off')); BRAIN_OFF = true; }
62
+ catch (e) { BRAIN_OFF = !(e && (e.code === 'ENOENT' || e.code === 'ENOTDIR')); }
63
+
64
+ // The dispatch table — hook id → { file (relative to <codeRoot>/plugin/scripts), interpreter, mode,
65
+ // offBehavior }. Adding a hook here is a SHELL change (requiresRestart); changing a BODY never is.
66
+ //
67
+ // `offBehavior` is the per-plane OFF contract (ADR-054 §3), as DATA rather than as ifs scattered
68
+ // through nine scripts. Both duel reviewers converged on why one boolean kill-switch is wrong: it
69
+ // either lies about being off (something keeps speaking) or it over-kills (it removes protections
70
+ // that have nothing to do with retrieval). So each hook states its own answer:
71
+ //
72
+ // 'silence' — this hook exists to advertise, ground, or learn. Off means it does not run at all
73
+ // and writes ZERO bytes. Nothing downstream can tell it apart from not being installed.
74
+ // 'run' — this is a SAFETY WALL that guards money or honesty, not retrieval. route-dispatch
75
+ // stops a subagent fan-out inheriting an expensive model; design-wall stops an
76
+ // ungraded surface shipping; protect-state guards the user's own consent record.
77
+ // None becomes acceptable because retrieval is off.
78
+ // 'partial' — the hook splits INTERNALLY. session-start-core still runs the auto-updater heartbeat, the
79
+ // GONG health alarm and the SLA banner (an off machine must still receive fixes,
80
+ // otherwise the fix for an off-state bug can never arrive) while suppressing every
81
+ // advertising byte. The snapshot is forwarded as RUVNET_BRAIN_OFF so the body reads
82
+ // ONE resolved answer instead of racing the filesystem again mid-run.
83
+ const TABLE = {
84
+ 'session-start': { file: 'session-start-core.mjs', interpreter: 'node', mode: 'advisory', offBehavior: 'partial' },
85
+ 'ground-ruvnet': { file: 'ground-ruvnet.sh', interpreter: 'bash', mode: 'advisory', offBehavior: 'silence' },
86
+ 'hijack-ruvnet': { file: 'hijack-ruvnet.sh', interpreter: 'bash', mode: 'advisory', offBehavior: 'silence' },
87
+ 'route-dispatch': { file: 'route-dispatch.sh', interpreter: 'bash', mode: 'blocking', offBehavior: 'run' },
88
+ 'ground-before-write': { file: 'ground-before-write.sh', interpreter: 'bash', mode: 'blocking', offBehavior: 'run' },
89
+ 'grounding-stamp': { file: 'grounding-stamp.sh', interpreter: 'bash', mode: 'advisory', offBehavior: 'silence' },
90
+ 'verify-interface': { file: 'verify-interface.sh', interpreter: 'bash', mode: 'advisory', offBehavior: 'silence' },
91
+ 'design-wall': { file: 'design-wall.sh', interpreter: 'bash', mode: 'blocking', offBehavior: 'run' },
92
+ // The consent guard (ADR-054 §3): it protects the OFF state itself, so it is the one hook that
93
+ // matters MORE while the brain is off. 'run', permanently.
94
+ 'protect-state': { file: 'protect-brain-state.sh', interpreter: 'bash', mode: 'blocking', offBehavior: 'run' },
95
+ 'learn-capture': { file: 'learn-capture.sh', interpreter: 'bash', mode: 'advisory', offBehavior: 'silence' },
96
+ 'learn-flush': { file: 'learn-flush.mjs', interpreter: 'node', mode: 'advisory', offBehavior: 'silence' },
97
+ 'md-stamp': { file: 'md-stamp.mjs', interpreter: 'node', mode: 'advisory', offBehavior: 'silence' },
98
+ // THE EXTERNAL-SIGNAL WATCH PLANE, W1 OBSERVED (ADR-058 §D3; DDD-0013 Context 2). PostToolUse,
99
+ // matcher ^Bash$ (anchored — an unanchored matcher is F3/F4). Classifies gh/vercel/netlify/npm
100
+ // publish/git push in EXECUTABLE POSITION (hook-input.mjs's findInvocations(), never a grep) and,
101
+ // on a successful `git push`, opens a pending CI-verdict debt that scripts/signal-watch.mjs (a
102
+ // SEPARATE process, SEPARATE cadence) resolves later via `gh run list`. offBehavior 'silence' per
103
+ // the ADR's explicit instruction: this plane observes and advises, it never guards money or
104
+ // honesty on its own — the git-push debt it opens is only ever SURFACED (never gated) by
105
+ // session-start-core.mjs, and that surfacing already lives under SessionStart's 'partial' contract.
106
+ 'signal-watch': { file: 'signal-watch.mjs', interpreter: 'node', mode: 'advisory', offBehavior: 'silence' },
107
+ 'routing-outcome': { file: 'routing-outcome-capture.mjs', interpreter: 'node', mode: 'advisory', offBehavior: 'run' },
108
+ // The unprompted-speech chokepoint (ADR-040 / DDD-0004). ONE runtime is the sole writer of
109
+ // user-facing bytes for every unprompted hook: it spawns the real producers (anticipate, lesson)
110
+ // in candidate mode, applies the per-channel policy, and writes the final envelope itself. `channel`
111
+ // marks it as the unprompted seam. It is mode:'blocking' on purpose — the runtime decides its own
112
+ // exit code, and an opted-in lesson BLOCK must propagate as exit 2 (an advisory delivery is exit 0
113
+ // and passes straight through; a spawn error is exit 1, which CC treats as a non-blocking notice).
114
+ // The CC event name is forwarded to the runtime as an extra argv (see runHook's arg plumbing).
115
+ 'unprompted-speech': { file: 'unprompted-runtime.mjs', interpreter: 'node', mode: 'blocking', channel: 'unprompted', offBehavior: 'silence' },
116
+ // THE LIFECYCLE PLANE (ADR-055 §2, build item 1). Stop was the ONE registered hook that bypassed
117
+ // this table entirely — hooks.json pointed straight at continuation-gate.mjs, so it had no mode,
118
+ // no offBehavior, and no spine resolution, contradicting this file's own `_note` and leaving
119
+ // ADR-054's off-contract undefined for the only event that can force a turn to continue
120
+ // (ADR-055 F5/F14). Routing it here costs nothing behaviourally — `mode:'advisory'` forces exit 0
121
+ // exactly as the `|| true` it replaces did — and buys the two things F5 was about: a hot body via
122
+ // the spine, and a declared answer to "what happens when the brain is off".
123
+ //
124
+ // offBehavior 'run', DECIDED (ADR-055 left this open by name: "Stop — undecided today, F5).
125
+ // ADR-054's rule is the discriminator, applied literally: 'silence' is for hooks whose job is to
126
+ // ADVERTISE, GROUND, or LEARN; 'run' is for walls that guard money or honesty rather than
127
+ // retrieval. The continuation gate does none of the first three — it reads the user's own work
128
+ // ledger, needs no corpus, no network and no brain — and it does the last: ADR-043 exists because
129
+ // a turn ending clean on an open commitment is a dishonest turn. Switching retrieval off is not
130
+ // consent to abandon work mid-goal, and a gate that disarmed itself there would be the ADR-054
131
+ // over-kill failure exactly.
132
+ // FORWARD NOTE, deliberately not pre-declared: ADR-055 §3.4 gives this gate a SECOND input —
133
+ // grounding debt — which IS brain-dependent. When build item 6 lands, this entry becomes
134
+ // 'partial' (work-ledger retained, grounding-debt bytes suppressed). Declaring 'partial' today
135
+ // would declare a split that does not exist, which is the ceremony ADR-055 §4 refuses by name.
136
+ 'continuation-gate': { file: 'continuation-gate.mjs', interpreter: 'node', mode: 'advisory', offBehavior: 'run' },
137
+ };
138
+
139
+ const hookId = process.argv[2];
140
+ const entry = TABLE[hookId];
141
+ if (!entry) {
142
+ process.stderr.write(`[hook-shim] unknown hook id: ${JSON.stringify(hookId)} — known: ${Object.keys(TABLE).join(', ')}\n`);
143
+ process.exit(0); // an unknown id is a shell bug, but it must never block the user's turn
144
+ }
145
+
146
+ // The 'silence' contract, enforced before ANY work: no spine resolution, no spawn, no stderr — not
147
+ // even the loud-fallback line, because a broken spine is not news to a user who switched the brain
148
+ // off. Exit 0 unconditionally: a silenced hook is the absence of a hook, and the absence of a hook
149
+ // never blocks a turn. An unknown/absent offBehavior defaults to running, so a future entry that
150
+ // forgets to declare one fails toward the pre-ADR-054 behaviour rather than toward silent death.
151
+ if (BRAIN_OFF && entry.offBehavior === 'silence') process.exit(0);
152
+
153
+ // The ground hook is registered on every prompt. On Windows, starting Git Bash and then jq can
154
+ // consume most of the hook's five-second declaration before an unrelated prompt reaches the
155
+ // shell body's "emit nothing" verdict. Read a bounded copy here, in the Node process that is
156
+ // already running, and skip the interpreter entirely only when BOTH prompt intent and project
157
+ // state prove that no advisory can fire. The regex deliberately over-approximates the shell gates:
158
+ // false positives take the established body; false negatives would be a product defect.
159
+ let groundInput = null;
160
+ const GROUND_RELEVANT = /ruvnet|ruflo|ruvector|\brvf\b|agentdb|agenticow|rulake|ruview|rupixel|ruv-fann|agentic-flow|synthlang|dspy|qudag|safla|metaharness|cve-bench|sparc|swarm|claude-flow|pinecone|pgvector|chroma|weaviate|faiss|milvus|qdrant|hnswlib|annoy|vector|langchain|llama|autogen|crew-ai|semantic-kernel|embedding|retrieval|prompt compression|token cost|post-quantum|quantum-resistant|\badr\b|decision|architect|design|plan|spec|refactor|migrat|implement|build|write|add|change|fix|update|deploy|create|enhance|set up|setup|wire|integrate|test|coverage|audit|review|benchmark|lint|scan|debug|optimi|app|feature|service|system|backend|frontend|\bapi\b|module|pipeline|infra|database|schema|workflow|roadmap|milestone|autonomous|unattended|do not stop|keep working|keep going|soak run|harness|quality|readiness|evolve|self-improv|hardening|cheaper|cheap|lower cost|compute arbitrage|cascade|scorecard|score .*repo/i;
161
+
162
+ function projectCanSpeakWithoutPrompt() {
163
+ if (process.env.RUVNET_AUTONOMOUS === '1') return true;
164
+ try {
165
+ if (fs.existsSync(path.join(process.cwd(), '.claude-flow')) ||
166
+ fs.existsSync(path.join(process.cwd(), '.swarm'))) return true;
167
+ for (const name of ['package.json', '.mcp.json']) {
168
+ try {
169
+ if (/claude-flow|ruflo/i.test(fs.readFileSync(path.join(process.cwd(), name), 'utf8'))) return true;
170
+ } catch { /* absent/unreadable project metadata cannot create an advisory */ }
171
+ }
172
+ } catch { /* fail toward running the body below */ return true; }
173
+ return false;
174
+ }
175
+
176
+ function readGroundInput() {
177
+ return new Promise((resolve) => {
178
+ const chunks = [];
179
+ let bytes = 0;
180
+ let settled = false;
181
+ let idle;
182
+ const finish = () => {
183
+ if (settled) return;
184
+ settled = true;
185
+ clearTimeout(idle);
186
+ process.stdin.pause();
187
+ resolve(Buffer.concat(chunks));
188
+ };
189
+ const armIdle = () => {
190
+ clearTimeout(idle);
191
+ // Claude closes stdin immediately. The short idle boundary exists for malformed hosts and
192
+ // selfcheck's intentionally-held pipe, so neither can freeze this already-running shim.
193
+ idle = setTimeout(finish, 100);
194
+ idle.unref?.();
195
+ };
196
+ process.stdin.on('data', (chunk) => {
197
+ if (bytes < 32768) {
198
+ const kept = chunk.subarray(0, 32768 - bytes);
199
+ chunks.push(kept);
200
+ bytes += kept.length;
201
+ }
202
+ armIdle();
203
+ });
204
+ process.stdin.once('end', finish);
205
+ process.stdin.once('error', finish);
206
+ armIdle();
207
+ process.stdin.resume();
208
+ });
209
+ }
210
+
211
+ /** Resolve the active code root. Returns { root, source } or null (→ fallback). */
212
+ function resolveCodeRoot() {
213
+ // Dev mode wins when explicitly declared and the target still looks like the checkout it names.
214
+ try {
215
+ const dev = JSON.parse(fs.readFileSync(DEV, 'utf8'));
216
+ if (dev && dev.codeRoot && fs.existsSync(path.join(dev.codeRoot, 'scripts'))) {
217
+ return { root: dev.codeRoot, source: 'dev' };
218
+ }
219
+ } catch { /* no dev mode */ }
220
+ try {
221
+ const active = JSON.parse(fs.readFileSync(ACTIVE, 'utf8'));
222
+ if (!active || typeof active.codeRoot !== 'string') return null;
223
+ const root = path.isAbsolute(active.codeRoot) ? active.codeRoot : path.join(BRAIN_HOME, active.codeRoot);
224
+ const real = fs.realpathSync(root);
225
+ // Containment: only ever execute from the immutable version store.
226
+ if (!real.startsWith(fs.realpathSync(VERSIONS) + path.sep)) return null;
227
+ return { root: real, source: `gen ${active.generation ?? '?'}` };
228
+ } catch { return null; }
229
+ }
230
+
231
+ // Run one hook body. No shell is ever involved: spawnSync with an argument array, interpreter
232
+ // chosen from the typed table — never from input.
233
+ function runHook(file) {
234
+ if (!fs.existsSync(file)) return 0; // nothing to run — never invent a failure
235
+ let cmd;
236
+ if (entry.interpreter === 'node') {
237
+ cmd = process.execPath;
238
+ } else {
239
+ cmd = resolveBash();
240
+ // No bash on this machine (issue #38, typically win32 without Git for Windows): skip the hook
241
+ // rather than spawnSync ENOENT on every tool call. Both blocking and advisory modes return 0 —
242
+ // an unsupported platform must not degrade the session.
243
+ if (!cmd) return skipNoBash(BRAIN_HOME);
244
+ }
245
+ // Forward any argv AFTER the hook id to the hook body. Every current hooks.json line calls
246
+ // `hook-shim.mjs <id>` with no trailing args, so slice(3) is empty and their behavior is unchanged.
247
+ // The unprompted-speech chokepoint uses this to receive the CC event name:
248
+ // `hook-shim.mjs unprompted-speech UserPromptSubmit` → `unprompted-runtime.mjs UserPromptSubmit`.
249
+ const extraArgs = process.argv.slice(3);
250
+ // Forward the ONE resolved OFF snapshot to a 'partial' body (ADR-054 §4). The native SessionStart
251
+ // core also reads the sentinel because the POSIX compatibility launcher and bare installs invoke
252
+ // it outside this shim. Passing the snapshot means the two readings cannot disagree within one
253
+ // invocation if the user flips the switch while the hook is mid-run.
254
+ const env = (BRAIN_OFF && entry.offBehavior === 'partial')
255
+ ? { ...process.env, RUVNET_BRAIN_OFF: '1' }
256
+ : process.env;
257
+ const io = hookId === 'ground-ruvnet' && groundInput !== null
258
+ ? { stdio: ['pipe', 'inherit', 'inherit'], input: groundInput }
259
+ : { stdio: 'inherit' };
260
+ const r = spawnSync(cmd, [file, ...extraArgs], { ...io, env });
261
+ if (r.error) {
262
+ process.stderr.write(`[hook-shim] ${entry.file}: ${r.error.message}\n`);
263
+ return entry.mode === 'blocking' ? 1 : 0;
264
+ }
265
+ // Blocking hooks: the exit code IS the contract (route-dispatch's exit-2 wall). Advisory: always 0.
266
+ return entry.mode === 'blocking' ? (r.status ?? 0) : 0;
267
+ }
268
+
269
+ function dispatchHook() {
270
+ const fallbackFile = path.join(PLUGIN_ROOT, 'scripts', entry.file);
271
+ const spine = resolveCodeRoot();
272
+ if (spine) {
273
+ // codeRoot IS a plugin-payload root (versions/<v>/ mirrors the plugin dir: scripts/, hooks/, mcp/).
274
+ const spineFile = path.join(spine.root, 'scripts', entry.file);
275
+ if (fs.existsSync(spineFile)) {
276
+ return runHook(spineFile);
277
+ }
278
+ // Spine resolved but the body file is missing — fall back LOUDLY (finding 25).
279
+ process.stderr.write(`[hook-shim] spine (${spine.source}) missing ${entry.file} — falling back to frozen plugin\n`);
280
+ return runHook(fallbackFile);
281
+ }
282
+ // No spine at all. First-install is the normal quiet case; a previously-seeded-but-broken spine
283
+ // still lands here — the seed marker distinguishes them so breakage is loud, first-run silent.
284
+ if (fs.existsSync(path.join(BRAIN_HOME, '.spine-seeded'))) {
285
+ process.stderr.write(`[hook-shim] spine unreadable — running frozen plugin fallback (run: node scripts/update-apply.mjs --doctor)\n`);
286
+ }
287
+ return runHook(fallbackFile);
288
+ }
289
+
290
+ if (hookId === 'ground-ruvnet') {
291
+ readGroundInput().then((input) => {
292
+ groundInput = input;
293
+ let text = input.toString('utf8');
294
+ try {
295
+ const parsed = JSON.parse(text);
296
+ text = parsed?.prompt ?? parsed?.user_prompt ?? parsed?.input ?? text;
297
+ } catch { /* raw/malformed input is classified as-is */ }
298
+ if (!GROUND_RELEVANT.test(String(text)) && !projectCanSpeakWithoutPrompt()) process.exit(0);
299
+ process.exit(dispatchHook());
300
+ }).catch(() => process.exit(dispatchHook()));
301
+ } else {
302
+ process.exit(dispatchHook());
303
+ }
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ // Host-neutral automatic updater. The published installer is the single coordinator for Claude
3
+ // Code and Codex, so lifecycle updates cannot drift into host-specific shell pipelines again.
4
+ import { spawnSync } from 'node:child_process';
5
+
6
+ const CHILD_ENV_KEYS = new Set([
7
+ 'PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL',
8
+ 'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH', 'APPDATA', 'LOCALAPPDATA',
9
+ 'TEMP', 'TMP', 'TMPDIR', 'SystemRoot', 'ComSpec', 'PATHEXT',
10
+ 'LANG', 'LC_ALL', 'LC_CTYPE', 'TERM', 'NO_COLOR', 'FORCE_COLOR',
11
+ 'CODEX_HOME', 'CLAUDE_CONFIG_DIR',
12
+ 'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy',
13
+ 'RUVNET_BRAIN_HOME', 'RUVNET_BRAIN_KB', 'RUVNET_BRAIN_MODEL_CACHE',
14
+ 'RUVNET_BRAIN_NO_UPDATE_FALLBACK', 'RUVNET_BRAIN_TEST',
15
+ ]);
16
+
17
+ export function childEnvironment(source = process.env) {
18
+ return Object.fromEntries(
19
+ Object.entries(source).filter(([key]) => CHILD_ENV_KEYS.has(key)),
20
+ );
21
+ }
22
+
23
+ if (process.argv.includes('--check')) {
24
+ try {
25
+ const response = await fetch('https://registry.npmjs.org/ruvnet-brain/latest', {
26
+ signal: AbortSignal.timeout(3_000),
27
+ });
28
+ if (!response.ok) process.exit(1);
29
+ const metadata = await response.json();
30
+ if (typeof metadata.version !== 'string' || !metadata.version) process.exit(1);
31
+ process.stdout.write(`${metadata.version}\n`);
32
+ process.exit(0);
33
+ } catch {
34
+ process.exit(1);
35
+ }
36
+ }
37
+
38
+ const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx';
39
+ const result = spawnSync(npx, [
40
+ '--yes',
41
+ 'ruvnet-brain@latest',
42
+ '--update',
43
+ '--host-sync-only',
44
+ '--no-nightly-prompt',
45
+ ], {
46
+ // The downloaded package must not inherit unrelated API keys, cloud credentials, or tokens from
47
+ // the interactive host. npm's registry integrity protects the package bytes; this boundary
48
+ // limits what those bytes can observe when they execute.
49
+ env: childEnvironment(),
50
+ stdio: 'inherit',
51
+ timeout: Number(process.env.RUVNET_HOST_UPDATE_TIMEOUT_MS || 9 * 60_000),
52
+ });
53
+
54
+ if (result.error) {
55
+ process.stderr.write(`[ruvnet-brain] host update failed: ${result.error.message}\n`);
56
+ process.exit(1);
57
+ }
58
+ process.exit(result.status ?? 1);
@@ -0,0 +1,146 @@
1
+ #!/bin/bash
2
+ # kling-preflight.sh — PreToolUse gate on Bash. NO PAID KLING GENERATION WITHOUT THE TECHNIQUE.
3
+ #
4
+ # ─────────────────────────────────────────────────────────────────────────────────────────────
5
+ # WHY (2026-07-13). Stuart asked me to become a Kling black belt BEFORE building his explainer.
6
+ # I researched the docs, wrote a complete operator guide… then generated six clips with my ORIGINAL
7
+ # prompts: no soundscape (Kling returned near-silent room-tone, -31dB), no film-language camera.
8
+ # He paid ~$4.20 for footage that ignored everything I had just learned.
9
+ # "You spent the time and the credits to learn how to do it, and then you never bothered to
10
+ # follow your own instructions?"
11
+ # LATENT KNOWLEDGE IS NOT KNOWLEDGE. A guide is advisory; a gate is not.
12
+ #
13
+ # WHY IT JUDGES INSTEAD OF PATTERN-MATCHING (Stuart, same day):
14
+ # "Regex is stupidly brittle and rarely works the right way. You need a qualitative pass on it."
15
+ # He was right, and v1 proved it within minutes: my audio word-list contained `rain`, which matched
16
+ # inside "b-RAIN orb" — the gate declared a prompt with zero sound design to be a soundscape. A word
17
+ # list cannot answer "did you DIRECT this shot?". So the verdict comes from a cheap model reading the
18
+ # prompt (haiku, ~$0.001, on the subscription), and the hook merely ENFORCES that verdict.
19
+ #
20
+ # ARCHITECTURE (this is the reusable shape for any semantic gate):
21
+ # • the WALL is deterministic — exit 2 blocks, exit 0 allows, always fails OPEN
22
+ # • the JUDGMENT is qualitative — a model answers two structured questions, no keyword guessing
23
+ # • keywords appear ONCE, as a cheap fast-path ALLOW (skip the model call when direction is
24
+ # unmistakable). They may never cause a BLOCK — that's what made v1 wrong.
25
+ # • judge unreachable / slow / weird output ⇒ ALLOW. A gate that strands you offline gets
26
+ # switched off, and then it protects nothing.
27
+ #
28
+ # CONTRACT: exit 0 = allow · exit 2 + stderr = BLOCK (stderr returns to the model as the reason).
29
+ # Opt-in via the router profile, like its siblings.
30
+ # ─────────────────────────────────────────────────────────────────────────────────────────────
31
+
32
+ set -uo pipefail
33
+
34
+ INPUT=""
35
+ # BOUNDED READ (2026-07-27, ADR-055 F20): an unqualified `read` never returns on a stdin that is
36
+ # opened and never closed — measured across the mesh, 18 of 37 registered commands sat until the
37
+ # harness killed them. Real Claude Code writes and closes, so this costs no normal turn; that is
38
+ # exactly why a hook that CAN hang forever survives unnoticed. -t bounds the wait, and the string
39
+ # is truncated AFTER the loop because a hook payload is one line with no newline, so `read` hands
40
+ # the whole thing back at once and a per-iteration cap never fires.
41
+ while IFS= read -r -t 2 _l; do
42
+ INPUT+="$_l"
43
+ [ ${#INPUT} -ge 65536 ] && break
44
+ done
45
+ [ -n "$_l" ] && INPUT+="$_l"
46
+ INPUT="${INPUT:0:65536}"
47
+ [ -n "$INPUT" ] || exit 0
48
+
49
+ PROFILE="${MODEL_ROUTER_PROFILE:-$HOME/.claude/model-router/profile.json}"
50
+ [ -f "$PROFILE" ] || exit 0
51
+ [ "${RUVNET_SKIP_KLING_PREFLIGHT:-0}" = "1" ] && exit 0
52
+
53
+ name_re='"tool_name"[[:space:]]*:[[:space:]]*"([^"]*)"'
54
+ [[ $INPUT =~ $name_re ]] || exit 0
55
+ [ "${BASH_REMATCH[1]}" = "Bash" ] || exit 0
56
+
57
+ # The command CONTAINS quotes (the prompt is quoted), which arrive JSON-escaped as \". A naive
58
+ # [^"]* stops at the first one and never sees the prompt — v1's other bug: it blocked everything,
59
+ # including perfect prompts, because it only ever read the flags.
60
+ cmd_re='"command"[[:space:]]*:[[:space:]]*"((\\.|[^"\\])*)"'
61
+ [[ $INPUT =~ $cmd_re ]] || exit 0
62
+ CMD="${BASH_REMATCH[1]}"
63
+ [ -n "$CMD" ] || exit 0
64
+
65
+ # Only the two BILLABLE VIDEO verbs. Stills, who_am_i, query_tasks, account, uploads, --help: free.
66
+ [[ $CMD =~ (image_to_video|text_to_video) ]] || exit 0
67
+ [[ $CMD =~ (--help|-h)([[:space:]]|$) ]] && exit 0
68
+
69
+ # A silent-by-design model (v2_5/v2_1/2.x master) has NO audio track and costs half — choosing one is
70
+ # legitimate, so only the camera question applies there.
71
+ SILENT_MODEL=0
72
+ [[ $CMD =~ (v2_5|v2-5|v2_1|v2-1|2_0.master|2_1.master) ]] && SILENT_MODEL=1
73
+
74
+ # Extract the prompt = the last double-quoted run in the command (JSON-escaped \" in our input).
75
+ PROMPT="$CMD"
76
+ p_re='\\"([^\\]*)\\"[^\\"]*$'
77
+ [[ $CMD =~ $p_re ]] && PROMPT="${BASH_REMATCH[1]}"
78
+ [ ${#PROMPT} -ge 12 ] || exit 0 # nothing meaningful to judge → don't get in the way
79
+
80
+ # ── FAST-PATH ALLOW ONLY (never a block) ──────────────────────────────────────────────────────
81
+ # If the direction is unmistakable, skip the model call. These words can only ALLOW, so a false
82
+ # match here costs a judgment call, not a wrongly-approved spend. (The v1 sin was letting a word
83
+ # list decide the negative — "b-RAIN" reading as rain-sounds.)
84
+ shopt -s nocasematch 2>/dev/null || true
85
+ CAM_FAST='(dolly[- ]?in|dolly[- ]?out|tracking shot|crane up|crane down|rack focus|whip[- ]?pan|crash zoom|steadicam|locked[- ]?off|push[- ]?in|pull[- ]?back)'
86
+ AUD_FAST='(soundscape|ambient (sound|audio|hum)|sound design|score swells|synth swell|foley)'
87
+ if [[ $PROMPT =~ $CAM_FAST ]]; then
88
+ if [ "$SILENT_MODEL" = "1" ] || [[ $PROMPT =~ $AUD_FAST ]]; then exit 0; fi
89
+ fi
90
+
91
+ # ── THE QUALITATIVE PASS ──────────────────────────────────────────────────────────────────────
92
+ CLAUDE_BIN="${CLAUDE_BIN:-$HOME/.npm-global/bin/claude}"
93
+ [ -x "$CLAUDE_BIN" ] || exit 0 # no judge available → fail OPEN
94
+
95
+ ASK="You are a film-production gate for a PAID AI video generation. Judge ONLY this prompt:
96
+
97
+ <prompt>$PROMPT</prompt>
98
+
99
+ Answer two questions about the PROMPT ITSELF (not about what the video might look like):
100
+ 1. CAMERA — does it direct the camera with real cinematographic intent (a move, framing, or a
101
+ deliberate static hold: dolly, tracking, pan, tilt, crane, orbit, zoom, push-in, rack focus,
102
+ wide/close framing, locked-off)? Vague phrases like 'gentle motion', 'the scene moves', or no
103
+ camera language at all = NO.
104
+ 2. AUDIO — does it describe a SOUNDSCAPE the model should generate (ambience, SFX, music, or
105
+ dialogue)? Merely naming objects that could make noise does NOT count; it must direct sound.
106
+ $([ "$SILENT_MODEL" = "1" ] && echo 'This model is silent-by-design; answer AUDIO=YES automatically.')
107
+
108
+ Reply with EXACTLY two lines, nothing else:
109
+ CAMERA=YES or CAMERA=NO
110
+ AUDIO=YES or AUDIO=NO"
111
+
112
+ VERDICT=$("$CLAUDE_BIN" -p --model haiku "$ASK" 2>/dev/null) || exit 0 # judge failed → fail OPEN
113
+ [ -n "$VERDICT" ] || exit 0
114
+
115
+ MISSING=""
116
+ [[ $VERDICT =~ CAMERA=NO ]] && MISSING="CAMERA"
117
+ if [ "$SILENT_MODEL" = "0" ] && [[ $VERDICT =~ AUDIO=NO ]]; then MISSING="${MISSING:+$MISSING + }AUDIO"; fi
118
+ # Unparseable verdict (neither YES nor NO present) → fail OPEN rather than block on confusion.
119
+ [[ $VERDICT =~ (CAMERA=(YES|NO)) ]] || exit 0
120
+ [ -n "$MISSING" ] || exit 0
121
+
122
+ read -r -d '' MSG <<EOF || true
123
+ ⛔ BLOCKED — this paid Kling generation is missing: ${MISSING}
124
+ (judged by reading your prompt, not by keyword matching)
125
+
126
+ You are about to spend real money (3.0-Turbo 1080p = \$0.14/sec) on a prompt that ignores the
127
+ technique. This EXACT miss already happened on 2026-07-13: six clips with no soundscape came back
128
+ as near-silent room-tone (-31dB). "You spent the time and the credits to learn how to do it, and
129
+ then you never bothered to follow your own instructions?"
130
+
131
+ YOUR PROMPT: "${PROMPT:0:160}"
132
+
133
+ FIX IT BEFORE RESUBMITTING:
134
+ • CAMERA — direct the shot in film language: "slow dolly-in", "tracking shot", "crane up",
135
+ "rack focus to…", "static locked-off hold". Not "gentle motion".
136
+ • AUDIO — 3.0/Turbo returns a real audio track ONLY if you describe the soundscape:
137
+ "a low synth swell, distant keyboard clicks, a warm chime as it connects."
138
+ (Deliberately silent? Use kling-video-v2_5 — half price, no audio — and this clears.)
139
+
140
+ Full technique: ~/.claude/docs/KLING-OPERATOR-GUIDE.md (§0 pre-flight · §4 formula · §5 audio)
141
+ LATENT KNOWLEDGE IS NOT KNOWLEDGE. Research is not a deliverable — an applied result is.
142
+ (Deliberate override, say why out loud: RUVNET_SKIP_KLING_PREFLIGHT=1)
143
+ EOF
144
+ bash "$(dirname "${BASH_SOURCE[0]}")/gate-receipt.sh" kling-preflight "paid generation" "no applied pre-flight — latent knowledge is not knowledge" 2>/dev/null || true
145
+ printf '%s\n' "$MSG" >&2
146
+ exit 2