ruvnet-brain 4.5.4 → 4.5.6

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 (58) hide show
  1. package/README.md +2 -2
  2. package/bin/install.mjs +162 -27
  3. package/config/model-router/catalog.template.json +126 -52
  4. package/config/model-router/claude-terminal-mod/.claude-plugin/plugin.json +1 -0
  5. package/config/model-router/claude-terminal-mod/README.md +22 -0
  6. package/config/model-router/claude-terminal-mod/hooks/hooks.json +1 -0
  7. package/config/model-router/claude-terminal-mod/hooks/policy.default.mjs +2 -0
  8. package/config/model-router/claude-terminal-mod/hooks/register.js +66 -0
  9. package/config/model-router/claude-terminal-mod/hooks/routing.js +55 -0
  10. package/config/model-router/claude-terminal-mod/hooks/runtime.js +2 -0
  11. package/config/model-router/claude-terminal-mod/tests/native.test.ts +86 -0
  12. package/config/model-router/policy.default.mjs +97 -75
  13. package/config/model-router/qualification-contract.json +124 -0
  14. package/config/model-router/routing-eval-cases.json +275 -0
  15. package/config/model-router/routing-policy.template.json +76 -0
  16. package/config/model-router/weekly-analyst-instruction.md +60 -0
  17. package/data/model-catalog.json +44 -49
  18. package/package.json +6 -3
  19. package/plugin/.claude-plugin/plugin.json +1 -1
  20. package/plugin/.codex-plugin/plugin.json +1 -1
  21. package/plugin/mcp/managed-cli-interface.mjs +9 -4
  22. package/plugin/scripts/capability-claim-evidence.mjs +9 -2
  23. package/plugin/scripts/codex-hook-adapter.mjs +18 -9
  24. package/plugin/scripts/project-capture-queue.mjs +7 -1
  25. package/plugin/scripts/project-progression-hook.mjs +16 -7
  26. package/plugin/scripts/project-progression-outbox.mjs +96 -15
  27. package/plugin/scripts/project-progression-producer.mjs +12 -1
  28. package/plugin/scripts/project-progression-store.mjs +53 -1
  29. package/plugin/scripts/project-transition-hook.mjs +19 -8
  30. package/plugin/scripts/session-snapshot-hook.mjs +2 -1
  31. package/scripts/claude-terminal-mod.mjs +89 -0
  32. package/scripts/codex-hook-trust-reconcile.mjs +247 -0
  33. package/scripts/codex-routed.sh +3 -36
  34. package/scripts/goldie-weekly.sh +8 -64
  35. package/scripts/metaharness-router.mjs +7 -1
  36. package/scripts/model-analyst-sandbox.mjs +54 -0
  37. package/scripts/model-currency-evidence.mjs +139 -0
  38. package/scripts/model-currency.mjs +230 -0
  39. package/scripts/model-native-catalog.mjs +111 -0
  40. package/scripts/model-native-qualification.mjs +251 -0
  41. package/scripts/model-router-agent-hook.mjs +136 -0
  42. package/scripts/model-router-dispatch.mjs +161 -0
  43. package/scripts/model-router-engine.mjs +155 -104
  44. package/scripts/model-routing-eval.mjs +108 -0
  45. package/scripts/model-routing-gateway.mjs +453 -0
  46. package/scripts/model-routing-launchers.mjs +209 -0
  47. package/scripts/model-routing-policy-promotion.mjs +203 -0
  48. package/scripts/model-terminal-gateway.mjs +310 -0
  49. package/scripts/model-terminal-launchers.mjs +300 -0
  50. package/scripts/model-weekly-analyst.mjs +299 -0
  51. package/scripts/model-weekly-assessment.mjs +91 -0
  52. package/scripts/model-weekly-cycle.mjs +183 -0
  53. package/scripts/model-weekly-qualification.mjs +362 -0
  54. package/scripts/native-subscription-usage.mjs +57 -0
  55. package/scripts/release-qualification-contract.mjs +50 -1
  56. package/scripts/release-qualification.mjs +9 -6
  57. package/scripts/security-guidance-codex-compat.mjs +142 -0
  58. package/scripts/user-model-prompt-hook.mjs +69 -0
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Explicit local repair for the Codex cache copy of security-guidance 2.0.9.
4
+ * Dry run by default; --apply is required. No plugin trust/settings changes.
5
+ * Codex 0.160.0's installed post-tool-use.command.output rejects metrics and
6
+ * rewakeSummary; its parser also rejects updatedMCPToolOutput semantically.
7
+ * This source-bound patch routes telemetry to the plugin's existing debug log,
8
+ * preserves security controls/context, and leaves other events/review logic intact.
9
+ * Future versions or different source bytes require a new reviewed compatibility
10
+ * patch. A successful repair is not native execution evidence or future-update support.
11
+ */
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import os from 'node:os';
15
+ import crypto from 'node:crypto';
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+ export const COMPATIBILITY = Object.freeze({
19
+ version: '2.0.9', nativeContract: 'Codex 0.160.0 PostToolUse',
20
+ originalSha256: '2766399e43bd923ed786f8bb0fcbf6d2055b3ad4ef79ec73f722be19122de1f8',
21
+ patchedSha256: '4bba541507b54b6b1f882ac13d49be9b8ea865b4e3dc3fd870820a3b48749de1',
22
+ patchSha256: '49f0c628775d9b12a5b50ea5d3072a031c3a2e986fee050dd80f46c044677a35',
23
+ });
24
+ const PATCH = fileURLToPath(new URL('../config/hook-compat/security-guidance-2.0.9.patch', import.meta.url));
25
+ const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
26
+ const noFollow = fs.constants.O_NOFOLLOW;
27
+
28
+ function requireRegularPath(file) {
29
+ let current = path.parse(file).root;
30
+ for (const segment of file.slice(current.length).split(path.sep)) {
31
+ current = path.join(current, segment);
32
+ const stat = fs.lstatSync(current);
33
+ if (stat.isSymbolicLink()) throw new Error(`Refusing symlink: ${current}`);
34
+ if (current !== file && !stat.isDirectory()) throw new Error(`Not a directory: ${current}`);
35
+ if (current === file && !stat.isFile()) throw new Error(`Not a regular file: ${file}`);
36
+ }
37
+ }
38
+
39
+ function readRegular(file) {
40
+ requireRegularPath(file);
41
+ if (noFollow === undefined) throw new Error('O_NOFOLLOW is required for this repair');
42
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | noFollow);
43
+ try {
44
+ const stat = fs.fstatSync(fd);
45
+ if (!stat.isFile()) throw new Error(`Not a regular file: ${file}`);
46
+ return { bytes: fs.readFileSync(fd), stat };
47
+ } finally { fs.closeSync(fd); }
48
+ }
49
+
50
+ /** Apply only the checked-in exact-context hunks, without invoking patch/git/Python. */
51
+ export function applyCompatibilityPatch(original) {
52
+ const patch = readRegular(PATCH).bytes;
53
+ if (sha256(patch) !== COMPATIBILITY.patchSha256) throw new Error('Compatibility patch digest mismatch');
54
+ if (sha256(original) !== COMPATIBILITY.originalSha256) throw new Error('Original source digest mismatch');
55
+ const source = original.toString('utf8').split('\n');
56
+ const lines = patch.toString('utf8').split('\n');
57
+ const result = [];
58
+ let cursor = 0;
59
+ for (let i = 2; i < lines.length; i++) {
60
+ const header = /^@@ -(\d+),(\d+) \+(\d+),(\d+) @@/.exec(lines[i]);
61
+ if (!header) { if (!lines[i]) continue; throw new Error('Unexpected patch structure'); }
62
+ const start = Number(header[1]) - 1;
63
+ if (start < cursor) throw new Error('Overlapping patch hunks');
64
+ result.push(...source.slice(cursor, start)); cursor = start;
65
+ let oldCount = 0; let newCount = 0;
66
+ while (i + 1 < lines.length && !lines[i + 1].startsWith('@@ ') && lines[i + 1] !== '') {
67
+ const line = lines[++i]; const type = line[0]; const content = line.slice(1);
68
+ if (![' ', '-', '+'].includes(type)) throw new Error('Unsupported patch line');
69
+ if (type !== '+') {
70
+ if (source[cursor++] !== content) throw new Error('Compatibility patch context mismatch');
71
+ oldCount++;
72
+ }
73
+ if (type !== '-') { result.push(content); newCount++; }
74
+ }
75
+ if (oldCount !== Number(header[2]) || newCount !== Number(header[4])) throw new Error('Patch hunk count mismatch');
76
+ }
77
+ result.push(...source.slice(cursor));
78
+ const patched = Buffer.from(result.join('\n'));
79
+ if (sha256(patched) !== COMPATIBILITY.patchedSha256) throw new Error('Patched source digest mismatch');
80
+ return patched;
81
+ }
82
+
83
+ function syncDirectory(dir) {
84
+ const fd = fs.openSync(dir, fs.constants.O_RDONLY | noFollow);
85
+ try { fs.fsyncSync(fd); } finally { fs.closeSync(fd); }
86
+ }
87
+
88
+ export function repairSecurityGuidance({ codexHome = process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), apply = false } = {}) {
89
+ if (!path.isAbsolute(codexHome) || path.normalize(codexHome).split(path.sep).includes('.claude')) {
90
+ throw new Error('An absolute Codex home is required; Claude cache repair is forbidden');
91
+ }
92
+ const target = path.join(codexHome, 'plugins/cache/claude-plugins-official/security-guidance', COMPATIBILITY.version, 'hooks/security_reminder_hook.py');
93
+ const original = readRegular(target);
94
+ const digest = sha256(original.bytes);
95
+ const receipt = { target, compatibility: COMPATIBILITY, sourceSha256: digest, apply, status: 'dry-run' };
96
+ if (digest === COMPATIBILITY.patchedSha256) return { ...receipt, status: 'already-patched' };
97
+ if (digest !== COMPATIBILITY.originalSha256) throw new Error(`Unknown security-guidance source: ${digest}; refusing to patch ${target}`);
98
+ const patched = applyCompatibilityPatch(original.bytes);
99
+ if (!apply) return receipt;
100
+ const dir = path.dirname(target);
101
+ const backup = `${target}.${COMPATIBILITY.originalSha256}.original`;
102
+ let fd;
103
+ try {
104
+ fd = fs.openSync(backup, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | noFollow, 0o600);
105
+ fs.writeFileSync(fd, original.bytes);
106
+ fs.fchmodSync(fd, (original.stat.mode & 0o444) || 0o400);
107
+ fs.fsyncSync(fd);
108
+ } catch (error) {
109
+ if (error.code !== 'EEXIST') throw error;
110
+ if (sha256(readRegular(backup).bytes) !== digest) throw new Error('Existing immutable backup digest mismatch');
111
+ } finally { if (fd !== undefined) fs.closeSync(fd); }
112
+ syncDirectory(dir);
113
+ const temporary = path.join(dir, `.security-guidance-compat-${crypto.randomUUID()}.tmp`);
114
+ try {
115
+ fd = fs.openSync(temporary, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | noFollow, 0o600);
116
+ try {
117
+ fs.writeFileSync(fd, patched);
118
+ fs.fchmodSync(fd, original.stat.mode & 0o777);
119
+ fs.fsyncSync(fd);
120
+ } finally { fs.closeSync(fd); }
121
+ const current = readRegular(target);
122
+ if (current.stat.dev !== original.stat.dev || current.stat.ino !== original.stat.ino || sha256(current.bytes) !== digest) {
123
+ throw new Error('Plugin source changed during repair; refusing replacement');
124
+ }
125
+ fs.renameSync(temporary, target);
126
+ syncDirectory(dir);
127
+ } finally { if (fs.existsSync(temporary)) fs.unlinkSync(temporary); }
128
+ return { ...receipt, status: 'patched', backup, patchedSha256: sha256(readRegular(target).bytes) };
129
+ }
130
+
131
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
132
+ try {
133
+ const args = process.argv.slice(2);
134
+ let codexHome; let apply = false;
135
+ for (let i = 0; i < args.length; i++) {
136
+ if (args[i] === '--apply') apply = true;
137
+ else if (args[i] === '--codex-home' && args[i + 1]) codexHome = args[++i];
138
+ else throw new Error(`Unknown argument: ${args[i]}`);
139
+ }
140
+ console.log(JSON.stringify(repairSecurityGuidance({ codexHome, apply }), null, 2));
141
+ } catch (error) { console.error(error.message); process.exitCode = 1; }
142
+ }
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ // Per-user prompt guidance. Execution is enforced separately by the managed dispatcher.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import os from 'node:os';
6
+ import { spawnSync } from 'node:child_process';
7
+ import { pathToFileURL } from 'node:url';
8
+ import { maybeLaunchWeeklyCycle } from './model-weekly-cycle.mjs';
9
+
10
+ export const PRESENTATION = 'For terminal briefings: give concise executive status; use narrow padded ASCII tables with borders, plain cell text, aligned columns and short rows. Never send pipe-delimited Markdown tables to this terminal. Use short lists when a table would wrap. Do not flood the response with commands or technical logs.';
11
+
12
+ export function promptContext(payload, { routerDir = path.join(os.homedir(), '.claude/model-router'), run = spawnSync, now = Date.now(), cycle = maybeLaunchWeeklyCycle } = {}) {
13
+ const lines = [PRESENTATION];
14
+ const weekly = cycle({ routerDir, now });
15
+ if (weekly.status === 'current' && weekly.reviewRequired === false) lines.push(`Weekly model-release check current (${weekly.checkedAt}); retain the approved policy. Full assessment runs only for newly discovered OpenAI or Anthropic models. This is not a semantic-review or promotion claim.`);
16
+ else if (weekly.launched) lines.push(weekly.reviewRequired
17
+ ? 'A new model release requires assessment; the bounded review is pending and the approved policy remains active.'
18
+ : 'A lightweight weekly model-release check started; no full assessment is required unless a new relevant model is found.');
19
+ else if (weekly.status === 'blocked') lines.push(`Weekly model-release check needs attention: ${weekly.reason || 'verification unavailable'}. Retain the approved policy; do not claim no changes or a completed review.`);
20
+ const harness = payload?.host === 'claude-code' ? 'claude-code' : 'codex';
21
+ const prompt = typeof payload?.prompt === 'string' ? payload.prompt : '';
22
+ let policy;
23
+ try { policy = JSON.parse(fs.readFileSync(path.join(routerDir, 'routing-policy.json'), 'utf8')); } catch { /* report absence below */ }
24
+ const reviewed = Date.parse(policy?.reviewedAt);
25
+ const age = now - reviewed;
26
+ const configuredMaxAge = policy?.maxAgeMs ?? 604800000;
27
+ const maxAge = Math.min(configuredMaxAge, 604800000);
28
+ const current = policy?.schemaVersion === 1 && Number.isSafeInteger(configuredMaxAge) && configuredMaxAge > 0 && Number.isFinite(reviewed) && age >= 0;
29
+ if (current && age > maxAge) lines.push('Policy approval is older than seven days. Retain the owner-approved allocation and its original date; verify native availability before every launch. Age alone does not trigger reassessment.');
30
+ lines.push(current ? `Model routing policy reviewed ${policy.reviewedAt}; consult this user's policy for every delegated launch.` : 'Model routing policy is missing or invalid. Establish a valid approved policy before managed dispatch.');
31
+ if (current && prompt && prompt.length <= 65536) {
32
+ const engine = path.join(routerDir, 'bin/model-router-engine.mjs');
33
+ const result = run(process.execPath, [engine, '--harness', harness, '--policy-only', '--json'], {
34
+ input: prompt, encoding: 'utf8', timeout: 1500, maxBuffer: 16384,
35
+ stdio: ['pipe', 'pipe', 'pipe'],
36
+ });
37
+ try {
38
+ const route = JSON.parse(result.stdout || '');
39
+ if (result.status === 0 && /^[a-zA-Z0-9._-]+$/.test(route.model || '') && ['low', 'medium', 'high', 'xhigh', 'max'].includes(route.effort)) {
40
+ lines.push(`This prompt's managed route: ${route.model}, effort ${route.effort}. Use the managed dispatcher for actual launch enforcement. This hook supplies context; it does not switch the active parent model.`);
41
+ } else lines.push('Prompt routing was not verified; do not infer an automatic model switch.');
42
+ } catch { lines.push('Prompt routing was not verified; do not infer an automatic model switch.'); }
43
+ }
44
+ return lines.join('\n');
45
+ }
46
+
47
+ export function envelope(context) {
48
+ return { hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: context } };
49
+ }
50
+
51
+ async function main() {
52
+ let input = '';
53
+ const timer = setTimeout(() => process.stdin.destroy(), 1000);
54
+ try {
55
+ for await (const chunk of process.stdin) {
56
+ input += chunk;
57
+ if (input.length > 65536) { input = ''; break; }
58
+ }
59
+ } catch { /* fail open with presentation guidance */ }
60
+ clearTimeout(timer);
61
+ let payload = {};
62
+ try { payload = JSON.parse(input); } catch { /* malformed input never launches a model */ }
63
+ if (process.argv.includes('--claude')) payload.host = 'claude-code';
64
+ process.stdout.write(JSON.stringify(envelope(promptContext(payload))) + '\n');
65
+ }
66
+
67
+ if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
68
+ main().catch(() => process.stdout.write(JSON.stringify(envelope(PRESENTATION)) + '\n'));
69
+ }