ruvnet-brain 4.5.9 → 4.5.11

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 (43) hide show
  1. package/README.md +1 -1
  2. package/bin/install.mjs +48 -93
  3. package/bin/nightly-refresh.mjs +20 -34
  4. package/kb/download-retry.mjs +97 -0
  5. package/kb/forge-update.mjs +12 -10
  6. package/kb/verify-citation.mjs +10 -13
  7. package/package.json +3 -1
  8. package/plugin/.claude-plugin/plugin.json +1 -1
  9. package/plugin/.codex-plugin/plugin.json +1 -1
  10. package/plugin/scripts/automatic-update.mjs +125 -0
  11. package/plugin/scripts/completion-claim-evidence.mjs +9 -4
  12. package/plugin/scripts/continuation-gate.mjs +34 -10
  13. package/plugin/scripts/continuation-objective.mjs +45 -10
  14. package/plugin/scripts/continuity-journal.mjs +11 -11
  15. package/plugin/scripts/ground-ruvnet.sh +3 -0
  16. package/plugin/scripts/grounding-code-projection.mjs +6 -1
  17. package/plugin/scripts/grounding-turn-evidence.mjs +2 -2
  18. package/plugin/scripts/grounding-turn-gate.mjs +8 -14
  19. package/plugin/scripts/grounding-turn-mark.mjs +106 -20
  20. package/plugin/scripts/hijack-ruvnet.sh +4 -4
  21. package/plugin/scripts/hook-input.mjs +1 -1
  22. package/plugin/scripts/host-update.mjs +16 -15
  23. package/plugin/scripts/lesson-presentation.mjs +2 -1
  24. package/plugin/scripts/nightly-scheduler.mjs +34 -7
  25. package/plugin/scripts/project-progression-hook.mjs +22 -10
  26. package/plugin/scripts/project-progression-producer.mjs +11 -1
  27. package/plugin/scripts/project-progression-store.mjs +3 -0
  28. package/plugin/scripts/project-transition-hook.mjs +7 -1
  29. package/plugin/scripts/ruvnet-gate1-pattern.mjs +33 -0
  30. package/plugin/scripts/session-snapshot-hook.mjs +4 -3
  31. package/plugin/scripts/session-start-core.mjs +3 -1
  32. package/plugin/scripts/turn-capture-privacy.mjs +202 -0
  33. package/plugin/scripts/turn-outcome-capture.mjs +28 -13
  34. package/plugin/scripts/user-settings.mjs +23 -1
  35. package/scripts/brain-grade-groundtruth.mjs +1 -0
  36. package/scripts/brain-score.mjs +11 -11
  37. package/scripts/claude-controlled-terminal.mjs +265 -0
  38. package/scripts/issue-watch.mjs +21 -10
  39. package/scripts/model-terminal-launchers.mjs +19 -9
  40. package/scripts/model-weekly-cycle.mjs +45 -9
  41. package/scripts/record-lesson.mjs +13 -7
  42. package/scripts/release-qualification-contract.mjs +35 -4
  43. package/scripts/user-model-hook-install.mjs +145 -0
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+ // DISTINCT-FROM: scripts/model-router-setup.mjs — native per-user registration only; never deploys policy or runtime.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import os from 'node:os';
6
+ import crypto from 'node:crypto';
7
+ import { pathToFileURL } from 'node:url';
8
+
9
+ export function shellQuote(value) { return `'${value.replaceAll("'", "'\\''")}'`; }
10
+
11
+ // Parse only standalone literal argv. Shell expansion, operators and redirection are never ownership.
12
+ function literalArgv(command) {
13
+ if (typeof command !== 'string') return null;
14
+ const args = []; let word = ''; let quote = ''; let active = false;
15
+ for (let i = 0; i < command.length; i++) {
16
+ const c = command[i];
17
+ if (quote === "'") { if (c === "'") quote = ''; else word += c; continue; }
18
+ if (quote === '"') {
19
+ if (c === '"') { quote = ''; continue; }
20
+ if ('$`\\'.includes(c)) return null;
21
+ word += c; continue;
22
+ }
23
+ if (c === "'" || c === '"') { quote = c; active = true; continue; }
24
+ if (c === '\\') { if (++i === command.length) return null; word += command[i]; active = true; continue; }
25
+ if (/\s/.test(c)) { if (active) { args.push(word); word = ''; active = false; } continue; }
26
+ if (';&|<>$`(){}*?!~'.includes(c)) return null;
27
+ word += c; active = true;
28
+ }
29
+ if (quote) return null;
30
+ if (active) args.push(word);
31
+ return args;
32
+ }
33
+
34
+ function realNode(command, nodeExecutable, searchPath) {
35
+ const candidates = path.isAbsolute(command) ? [command] : command === 'node'
36
+ ? searchPath.split(path.delimiter).filter(Boolean).map(dir => path.join(dir, command)) : [];
37
+ const trusted = fs.realpathSync(nodeExecutable);
38
+ for (const candidate of candidates) {
39
+ try { fs.accessSync(candidate, fs.constants.X_OK); return fs.realpathSync(candidate) === trusted; } catch { /* next PATH entry */ }
40
+ }
41
+ return false;
42
+ }
43
+
44
+ function assertSafePath(file, home) {
45
+ const relative = path.relative(home, file);
46
+ if (relative.startsWith('..') || path.isAbsolute(relative)) throw new Error('Path escapes supplied home');
47
+ let cursor = home;
48
+ for (const part of ['', ...relative.split(path.sep)]) {
49
+ if (part) cursor = path.join(cursor, part);
50
+ try {
51
+ const stat = fs.lstatSync(cursor);
52
+ if (stat.isSymbolicLink()) throw new Error(`Symlink refused: ${cursor}`);
53
+ if (cursor !== file && !stat.isDirectory()) throw new Error(`Non-directory ancestor: ${cursor}`);
54
+ if (cursor === file && !stat.isFile()) throw new Error(`Non-file target: ${cursor}`);
55
+ } catch (error) { if (error.code !== 'ENOENT') throw error; }
56
+ }
57
+ }
58
+
59
+ function configPlan(file, host, options) {
60
+ assertSafePath(file, options.home);
61
+ let before = null; let mode = 0o600;
62
+ try { before = fs.readFileSync(file, 'utf8'); mode = fs.statSync(file).mode & 0o777; } catch (error) { if (error.code !== 'ENOENT') throw error; }
63
+ let config;
64
+ try { config = before === null ? {} : JSON.parse(before); } catch { throw new Error(`Malformed JSON: ${file}`); }
65
+ const object = value => value && typeof value === 'object' && !Array.isArray(value);
66
+ if (!object(config) || (config.hooks !== undefined && !object(config.hooks))) throw new Error(`Malformed hooks object: ${file}`);
67
+ config.hooks ??= {};
68
+ const entries = config.hooks.UserPromptSubmit === undefined ? [] : config.hooks.UserPromptSubmit;
69
+ if (!Array.isArray(entries)) throw new Error(`Malformed UserPromptSubmit: ${file}`);
70
+ const owned = [];
71
+ for (const [index, entry] of entries.entries()) {
72
+ if (!object(entry) || !Array.isArray(entry.hooks)) throw new Error(`Malformed hook group: ${file}`);
73
+ for (const hook of entry.hooks) {
74
+ if (!object(hook) || typeof hook.type !== 'string') throw new Error(`Malformed hook: ${file}`);
75
+ if (hook.type !== 'command') continue;
76
+ if (typeof hook.command !== 'string') throw new Error(`Malformed command: ${file}`);
77
+ const args = literalArgv(hook.command);
78
+ const expected = host === 'claude' ? ['--claude'] : [];
79
+ if (args?.[1] === options.script && realNode(args[0], options.nodeExecutable, options.searchPath)
80
+ && JSON.stringify(args.slice(2)) === JSON.stringify(expected)) owned.push({ index, hook });
81
+ else if (args?.includes(options.script) || hook.command.includes(options.script)) {
82
+ throw new Error(`Unowned model-hook override requires review: ${file}`);
83
+ }
84
+ }
85
+ }
86
+ if (owned.length > 1) throw new Error(`Ambiguous duplicate owned registrations: ${file}`);
87
+ const nodeCommand = realNode('node', options.nodeExecutable, options.searchPath) ? 'node' : shellQuote(options.nodeExecutable);
88
+ const command = `${nodeCommand} ${shellQuote(options.script)}${host === 'claude' ? ' --claude' : ''}`;
89
+ if (!owned.length) entries.push({ hooks: [{ type: 'command', command, timeout: 3 }] });
90
+ // Existing exact registrations are preserved byte-for-byte, including private group options.
91
+ config.hooks.UserPromptSubmit = entries;
92
+ const after = owned.length ? before : `${JSON.stringify(config, null, 2)}\n`;
93
+ return { host, file, before, after, mode, changed: before !== after, registration: owned.length ? 'present' : 'add' };
94
+ }
95
+
96
+ export function planUserModelHooks({ home = os.homedir(), nodeExecutable = process.execPath, searchPath = process.env.PATH || '' } = {}) {
97
+ home = path.resolve(home);
98
+ const script = path.join(home, '.claude/model-router/bin/user-model-prompt-hook.mjs');
99
+ const options = { home, script, nodeExecutable: path.resolve(nodeExecutable), searchPath };
100
+ return { home, script, changes: [
101
+ configPlan(path.join(home, '.claude/settings.json'), 'claude', options),
102
+ configPlan(path.join(home, '.codex/hooks.json'), 'codex', options),
103
+ ] };
104
+ }
105
+
106
+ export function applyUserModelHooks(plan) {
107
+ assertSafePath(plan.script, plan.home);
108
+ if (!fs.existsSync(plan.script)) throw new Error('Install the user hook runtime before applying registrations');
109
+ // Validate every original before any writes, so stale plans never overwrite a concurrent edit.
110
+ for (const change of plan.changes) {
111
+ assertSafePath(change.file, plan.home);
112
+ const current = fs.existsSync(change.file) ? fs.readFileSync(change.file, 'utf8') : null;
113
+ if (current !== change.before) throw new Error(`Config changed since plan: ${change.file}`);
114
+ }
115
+ const written = [];
116
+ for (const change of plan.changes.filter(item => item.changed)) {
117
+ fs.mkdirSync(path.dirname(change.file), { recursive: true, mode: 0o700 });
118
+ assertSafePath(change.file, plan.home);
119
+ const suffix = `${Date.now()}-${crypto.randomBytes(6).toString('hex')}`;
120
+ const backup = change.before === null ? null : `${change.file}.backup-${suffix}`;
121
+ if (backup) fs.copyFileSync(change.file, backup, fs.constants.COPYFILE_EXCL);
122
+ const temporary = `${change.file}.tmp-${suffix}`;
123
+ let fd;
124
+ try {
125
+ fd = fs.openSync(temporary, 'wx', change.mode);
126
+ fs.writeFileSync(fd, change.after); fs.fchmodSync(fd, change.mode); fs.fsyncSync(fd); fs.closeSync(fd); fd = undefined;
127
+ assertSafePath(change.file, plan.home);
128
+ const latest = fs.existsSync(change.file) ? fs.readFileSync(change.file, 'utf8') : null;
129
+ if (latest !== change.before) throw new Error(`Config changed before rename: ${change.file}`);
130
+ fs.renameSync(temporary, change.file);
131
+ written.push({ file: change.file, backup });
132
+ } finally { if (fd !== undefined) fs.closeSync(fd); if (fs.existsSync(temporary)) fs.unlinkSync(temporary); }
133
+ }
134
+ return { applied: true, written };
135
+ }
136
+
137
+ if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
138
+ try {
139
+ const args = process.argv.slice(2);
140
+ if (args.some(arg => !['--apply', '--dry-run', '--plan'].includes(arg))) throw new Error('Usage: user-model-hook-install.mjs [--apply|--dry-run|--plan]');
141
+ const plan = planUserModelHooks();
142
+ const result = args.includes('--apply') ? applyUserModelHooks(plan) : { applied: false, script: plan.script, changes: plan.changes.map(({ host, file, changed, registration }) => ({ host, file, changed, registration })) };
143
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
144
+ } catch (error) { process.stderr.write(`${error.message}\n`); process.exitCode = 1; }
145
+ }