ruvnet-brain 4.0.90-dev → 4.3.1-dev

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 (74) hide show
  1. package/README.md +31 -7
  2. package/bin/install.mjs +153 -22
  3. package/console/architecture.html +12 -14
  4. package/console/install-mockup.html +3 -3
  5. package/kb/zip-extract.mjs +8 -2
  6. package/package.json +7 -1
  7. package/plugin/.claude-plugin/plugin.json +1 -1
  8. package/plugin/.codex-plugin/plugin.json +1 -1
  9. package/plugin/hooks/codex-hooks.json +1 -1
  10. package/plugin/hooks/hooks.json +10 -0
  11. package/plugin/host-adapters/claude.json +11 -0
  12. package/plugin/host-adapters/codex.json +11 -0
  13. package/plugin/mcp/managed-cli-interface.mjs +77 -10
  14. package/plugin/mcp/server.mjs +2 -1
  15. package/plugin/scripts/anticipate.sh +30 -1
  16. package/plugin/scripts/capability-claim-evidence.mjs +431 -0
  17. package/plugin/scripts/capability-inventory-receipt.mjs +225 -0
  18. package/plugin/scripts/capability-registry.mjs +6 -0
  19. package/plugin/scripts/capability-routing.mjs +70 -0
  20. package/plugin/scripts/continuation-gate.mjs +68 -2
  21. package/plugin/scripts/coverage-integrity.mjs +575 -0
  22. package/plugin/scripts/decision-outcomes.mjs +0 -0
  23. package/plugin/scripts/ground-ruvnet.sh +2 -2
  24. package/plugin/scripts/nightly-controller.mjs +19 -2
  25. package/plugin/scripts/project-progression-contract.mjs +409 -0
  26. package/plugin/scripts/project-progression-hook.mjs +196 -0
  27. package/plugin/scripts/project-progression-outbox.mjs +95 -0
  28. package/plugin/scripts/project-progression-session-start.mjs +183 -0
  29. package/plugin/scripts/project-progression-store.mjs +248 -0
  30. package/plugin/scripts/project-store-resolver.mjs +106 -0
  31. package/plugin/scripts/session-snapshot-hook.mjs +29 -1
  32. package/plugin/scripts/session-start-core.mjs +22 -18
  33. package/plugin/scripts/spend-guard.mjs +6 -1
  34. package/scripts/adr-072-completion.mjs +98 -0
  35. package/scripts/behavioral-l1-l4.mjs +11 -8
  36. package/scripts/brain-score.mjs +23 -9
  37. package/scripts/build-bundle.mjs +94 -8
  38. package/scripts/card-from-source.mjs +140 -0
  39. package/scripts/corpus-aggregates.mjs +194 -0
  40. package/scripts/corpus-candidate.mjs +314 -0
  41. package/scripts/corpus-reconcile.mjs +540 -0
  42. package/scripts/corpus-seed-publish.mjs +110 -0
  43. package/scripts/coverage-integrity.mjs +3 -0
  44. package/scripts/gist-receipts.mjs +183 -0
  45. package/scripts/git-hooks/pre-push +21 -0
  46. package/scripts/host-registry.mjs +118 -0
  47. package/scripts/independent-review-receipt.mjs +499 -0
  48. package/scripts/ingest-gists.mjs +34 -5
  49. package/scripts/ingest-new-repos.mjs +127 -0
  50. package/scripts/metaharness-gate.mjs +58 -0
  51. package/scripts/nightly-gists.sh +3 -3
  52. package/scripts/nightly-wrapper.sh +105 -64
  53. package/scripts/product-integrity-contract.mjs +156 -0
  54. package/scripts/public-inventory.mjs +4 -0
  55. package/scripts/public-verification-aggregate.mjs +342 -0
  56. package/scripts/public-verification-finalizer.mjs +59 -0
  57. package/scripts/public-verification-inputs.mjs +473 -0
  58. package/scripts/public-verification-lane.mjs +244 -0
  59. package/scripts/publication-receipt.mjs +50 -6
  60. package/scripts/rebuild-gists-from-receipts.mjs +291 -0
  61. package/scripts/release-projection.mjs +86 -0
  62. package/scripts/release-transaction-provider.mjs +43 -102
  63. package/scripts/release-transaction.mjs +165 -49
  64. package/scripts/release.mjs +158 -3
  65. package/scripts/restore-local-ingests.mjs +2 -2
  66. package/scripts/retrieval-canary.mjs +533 -0
  67. package/scripts/routing-flywheel.mjs +12 -1
  68. package/scripts/rvf-generation.mjs +50 -0
  69. package/scripts/self-update.mjs +19 -44
  70. package/scripts/source-coverage.mjs +415 -0
  71. package/scripts/source-scope-receipt.mjs +84 -0
  72. package/scripts/sync-census.mjs +0 -0
  73. package/scripts/wired-check.mjs +114 -24
  74. package/scripts/worktree-integrity.mjs +123 -0
@@ -3,6 +3,7 @@ import fs from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { loadRuntimePreferences, runtimeChildEnv } from '../scripts/runtime-preferences.mjs';
6
+ import { recordManagedCliObservation, recordRegistryLatestObservation } from '../scripts/capability-claim-evidence.mjs';
6
7
 
7
8
  export const MANAGED_EXECUTABLES = Object.freeze([
8
9
  'ruflo',
@@ -15,6 +16,15 @@ export const MANAGED_EXECUTABLES = Object.freeze([
15
16
  ]);
16
17
 
17
18
  const MANAGED = new Set(MANAGED_EXECUTABLES);
19
+ const REGISTRY_PACKAGES = Object.freeze({
20
+ ruflo: 'ruflo',
21
+ 'claude-flow': '@claude-flow/cli',
22
+ 'agentic-flow': 'agentic-flow',
23
+ 'agentic-qe': 'agentic-qe',
24
+ ruvector: 'ruvector',
25
+ 'agent-browser': 'agent-browser',
26
+ 'ruv-swarm': 'ruv-swarm',
27
+ });
18
28
  const SUBCOMMAND = /^[a-z][a-z0-9-]*$/;
19
29
  const MAX_ARGS = 256;
20
30
  const MAX_ARG_BYTES = 8192;
@@ -29,6 +39,17 @@ const executableSchema = {
29
39
  };
30
40
 
31
41
  export const MANAGED_CLI_TOOLS = Object.freeze([
42
+ {
43
+ name: 'ruvnet_registry_latest',
44
+ description: 'Read the exact npm registry latest version for a managed RuvNet executable and record a content-bound public-registry receipt.',
45
+ inputSchema: {
46
+ type: 'object',
47
+ additionalProperties: false,
48
+ properties: { executable: executableSchema },
49
+ required: ['executable'],
50
+ },
51
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
52
+ },
32
53
  {
33
54
  name: 'ruvnet_cli_help',
34
55
  description: 'Read a managed CLI interface from the executable itself. Runs only the supplied subcommand path plus --help and records a fresh stamp only after exit 0.',
@@ -146,20 +167,43 @@ function writeStamps(executable, argv, env) {
146
167
  export function resolveManagedExecutable(executable, env = process.env) {
147
168
  if (executable !== 'ruflo') return executable;
148
169
  const home = env.HOME || os.homedir();
149
- const canonical = path.join(home, '.npm-global', 'bin', 'ruflo');
150
- try {
151
- fs.accessSync(canonical, fs.constants.X_OK);
152
- return canonical;
153
- } catch {
154
- return executable;
170
+ const candidates = [path.join(home, '.npm-global', 'bin', 'ruflo')];
171
+ if (process.platform === 'win32') candidates.push(`${candidates[0]}.cmd`);
172
+ return candidates.find((candidate) => {
173
+ try { fs.accessSync(candidate, fs.constants.X_OK); return true; } catch { return false; }
174
+ }) || executable;
175
+ }
176
+
177
+ function quoteWindowsCommandArg(value) {
178
+ // cmd.exe is required for npm's .cmd shims. Keep the child_process call itself shell:false,
179
+ // and quote every literal token so shell metacharacters remain data rather than syntax.
180
+ // Delayed expansion is disabled by default; doubling percent signs prevents environment
181
+ // expansion while cmd parses the /c command line.
182
+ return `"${value.replace(/%/g, '%%').replace(/"/g, '""')}"`;
183
+ }
184
+
185
+ function spawnSpec(executable, argv, env) {
186
+ const resolved = resolveManagedExecutable(executable, env);
187
+ if (process.platform !== 'win32' || !resolved.toLowerCase().endsWith('.cmd')) {
188
+ return { file: resolved, args: argv, windowsVerbatimArguments: false };
155
189
  }
190
+ // `/s /c` removes the first and last quote characters from its command string. The outer
191
+ // envelope therefore preserves the inner quotes around a shim path containing spaces.
192
+ const command = `"${[resolved, ...argv].map(quoteWindowsCommandArg).join(' ')}"`;
193
+ return {
194
+ file: env.ComSpec || env.COMSPEC || 'cmd.exe',
195
+ args: ['/d', '/s', '/c', command],
196
+ windowsVerbatimArguments: true,
197
+ };
156
198
  }
157
199
 
158
200
  function execute(executable, argv, env) {
159
201
  return new Promise((resolve) => {
160
- const child = spawn(resolveManagedExecutable(executable, env), argv, {
202
+ const spec = spawnSpec(executable, argv, env);
203
+ const child = spawn(spec.file, spec.args, {
161
204
  env,
162
205
  shell: false,
206
+ windowsVerbatimArguments: spec.windowsVerbatimArguments,
163
207
  stdio: ['ignore', 'pipe', 'pipe'],
164
208
  });
165
209
  const stdout = [];
@@ -213,16 +257,37 @@ function resultOf(executable, argv, result) {
213
257
  };
214
258
  }
215
259
 
216
- export async function callManagedCli(toolName, args, env = process.env) {
260
+ export async function callManagedCli(toolName, args, env = process.env, fetchImpl = globalThis.fetch) {
217
261
  try {
218
262
  const executable = assertExecutable(args?.executable);
219
- const argv = literalArgv(args?.argv);
263
+ const argv = literalArgv(args?.argv ?? []);
264
+
265
+ if (toolName === 'ruvnet_registry_latest') {
266
+ const packageName = REGISTRY_PACKAGES[executable];
267
+ const registryUrl = `https://registry.npmjs.org/${packageName.replace('/', '%2F')}/latest`;
268
+ const response = await fetchImpl(registryUrl, {
269
+ headers: { accept: 'application/json' },
270
+ signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS),
271
+ });
272
+ const body = await response.text();
273
+ if (!response.ok) throw new Error(`registry latest lookup failed with HTTP ${response.status}`);
274
+ let metadata;
275
+ try { metadata = JSON.parse(body); } catch { throw new Error('registry latest response was not JSON'); }
276
+ const receipt = recordRegistryLatestObservation({
277
+ executable, packageName, version: metadata?.version, registryUrl, responseBody: body, env,
278
+ });
279
+ return {
280
+ content: [{ type: 'text', text: `${packageName} latest version: ${receipt.observedVersion} (registry receipt ${receipt.receiptSha256})` }],
281
+ isError: false,
282
+ };
283
+ }
220
284
 
221
285
  if (toolName === 'ruvnet_cli_help') {
222
286
  stampKeysForHelp(executable, argv);
223
287
  const commandArgv = [...argv, '--help'];
224
288
  const execution = await execute(executable, commandArgv, env);
225
289
  if (execution.code === 0 && !execution.error) writeStamps(executable, argv, env);
290
+ recordManagedCliObservation({ toolName, executable, argv: commandArgv, execution, env });
226
291
  return resultOf(executable, commandArgv, execution);
227
292
  }
228
293
 
@@ -263,7 +328,9 @@ export async function callManagedCli(toolName, args, env = process.env) {
263
328
  const childEnv = (executable === 'agentic-flow' || executable === 'agentic-qe')
264
329
  ? runtimeChildEnv({ env, cwd: env.RUVNET_BRAIN_PROJECT_DIR || process.cwd() })
265
330
  : env;
266
- return resultOf(executable, argv, await execute(executable, argv, childEnv));
331
+ const execution = await execute(executable, argv, childEnv);
332
+ recordManagedCliObservation({ toolName, executable, argv, execution, env });
333
+ return resultOf(executable, argv, execution);
267
334
  }
268
335
 
269
336
  return {
@@ -327,7 +327,8 @@ async function handleClient(msg) {
327
327
  return clientOk(id, { tools: FALLBACK_TOOLS });
328
328
  }
329
329
  case 'tools/call': {
330
- if (params?.name === 'ruvnet_cli_help' || params?.name === 'ruvnet_cli_run') {
330
+ if (params?.name === 'ruvnet_cli_help' || params?.name === 'ruvnet_cli_run'
331
+ || params?.name === 'ruvnet_registry_latest') {
331
332
  return clientOk(id, await callManagedCli(params.name, params.arguments || {}));
332
333
  }
333
334
  if (params?.name !== 'search_ruvnet') return clientErr(id, -32602, `unknown tool: ${params?.name}`);
@@ -195,6 +195,7 @@ import { pathToFileURL } from 'node:url';
195
195
  const MODE = process.env.RUVNET_ANTICIPATE_MODE || 'suggest';
196
196
  const ARG = process.env.RUVNET_ANTICIPATE_ARG || '';
197
197
  const SELF = process.env.RUVNET_ANTICIPATE_SELF || 'anticipate.sh';
198
+ const SELF_DIR = path.dirname(SELF);
198
199
 
199
200
  // CANDIDATE MODE (ADR-040 / DDD-0004 "the enforcement chokepoint"). Set by unprompted-runtime.mjs on
200
201
  // every producer child. When on, this hook writes ZERO user-facing prose: it emits ONE advocacy
@@ -414,13 +415,18 @@ const sessions = st.sessions && typeof st.sessions === 'object' ? st.sessions :
414
415
  const said = new Set(strings(sessions[sid]?.said));
415
416
  if (said.size >= MAX_PER_SESSION) quit();
416
417
 
417
- let auditAll, matchGoal, floor;
418
+ let auditAll, matchGoal, floor, buildCapabilityRoutingReceipt, hasToolPreference;
418
419
  try { ({ auditAll } = await import(pathToFileURL(process.env.RUVNET_CAPABILITY_REGISTRY).href)); } catch { quit(); }
419
420
  try {
420
421
  const gm = await import(pathToFileURL(process.env.RUVNET_GOAL_MATCH).href);
421
422
  matchGoal = gm.matchGoal;
422
423
  floor = gm.CONFIDENCE_FLOOR;
423
424
  } catch { quit(); }
425
+ try {
426
+ const routingPath = process.env.RUVNET_CAPABILITY_ROUTING_MODULE
427
+ || path.join(SELF_DIR, 'capability-routing.mjs');
428
+ ({ buildCapabilityRoutingReceipt, hasToolPreference } = await import(pathToFileURL(routingPath).href));
429
+ } catch { /* older packed hosts predate the optional routing receipt */ }
424
430
  if (typeof auditAll !== 'function' || typeof matchGoal !== 'function') quit();
425
431
  const MIN_CONFIDENCE = typeof floor === 'number' && Number.isFinite(floor) ? floor : FALLBACK_CONFIDENCE_FLOOR;
426
432
 
@@ -459,6 +465,28 @@ let matches = [];
459
465
  try { matches = matchGoal(prompt, dormant) || []; } catch { quit(); }
460
466
  if (!Array.isArray(matches) || !matches.length) quit();
461
467
 
468
+ // Only an evidence-bound route to an existing RuvNet building block may leave this hook.
469
+ let routingReceipt;
470
+ try {
471
+ routingReceipt = typeof buildCapabilityRoutingReceipt === 'function'
472
+ ? buildCapabilityRoutingReceipt({ prompt, matches, capabilities: rows }) : null;
473
+ // Test/host adapters may provide synthetic capabilities outside the shipped registry. Preserve
474
+ // their existing protocol; real RuvNet capabilities are fail-closed when no route receipt exists.
475
+ const knownRoute = typeof hasToolPreference === 'function' && matches.some((match) => {
476
+ const key = match?.capability?.key || match?.capability;
477
+ const liveRow = rows.find((row) => row?.key === key);
478
+ return hasToolPreference(key) && typeof liveRow?.evidence === 'string' && liveRow.evidence.trim()
479
+ && typeof liveRow?.evidenceDigest === 'string' && liveRow.evidenceDigest.length === 64;
480
+ });
481
+ if (!routingReceipt && knownRoute) quit();
482
+ if (!routingReceipt) routingReceipt = { receiptSha256: 'synthetic-adapter' };
483
+ const receiptFile = process.env.RUVNET_CAPABILITY_ROUTING_RECEIPTS;
484
+ if (receiptFile) {
485
+ fs.mkdirSync(path.dirname(receiptFile), { recursive: true });
486
+ fs.appendFileSync(receiptFile, `${JSON.stringify(routingReceipt)}\n`, { mode: 0o600 });
487
+ }
488
+ } catch { quit(); }
489
+
462
490
  // The matcher is trusted to rank, never to assert existence: every match is re-resolved against the
463
491
  // dormant rows THIS audit produced. A capability the matcher names that is not dormant right now is
464
492
  // dropped, so a stale or over-eager matcher can only ever cause silence, never a false claim.
@@ -534,6 +562,7 @@ if (EMIT_CANDIDATES) {
534
562
  findingId: best.row.key,
535
563
  severity: best.row.severity || 'normal',
536
564
  observationHash: stateHashOf(best.row.evidence),
565
+ routingReceiptSha256: routingReceipt.receiptSha256,
537
566
  }));
538
567
  quit();
539
568
  }
@@ -0,0 +1,431 @@
1
+ #!/usr/bin/env node
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+
7
+ const HEX40 = /^[a-f0-9]{40}$/;
8
+ const HEX64 = /^[a-f0-9]{64}$/;
9
+ const HOSTS = Object.freeze(['claude', 'codex']);
10
+ const OS_LANES = Object.freeze(['linux', 'macos', 'windows']);
11
+ const CLAIM_CLASSES = Object.freeze(['installation', 'behavior', 'currentVersion', 'latestVersion', 'health']);
12
+ const VERSION = /\bv?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\b/;
13
+ const RUVNET_TOOL = '(?:Ruflo|Claude Flow|Agentic Flow|Agentic QE|RuVector|Agent Browser|Ruv Swarm|AgentDB|RuLake|RuvNet Brain)';
14
+ const MAX_AGE_MS = 10 * 60_000;
15
+ const MAX_LEDGER_BYTES = 1024 * 1024;
16
+ const STOPWORDS = new Set(['a', 'an', 'and', 'are', 'can', 'could', 'does', 'for', 'has', 'have',
17
+ 'is', 'it', 'not', 'of', 'or', 'the', 'this', 'to', 'version', 'with', 'would']);
18
+
19
+ const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
20
+ export const canonicalJson = (value) => {
21
+ if (Array.isArray(value)) return `[${value.map((item) => canonicalJson(item)).join(',')}]`;
22
+ if (value && typeof value === 'object') {
23
+ return `{${Object.keys(value).sort().filter((key) => value[key] !== undefined)
24
+ .map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
25
+ }
26
+ return JSON.stringify(value);
27
+ };
28
+ const digest = (value) => sha256(canonicalJson(value));
29
+ const normalize = (value) => String(value || '').replace(/\s+/g, ' ').replace(/[.!?]+$/g, '').trim().toLowerCase();
30
+ const tokens = (value) => String(value || '').replace(/([a-z0-9])([A-Z])/g, '$1 $2').toLowerCase()
31
+ .match(/[a-z0-9][a-z0-9-]*[a-z0-9]|[a-z0-9]/g) || [];
32
+ const significant = (value) => tokens(value).filter((token) => !STOPWORDS.has(token));
33
+ const overlap = (left, right) => {
34
+ const rightSet = new Set(significant(right));
35
+ return significant(left).filter((token) => rightSet.has(token));
36
+ };
37
+ const safeMessage = (message) => String(message || '')
38
+ .replace(/```[\s\S]*?```/g, ' ')
39
+ .replace(/`[^`\n]*`/g, ' ')
40
+ .replace(/"[^"\n]*"|'[^'\n]*'/g, ' ')
41
+ .replace(/^\s*>.*$/gm, ' ');
42
+ const toolId = (name) => String(name || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
43
+ const executableFor = (name) => ({
44
+ ruflo: 'ruflo', 'claude-flow': 'claude-flow', 'agentic-flow': 'agentic-flow',
45
+ 'agentic-qe': 'agentic-qe', ruvector: 'ruvector', 'agent-browser': 'agent-browser',
46
+ 'ruv-swarm': 'ruv-swarm',
47
+ })[toolId(name)] || null;
48
+
49
+ function liveEvidenceFile(env = process.env) {
50
+ if (env.RUVNET_CAPABILITY_LIVE_EVIDENCE) return path.resolve(env.RUVNET_CAPABILITY_LIVE_EVIDENCE);
51
+ const home = env.HOME || env.USERPROFILE || os.homedir();
52
+ const brainHome = env.RUVNET_BRAIN_HOME || path.join(home, '.cache', 'ruvnet-brain');
53
+ return path.join(brainHome, 'capability-live-evidence.jsonl');
54
+ }
55
+
56
+ function groundingEvidenceFile(env = process.env) {
57
+ if (env.RUVNET_EVIDENCE_FILE) return path.resolve(env.RUVNET_EVIDENCE_FILE);
58
+ const home = env.HOME || env.USERPROFILE || os.homedir();
59
+ const brainHome = env.RUVNET_BRAIN_HOME || path.join(home, '.cache', 'ruvnet-brain');
60
+ return path.join(brainHome, 'evidence.jsonl');
61
+ }
62
+
63
+ function sourcePayload(receipt) {
64
+ const { receiptSha256: _receiptSha256, ...payload } = receipt || {};
65
+ return payload;
66
+ }
67
+
68
+ export function validateSourceClaimReceipt(receipt) {
69
+ if (receipt?.schemaVersion !== 1 || receipt?.kind !== 'ruvnet-brain-source-claim'
70
+ || receipt.verdict !== 'PASS' || !HEX64.test(String(receipt.claimDigest || ''))
71
+ || !HEX64.test(String(receipt.groundingReceiptDigest || ''))
72
+ || !HEX64.test(String(receipt.source?.sha256 || '')) || !receipt.source?.repo || !receipt.source?.path
73
+ || !HEX64.test(String(receipt.receiptSha256 || ''))
74
+ || digest(sourcePayload(receipt)) !== receipt.receiptSha256) {
75
+ throw new Error('source claim receipt is malformed or digest-invalid');
76
+ }
77
+ return receipt;
78
+ }
79
+
80
+ function sourceBindsClaim(claim, source, query) {
81
+ const negative = /\b(?:cannot|can't|does\s+not|doesn't|not\s+(?:support|provide|implement|export|expose))\b/i.test(claim);
82
+ if (negative) {
83
+ return (source.negatives || []).some(({ symbol, quote }) =>
84
+ (symbol && normalize(claim).includes(normalize(symbol))) || overlap(claim, quote).length >= 2);
85
+ }
86
+ const boundQuery = source.claimBinding?.query || query;
87
+ if (source.claimBinding?.method === 'tight-source-token-pair' && overlap(claim, boundQuery).length >= 2) return true;
88
+ if ((source.symbols || []).some(({ name }) => normalize(claim).includes(normalize(name)))) return true;
89
+ return (source.posture || []).some((posture) => overlap(claim, posture).length >= 2);
90
+ }
91
+
92
+ export function buildSourceClaimReceipt({ claim, groundingReceipt, sourcePath, observedAt = new Date().toISOString() } = {}) {
93
+ if (typeof claim !== 'string' || !claim.trim() || !groundingReceipt || !Array.isArray(groundingReceipt.sources)) {
94
+ throw new Error('source claim receipt requires a claim and grounding receipt');
95
+ }
96
+ const source = groundingReceipt.sources.find((row) => `${row.repo}/${row.path}` === sourcePath);
97
+ if (!source || !HEX64.test(String(source.sha || '')) || source.enforceable !== true) {
98
+ throw new Error('grounding source lacks a full content identity or enforceable evidence');
99
+ }
100
+ if (!sourceBindsClaim(claim, source, groundingReceipt.query)) throw new Error('grounding source does not bind the claim');
101
+ const payload = {
102
+ schemaVersion: 1,
103
+ kind: 'ruvnet-brain-source-claim',
104
+ observedAt,
105
+ claim: claim.trim(),
106
+ claimDigest: digest(normalize(claim)),
107
+ groundingReceiptId: String(groundingReceipt.id || ''),
108
+ groundingReceiptDigest: digest(groundingReceipt),
109
+ source: { repo: source.repo, path: source.path, sha256: source.sha },
110
+ binding: source.claimBinding?.method || ((source.negatives || []).length ? 'explicit-negative' : 'explicit-source-fact'),
111
+ verdict: 'PASS',
112
+ };
113
+ return validateSourceClaimReceipt({ ...payload, receiptSha256: digest(payload) });
114
+ }
115
+
116
+ function livePayload(receipt) {
117
+ const { receiptSha256: _receiptSha256, ...payload } = receipt || {};
118
+ return payload;
119
+ }
120
+
121
+ export function validateLiveSurfaceReceipt(receipt) {
122
+ if (receipt?.schemaVersion !== 1 || receipt?.kind !== 'ruvnet-brain-live-surface'
123
+ || !['claude', 'codex', 'shared'].includes(receipt.host) || !receipt.executable
124
+ || !['current-version', 'latest-version', 'health'].includes(receipt.observationClass)
125
+ || !HEX64.test(String(receipt.outputSha256 || '')) || !HEX64.test(String(receipt.receiptSha256 || ''))
126
+ || digest(livePayload(receipt)) !== receipt.receiptSha256) {
127
+ throw new Error('live surface receipt is malformed or digest-invalid');
128
+ }
129
+ if (receipt.observationClass === 'current-version' && !VERSION.test(String(receipt.observedVersion || ''))) {
130
+ throw new Error('live version receipt has no observed semantic version');
131
+ }
132
+ if (receipt.observationClass === 'latest-version'
133
+ && (receipt.host !== 'shared' || !VERSION.test(String(receipt.observedVersion || ''))
134
+ || !receipt.registryPackage || !/^https:\/\/registry\.npmjs\.org\//.test(String(receipt.registryUrl || '')))) {
135
+ throw new Error('registry latest receipt has no exact public semantic version observation');
136
+ }
137
+ if (receipt.observationClass === 'health' && !['PASS', 'FAIL', 'UNKNOWN'].includes(receipt.healthVerdict)) {
138
+ throw new Error('live health receipt has no typed verdict');
139
+ }
140
+ return receipt;
141
+ }
142
+
143
+ function appendReceipt(file, receipt) {
144
+ try {
145
+ fs.mkdirSync(path.dirname(file), { recursive: true });
146
+ fs.appendFileSync(file, `${JSON.stringify(receipt)}\n`, { mode: 0o600 });
147
+ if (fs.statSync(file).size > MAX_LEDGER_BYTES) {
148
+ const lines = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean).slice(-200);
149
+ const temporary = `${file}.tmp-${process.pid}-${Date.now()}`;
150
+ fs.writeFileSync(temporary, `${lines.join('\n')}\n`, { flag: 'wx', mode: 0o600 });
151
+ fs.renameSync(temporary, file);
152
+ }
153
+ } catch { /* evidence capture cannot break the managed CLI boundary */ }
154
+ }
155
+
156
+ export function recordManagedCliObservation({ toolName, executable, argv, execution, env = process.env,
157
+ observedAt = new Date().toISOString() } = {}) {
158
+ try {
159
+ const output = [execution?.stdout, execution?.stderr].filter(Boolean).join('\n');
160
+ const reachable = execution?.code === 0 && !execution?.error;
161
+ const version = VERSION.exec(output)?.[1] || null;
162
+ let observationClass = null;
163
+ let fields = {};
164
+ if (reachable && version && (toolName === 'ruvnet_cli_help'
165
+ || (argv || []).some((arg) => ['--version', '-V', 'version'].includes(arg)))) {
166
+ observationClass = 'current-version';
167
+ fields = { observedVersion: version };
168
+ } else if (['doctor', 'status'].includes(String(argv?.[0] || ''))) {
169
+ observationClass = 'health';
170
+ const negative = /\b(?:critical|degraded|error|failed|failure|unhealthy)\b/i.test(output);
171
+ const positive = /\b(?:all checks passed|healthy|overall\s*[:=-]?\s*(?:ok|pass)|status\s*[:=-]\s*(?:ok|pass|healthy))\b/i.test(output);
172
+ fields = { healthVerdict: negative ? 'FAIL' : reachable && positive ? 'PASS' : 'UNKNOWN', reachable };
173
+ }
174
+ if (!observationClass) return null;
175
+ const payload = {
176
+ schemaVersion: 1,
177
+ kind: 'ruvnet-brain-live-surface',
178
+ host: HOSTS.includes(env.RUVNET_HOOK_HOST) ? env.RUVNET_HOOK_HOST : 'shared',
179
+ observedAt,
180
+ toolName,
181
+ executable,
182
+ argv: [...(argv || [])],
183
+ observationClass,
184
+ outputSha256: sha256(output),
185
+ ...fields,
186
+ };
187
+ const receipt = validateLiveSurfaceReceipt({ ...payload, receiptSha256: digest(payload) });
188
+ appendReceipt(liveEvidenceFile(env), receipt);
189
+ return receipt;
190
+ } catch { return null; }
191
+ }
192
+
193
+ export function recordRegistryLatestObservation({ executable, packageName, version, registryUrl, responseBody,
194
+ env = process.env, observedAt = new Date().toISOString() } = {}) {
195
+ const matched = VERSION.exec(String(version || ''));
196
+ const normalizedVersion = String(version || '').replace(/^v/, '');
197
+ if (!executable || !packageName || !matched || matched[1] !== normalizedVersion) {
198
+ throw new Error('registry latest receipt requires an exact semantic version');
199
+ }
200
+ const payload = {
201
+ schemaVersion: 1,
202
+ kind: 'ruvnet-brain-live-surface',
203
+ host: 'shared',
204
+ observedAt,
205
+ toolName: 'ruvnet_registry_latest',
206
+ executable,
207
+ argv: [packageName],
208
+ observationClass: 'latest-version',
209
+ outputSha256: sha256(String(responseBody || '')),
210
+ observedVersion: normalizedVersion,
211
+ registryPackage: packageName,
212
+ registryUrl,
213
+ };
214
+ const receipt = validateLiveSurfaceReceipt({ ...payload, receiptSha256: digest(payload) });
215
+ appendReceipt(liveEvidenceFile(env), receipt);
216
+ return receipt;
217
+ }
218
+
219
+ export function readLiveSurfaceReceipts({ file = null, env = process.env, limit = 100 } = {}) {
220
+ try {
221
+ const lines = fs.readFileSync(file || liveEvidenceFile(env), 'utf8').split('\n').filter(Boolean);
222
+ const receipts = [];
223
+ for (let index = lines.length - 1; index >= 0 && receipts.length < limit; index -= 1) {
224
+ try { receipts.push(validateLiveSurfaceReceipt(JSON.parse(lines[index]))); } catch { /* reject torn/tampered rows */ }
225
+ }
226
+ return receipts;
227
+ } catch { return []; }
228
+ }
229
+
230
+ function extractClaims(message) {
231
+ const claims = [];
232
+ for (const sentence of safeMessage(message).split(/(?<=[.!?])\s+|\n+/).map((value) => value.trim()).filter(Boolean)) {
233
+ let match = new RegExp(`\\b(${RUVNET_TOOL})\\b[^.!?]{0,48}\\b(?:current|installed)\\s+version\\s+(?:is\\s+)?v?(\\d+\\.\\d+\\.\\d+(?:[-+][0-9A-Za-z.-]+)?)`, 'i').exec(sentence);
234
+ if (match) { claims.push({ class: 'current-version', text: sentence, tool: match[1], version: match[2] }); continue; }
235
+ match = new RegExp(`\\b(${RUVNET_TOOL})\\b[^.!?]{0,32}\\bv?(\\d+\\.\\d+\\.\\d+(?:[-+][0-9A-Za-z.-]+)?)\\b[^.!?]{0,32}\\blatest\\s+version`, 'i').exec(sentence);
236
+ if (match) { claims.push({ class: 'latest-version', text: sentence, tool: match[1], version: match[2] }); continue; }
237
+ match = new RegExp(`\\b(${RUVNET_TOOL})\\b\\s+is\\s+(healthy|unhealthy|reachable|unreachable|available|down)\\b`, 'i').exec(sentence);
238
+ if (match) { claims.push({ class: 'health', text: sentence, tool: match[1], state: match[2].toLowerCase() }); continue; }
239
+ match = new RegExp(`\\b(${RUVNET_TOOL})\\b\\s+(?:can(?:not|'t)?|supports?|does\\s+not\\s+support|provides?|does\\s+not\\s+provide|implements?|does\\s+not\\s+implement|exports?|does\\s+not\\s+export|exposes?|does\\s+not\\s+expose|requires?|uses?)\\s+([^.!?]{1,160})`, 'i').exec(sentence);
240
+ if (match) claims.push({ class: 'behavior', text: sentence, tool: match[1] });
241
+ }
242
+ return claims;
243
+ }
244
+
245
+ export function deriveSourceClaimReceipts(message, { file = null, env = process.env,
246
+ now = new Date().toISOString(), maxAgeMs = 24 * 60 * 60_000 } = {}) {
247
+ const behaviors = extractClaims(message).filter(({ class: claimClass }) => claimClass === 'behavior');
248
+ if (!behaviors.length) return [];
249
+ let lines;
250
+ try { lines = fs.readFileSync(file || groundingEvidenceFile(env), 'utf8').split('\n').filter(Boolean); }
251
+ catch { return []; }
252
+ const currentTime = Date.parse(now);
253
+ const grounding = [];
254
+ for (let index = lines.length - 1; index >= 0 && grounding.length < 60; index -= 1) {
255
+ try {
256
+ const receipt = JSON.parse(lines[index]);
257
+ const age = currentTime - Date.parse(receipt.ts);
258
+ if (receipt?.v === 1 && Array.isArray(receipt.sources) && Number.isFinite(age) && age >= 0 && age <= maxAgeMs) {
259
+ grounding.push(receipt);
260
+ }
261
+ } catch { /* torn or malformed evidence cannot authorize a claim */ }
262
+ }
263
+ const sourceClaims = [];
264
+ for (const claim of behaviors) {
265
+ let bound = null;
266
+ for (const receipt of grounding) {
267
+ for (const source of receipt.sources || []) {
268
+ try {
269
+ bound = buildSourceClaimReceipt({ claim: claim.text, groundingReceipt: receipt,
270
+ sourcePath: `${source.repo}/${source.path}`, observedAt: now });
271
+ } catch { /* this source does not bind this exact claim */ }
272
+ if (bound) break;
273
+ }
274
+ if (bound) break;
275
+ }
276
+ if (bound) sourceClaims.push(bound);
277
+ }
278
+ return sourceClaims;
279
+ }
280
+
281
+ export function auditCurrentCapabilityEvidence(message, { env = process.env,
282
+ now = new Date().toISOString() } = {}) {
283
+ return auditEvidenceBoundCapabilityClaims(message, {
284
+ sourceClaims: deriveSourceClaimReceipts(message, { env, now }),
285
+ liveSurfaces: readLiveSurfaceReceipts({ env }),
286
+ host: HOSTS.includes(env.RUVNET_HOOK_HOST) ? env.RUVNET_HOOK_HOST : null,
287
+ now,
288
+ });
289
+ }
290
+
291
+ export function auditEvidenceBoundCapabilityClaims(message, {
292
+ sourceClaims = [], liveSurfaces = [], host = null,
293
+ now = new Date().toISOString(), maxAgeMs = MAX_AGE_MS,
294
+ } = {}) {
295
+ const claims = extractClaims(message);
296
+ const contradictions = [];
297
+ const unresolved = [];
298
+ const passed = [];
299
+ const currentTime = Date.parse(now);
300
+ const live = liveSurfaces.flatMap((receipt) => {
301
+ try {
302
+ validateLiveSurfaceReceipt(receipt);
303
+ const age = currentTime - Date.parse(receipt.observedAt);
304
+ const hostMatches = !host || receipt.host === 'shared' || receipt.host === host;
305
+ return hostMatches && Number.isFinite(age) && age >= 0 && age <= maxAgeMs ? [receipt] : [];
306
+ } catch { return []; }
307
+ });
308
+ const sources = sourceClaims.flatMap((receipt) => { try { return [validateSourceClaimReceipt(receipt)]; } catch { return []; } });
309
+ for (const claim of claims) {
310
+ if (claim.class === 'behavior') {
311
+ const match = sources.find((receipt) => receipt.claimDigest === digest(normalize(claim.text)));
312
+ (match ? passed : unresolved).push({ ...claim, evidence: match?.receiptSha256,
313
+ reason: match ? 'exact behavior claim is source-bound' : 'behavior claim has no exact source receipt' });
314
+ continue;
315
+ }
316
+ const executable = executableFor(claim.tool);
317
+ const candidates = live.filter((receipt) => receipt.executable === executable);
318
+ if (claim.class === 'current-version') {
319
+ const observed = candidates.find((receipt) => receipt.observationClass === 'current-version');
320
+ if (!observed) unresolved.push({ ...claim, reason: 'current version has no fresh live receipt' });
321
+ else if (observed.observedVersion !== claim.version) contradictions.push({ ...claim,
322
+ reason: `live current version is ${observed.observedVersion}`, evidence: observed.receiptSha256 });
323
+ else passed.push({ ...claim, evidence: observed.receiptSha256, reason: 'current version matches live receipt' });
324
+ } else if (claim.class === 'latest-version') {
325
+ const observed = candidates.find((receipt) => receipt.observationClass === 'latest-version');
326
+ if (!observed) unresolved.push({ ...claim, reason: 'latest version has no fresh registry receipt' });
327
+ else if (observed.observedVersion !== claim.version) contradictions.push({ ...claim,
328
+ reason: `public registry latest version is ${observed.observedVersion}`, evidence: observed.receiptSha256 });
329
+ else passed.push({ ...claim, evidence: observed.receiptSha256, reason: 'latest version matches registry receipt' });
330
+ } else {
331
+ const observed = candidates.find((receipt) => receipt.observationClass === 'health');
332
+ let state = 'UNKNOWN';
333
+ if (observed && claim.state === 'healthy') state = observed.healthVerdict === 'PASS' ? 'PASS'
334
+ : observed.healthVerdict === 'FAIL' ? 'FAIL' : 'UNKNOWN';
335
+ else if (observed && claim.state === 'unhealthy') state = observed.healthVerdict === 'FAIL' ? 'PASS'
336
+ : observed.healthVerdict === 'PASS' ? 'FAIL' : 'UNKNOWN';
337
+ else if (observed && ['reachable', 'available'].includes(claim.state)) state = observed.reachable === true ? 'PASS' : 'FAIL';
338
+ else if (observed && ['unreachable', 'down'].includes(claim.state)) state = observed.reachable === false ? 'PASS' : 'FAIL';
339
+ if (state === 'PASS') passed.push({ ...claim, evidence: observed.receiptSha256 });
340
+ else if (state === 'FAIL') contradictions.push({ ...claim, evidence: observed.receiptSha256,
341
+ reason: 'fresh live health receipt contradicts the claim' });
342
+ else unresolved.push({ ...claim, reason: 'health or reachability claim has no conclusive fresh live receipt' });
343
+ }
344
+ }
345
+ return { schemaVersion: 1, kind: 'ruvnet-brain-evidence-bound-capability-audit', claims, passed,
346
+ contradictions, unresolved, verdict: contradictions.length ? 'FAIL' : unresolved.length ? 'UNKNOWN' : 'PASS' };
347
+ }
348
+
349
+ function aggregatePayload(aggregate) {
350
+ const { aggregateSha256: _aggregateSha256, signature: _signature, ...payload } = aggregate || {};
351
+ return payload;
352
+ }
353
+
354
+ function validateAggregatePayload(payload) {
355
+ if (payload?.schemaVersion !== 1 || payload?.kind !== 'ruvnet-brain-capability-claim-aggregate'
356
+ || !HEX40.test(String(payload.identity?.sourceSha || '')) || !HEX64.test(String(payload.identity?.artifactSha256 || ''))
357
+ || !Array.isArray(payload.lanes) || !Array.isArray(payload.untested)
358
+ || !Array.isArray(payload.sourceClaimDigests) || !Array.isArray(payload.liveSurfaceDigests)
359
+ || payload.sourceClaimDigests.some((value) => !HEX64.test(String(value)))
360
+ || payload.liveSurfaceDigests.some((value) => !HEX64.test(String(value)))) {
361
+ throw new Error('capability claim aggregate payload is malformed');
362
+ }
363
+ const laneIds = new Set();
364
+ for (const lane of payload.lanes) {
365
+ const id = `${lane.os}/${lane.host}`;
366
+ const claims = lane.claims || {};
367
+ const claimVerdicts = CLAIM_CLASSES.map((name) => claims[name]);
368
+ const derivedVerdict = claimVerdicts.includes('FAIL') ? 'FAIL'
369
+ : claimVerdicts.includes('UNKNOWN') ? 'PARTIAL' : 'PASS';
370
+ if (!OS_LANES.includes(lane.os) || !HOSTS.includes(lane.host) || laneIds.has(id)
371
+ || claimVerdicts.some((verdict) => !['PASS', 'FAIL', 'UNKNOWN'].includes(verdict))
372
+ || lane.verdict !== derivedVerdict
373
+ || !HEX64.test(String(lane.receiptSha256 || ''))) throw new Error('capability claim aggregate lane is incomplete');
374
+ laneIds.add(id);
375
+ }
376
+ const os = [...new Set(payload.lanes.map(({ os: laneOs }) => laneOs))].sort();
377
+ const hosts = [...new Set(payload.lanes.map(({ host }) => host))].sort();
378
+ if (canonicalJson(os) !== canonicalJson(payload.os) || canonicalJson(hosts) !== canonicalJson(payload.hosts)) {
379
+ throw new Error('capability claim aggregate lane projection differs');
380
+ }
381
+ const complete = OS_LANES.every((laneOs) => HOSTS.every((host) => laneIds.has(`${laneOs}/${host}`)));
382
+ const laneVerdicts = payload.lanes.map(({ verdict }) => verdict);
383
+ const derivedVerdict = laneVerdicts.includes('FAIL') ? 'FAIL'
384
+ : !complete || payload.untested.length || laneVerdicts.includes('PARTIAL') ? 'PARTIAL' : 'PASS';
385
+ if (payload.verdict !== derivedVerdict) throw new Error('capability claim aggregate verdict differs from its lanes');
386
+ if (payload.verdict === 'PASS' && (!complete || payload.untested.length)) {
387
+ throw new Error('capability claim PASS lacks the complete OS and host matrix');
388
+ }
389
+ if (payload.verdict === 'PARTIAL' && !payload.untested.length && !laneVerdicts.includes('PARTIAL')) {
390
+ throw new Error('capability claim PARTIAL has no disclosed incomplete scope');
391
+ }
392
+ return payload;
393
+ }
394
+
395
+ export function signCapabilityClaimAggregate(input, privateKey) {
396
+ const lanes = [...(input.lanes || [])].sort((a, b) => `${a.os}/${a.host}`.localeCompare(`${b.os}/${b.host}`));
397
+ const complete = OS_LANES.every((laneOs) => HOSTS.every((host) =>
398
+ lanes.some((lane) => lane.os === laneOs && lane.host === host)));
399
+ const laneVerdicts = lanes.map(({ verdict }) => verdict);
400
+ const untested = [...new Set(input.untested || [])].sort();
401
+ const verdict = laneVerdicts.includes('FAIL') ? 'FAIL'
402
+ : !complete || untested.length || laneVerdicts.includes('PARTIAL') ? 'PARTIAL' : 'PASS';
403
+ const payload = validateAggregatePayload({
404
+ schemaVersion: 1, kind: 'ruvnet-brain-capability-claim-aggregate', identity: input.identity,
405
+ os: [...new Set(lanes.map(({ os: laneOs }) => laneOs))].sort(),
406
+ hosts: [...new Set(lanes.map(({ host }) => host))].sort(), lanes,
407
+ sourceClaimDigests: [...new Set(input.sourceClaimDigests || [])].sort(),
408
+ liveSurfaceDigests: [...new Set(input.liveSurfaceDigests || [])].sort(),
409
+ verdict,
410
+ untested,
411
+ });
412
+ const aggregateSha256 = digest(payload);
413
+ const signed = { ...payload, aggregateSha256 };
414
+ return { ...signed, signature: crypto.sign(null, Buffer.from(canonicalJson(signed)), privateKey).toString('base64') };
415
+ }
416
+
417
+ export function verifyCapabilityClaimAggregate(aggregate, publicKey, expectedIdentity = null) {
418
+ if (!HEX64.test(String(aggregate?.aggregateSha256 || '')) || typeof aggregate?.signature !== 'string') {
419
+ throw new Error('capability claim aggregate signature envelope is malformed');
420
+ }
421
+ const payload = validateAggregatePayload(aggregatePayload(aggregate));
422
+ if (digest(payload) !== aggregate.aggregateSha256) throw new Error('capability claim aggregate digest mismatch');
423
+ const signed = { ...payload, aggregateSha256: aggregate.aggregateSha256 };
424
+ if (!crypto.verify(null, Buffer.from(canonicalJson(signed)), publicKey, Buffer.from(aggregate.signature, 'base64'))) {
425
+ throw new Error('capability claim aggregate signature mismatch');
426
+ }
427
+ if (expectedIdentity && canonicalJson(expectedIdentity) !== canonicalJson(aggregate.identity)) {
428
+ throw new Error('capability claim aggregate identity differs');
429
+ }
430
+ return aggregate;
431
+ }