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
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env node
2
+ import crypto from 'node:crypto';
3
+ import path from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { canonicalJson, digest } from './coverage-integrity.mjs';
6
+
7
+ const TEXT_EXT = new Set(['.md', '.markdown', '.txt', '.rst']);
8
+ const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
9
+ const HEX40 = /^[a-f0-9]{40}$/;
10
+ const HEX64 = /^[a-f0-9]{64}$/;
11
+ const compareCanonicalText = (left, right) => String(left) < String(right) ? -1
12
+ : String(left) > String(right) ? 1 : 0;
13
+
14
+ const withoutReceiptDigest = ({ receiptSha256: _receiptSha256, ...payload }) => payload;
15
+
16
+ function validDate(value) {
17
+ return typeof value === 'string' && Number.isFinite(Date.parse(value));
18
+ }
19
+
20
+ export function sealGistReceipt(receipt) {
21
+ const files = [...(receipt.files || [])].sort((a, b) => compareCanonicalText(a.filename, b.filename));
22
+ const payload = {
23
+ schemaVersion: 3,
24
+ kind: 'ruvnet-brain-gist-source-receipt',
25
+ gistId: String(receipt.gistId || ''),
26
+ versionSha: receipt.versionSha,
27
+ updatedAt: receipt.updatedAt,
28
+ ingestedAt: receipt.ingestedAt,
29
+ fileCount: files.length,
30
+ files,
31
+ contentDigest: digest(files),
32
+ complete: receipt.complete === true,
33
+ };
34
+ return { ...payload, receiptSha256: digest(payload) };
35
+ }
36
+
37
+ export function sealGistReceiptSet(receipt) {
38
+ const ids = Object.keys(receipt.gists || {}).sort();
39
+ const gists = Object.fromEntries(ids.map((id) => [id, receipt.gists[id]]));
40
+ const sourceSet = ids.map((id) => ({ id, receiptSha256: gists[id]?.receiptSha256 }));
41
+ const payload = {
42
+ schemaVersion: 3,
43
+ kind: 'ruvnet-brain-gist-source-receipts',
44
+ owner: receipt.owner,
45
+ generated: receipt.generated,
46
+ observedAt: receipt.observedAt,
47
+ sourceObservationSha256: receipt.sourceObservationSha256,
48
+ gistSet: {
49
+ count: ids.length,
50
+ idsSha256: digest(ids),
51
+ observationSha256: receipt.sourceObservationSha256,
52
+ },
53
+ sourceSetSha256: digest(sourceSet),
54
+ passagesSha256: receipt.passagesSha256 ?? null,
55
+ gists,
56
+ };
57
+ return { ...payload, receiptSha256: digest(payload) };
58
+ }
59
+
60
+ function defaultFetchGist(id) {
61
+ const result = spawnSync('gh', ['api', `gists/${id}`], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
62
+ if (result.status !== 0) throw new Error(`gist ${id} fetch failed: ${String(result.stderr || '').trim()}`);
63
+ return JSON.parse(result.stdout);
64
+ }
65
+
66
+ async function defaultFetchBody(file) {
67
+ if (!file.truncated) return Buffer.from(file.content || '', 'utf8');
68
+ const response = await fetch(file.raw_url, { headers: { 'user-agent': 'ruvnet-brain-gist-receipts' } });
69
+ if (!response.ok) throw new Error(`raw gist fetch failed: HTTP ${response.status}`);
70
+ return Buffer.from(await response.arrayBuffer());
71
+ }
72
+
73
+ function listedFileIdentity(gist) {
74
+ return Object.entries(gist?.files || {}).sort(([a], [b]) => compareCanonicalText(a, b)).map(([key, file]) => ({
75
+ key, filename: file?.filename, rawUrl: file?.raw_url, size: file?.size, type: file?.type,
76
+ language: file?.language ?? null,
77
+ }));
78
+ }
79
+
80
+ function exactFileIdentity(file) {
81
+ if (!file || typeof file.filename !== 'string' || !file.filename) return false;
82
+ const keys = Object.keys(file).sort();
83
+ if (file.included === true) {
84
+ return canonicalJson(keys) === canonicalJson(['bytes', 'filename', 'included', 'sha256'])
85
+ && HEX64.test(String(file.sha256 || '')) && Number.isSafeInteger(file.bytes) && file.bytes >= 0;
86
+ }
87
+ return file.included === false
88
+ && canonicalJson(keys) === canonicalJson(['filename', 'included', 'reason', 'size'])
89
+ && typeof file.reason === 'string' && Boolean(file.reason)
90
+ && (file.size === null || (Number.isSafeInteger(file.size) && file.size >= 0));
91
+ }
92
+
93
+ function reusableReceipt(row, { gistId, updatedAt }) {
94
+ if (row?.schemaVersion !== 3 || row?.kind !== 'ruvnet-brain-gist-source-receipt'
95
+ || row.gistId !== gistId || row.complete !== true || row.updatedAt !== updatedAt
96
+ || !validDate(row.ingestedAt)
97
+ || !Array.isArray(row.files) || !row.files.length || row.fileCount !== row.files.length
98
+ || row.files.some((file) => !exactFileIdentity(file))) return false;
99
+ const filenames = row.files.map(({ filename }) => filename);
100
+ return new Set(filenames).size === filenames.length
101
+ && canonicalJson(filenames) === canonicalJson([...filenames].sort(compareCanonicalText))
102
+ && canonicalJson(row) === canonicalJson(sealGistReceipt(row));
103
+ }
104
+
105
+ export function validateGistReceiptSet(receipt, observation) {
106
+ const ids = observation?.gists?.rows?.map(({ id }) => String(id)).sort() || [];
107
+ const received = Object.keys(receipt?.gists || {}).sort();
108
+ if (receipt?.schemaVersion !== 3 || receipt?.kind !== 'ruvnet-brain-gist-source-receipts'
109
+ || receipt.owner !== observation?.owner || !validDate(receipt.generated)
110
+ || !validDate(receipt.observedAt)
111
+ || canonicalJson(ids) !== canonicalJson(received)
112
+ || receipt.gistSet?.count !== ids.length || receipt.gistSet?.idsSha256 !== digest(ids)
113
+ || receipt.gistSet?.observationSha256 !== observation?.observationSha256
114
+ || receipt.sourceObservationSha256 !== observation?.observationSha256) {
115
+ throw new Error('gist receipt set does not exactly match the sealed source observation');
116
+ }
117
+ for (const id of ids) {
118
+ const row = receipt.gists[id];
119
+ const stub = observation.gists.rows.find((gist) => String(gist.id) === id);
120
+ if (row?.schemaVersion !== 3 || row?.kind !== 'ruvnet-brain-gist-source-receipt'
121
+ || row?.gistId !== id || row?.complete !== true || row.updatedAt !== stub?.updated_at
122
+ || !HEX40.test(String(row.versionSha || ''))
123
+ || !validDate(row.updatedAt) || !validDate(row.ingestedAt)
124
+ || !Array.isArray(row.files) || row.fileCount !== row.files.length || row.files.length === 0
125
+ || row.files.some((file) => !exactFileIdentity(file))) throw new Error(`gist ${id} receipt is incomplete`);
126
+ const filenames = row.files.map(({ filename }) => filename);
127
+ if (filenames.some((filename) => typeof filename !== 'string' || !filename)
128
+ || new Set(filenames).size !== filenames.length
129
+ || canonicalJson(filenames) !== canonicalJson([...filenames].sort(compareCanonicalText))) {
130
+ throw new Error(`gist ${id} file inventory is not unique and sorted`);
131
+ }
132
+ if (row.contentDigest !== digest(row.files)) throw new Error(`gist ${id} content digest differs`);
133
+ if (row.receiptSha256 !== digest(withoutReceiptDigest(row))) throw new Error(`gist ${id} receipt digest differs`);
134
+ }
135
+ const sourceSet = ids.map((id) => ({ id, receiptSha256: receipt.gists[id].receiptSha256 }));
136
+ if (receipt.sourceSetSha256 !== digest(sourceSet)) throw new Error('gist source set digest differs');
137
+ if (!(receipt.passagesSha256 === null || HEX64.test(String(receipt.passagesSha256 || '')))) {
138
+ throw new Error('gist passage digest is malformed');
139
+ }
140
+ if (receipt.receiptSha256 !== digest(withoutReceiptDigest(receipt))) throw new Error('gist receipt set digest differs');
141
+ return receipt;
142
+ }
143
+
144
+ export async function reconcileGistReceipts({ observation, existing = null, fetchGist = defaultFetchGist,
145
+ fetchBody = defaultFetchBody, now = () => new Date().toISOString() } = {}) {
146
+ const stubs = observation?.gists?.rows;
147
+ if (!Array.isArray(stubs) || stubs.some(({ id }) => !/^[a-f0-9]{20,64}$/.test(String(id || '')))
148
+ || new Set(stubs.map(({ id }) => id)).size !== stubs.length) {
149
+ throw new Error('source observation has missing or duplicate gist ids');
150
+ }
151
+ const gists = {};
152
+ for (const stub of [...stubs].sort((a, b) => String(a.id).localeCompare(String(b.id)))) {
153
+ const prior = existing?.gists?.[stub.id];
154
+ const reusable = reusableReceipt(prior, { gistId: stub.id, updatedAt: stub.updated_at });
155
+ if (reusable) { gists[stub.id] = prior; continue; }
156
+ const full = await fetchGist(stub.id);
157
+ const versionSha = full?.history?.[0]?.version;
158
+ if (full?.id !== stub.id || full.updated_at !== stub.updated_at || !HEX40.test(String(versionSha || ''))
159
+ || canonicalJson(listedFileIdentity(full)) !== canonicalJson(listedFileIdentity(stub))) {
160
+ const error = new Error(`gist ${stub.id} individual identity differs from the list observation`);
161
+ error.code = 'GIST_OBSERVATION_MOVED';
162
+ error.gistId = stub.id;
163
+ throw error;
164
+ }
165
+ const files = [];
166
+ for (const [filename, file] of Object.entries(full.files || {}).sort(([a], [b]) => a.localeCompare(b))) {
167
+ if (!TEXT_EXT.has(path.extname(filename).toLowerCase())) {
168
+ files.push({ filename, included: false, reason: 'non-text policy exclusion', size: file.size ?? null });
169
+ continue;
170
+ }
171
+ const fetched = await fetchBody(file);
172
+ const body = Buffer.isBuffer(fetched) ? fetched : Buffer.from(String(fetched), 'utf8');
173
+ try { new TextDecoder('utf-8', { fatal: true }).decode(body); }
174
+ catch { throw new Error(`gist ${stub.id}/${filename} is not valid UTF-8 text`); }
175
+ files.push({ filename, included: true, sha256: sha256(body), bytes: body.length });
176
+ }
177
+ gists[stub.id] = sealGistReceipt({ gistId: stub.id, versionSha, updatedAt: full.updated_at,
178
+ ingestedAt: now(), files, complete: files.length === Object.keys(full.files || {}).length });
179
+ }
180
+ return validateGistReceiptSet(sealGistReceiptSet({ owner: observation.owner, generated: now(),
181
+ observedAt: observation.observedAt, sourceObservationSha256: observation.observationSha256,
182
+ passagesSha256: null, gists }), observation);
183
+ }
@@ -93,6 +93,27 @@ if [ "$status" -ne 0 ]; then
93
93
  exit 1
94
94
  fi
95
95
 
96
+ # Generated public census and command aliases have one source of truth each. Both writers have a
97
+ # `--check` mode specifically for CI/pre-push; until 2026-08-22 nothing invoked either check, so the
98
+ # writers were built and tested but their promised gate did not exist.
99
+ node "$ROOT/scripts/sync-census.mjs" --check
100
+ census_status=$?
101
+ if [ "$census_status" -ne 0 ]; then
102
+ echo ""
103
+ echo " ⛔ pre-push gate refused this push — public census claims have drifted."
104
+ echo " Run: node scripts/sync-census.mjs"
105
+ exit 1
106
+ fi
107
+
108
+ node "$ROOT/scripts/sync-commands.mjs" --check
109
+ commands_status=$?
110
+ if [ "$commands_status" -ne 0 ]; then
111
+ echo ""
112
+ echo " ⛔ pre-push gate refused this push — command aliases no longer share one body."
113
+ echo " Run: node scripts/sync-commands.mjs"
114
+ exit 1
115
+ fi
116
+
96
117
  # ── DOCUMENT CURRENCY (ADR-056 §5) ─────────────────────────────────────────────────────────────────
97
118
  # The tool was written and tested on 2026-07-22 and then invoked by NOTHING for five days — absent
98
119
  # from this hook, gate.sh, gates.mjs and every workflow, while `wired-check` reported it "wired"
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { canonicalJson, digest } from './coverage-integrity.mjs';
7
+ import { HOST_MODES, MODE_HOSTS } from './host-install-matrix.mjs';
8
+
9
+ export const HOST_ADAPTER_FILES = Object.freeze([
10
+ 'plugin/host-adapters/claude.json',
11
+ 'plugin/host-adapters/codex.json',
12
+ ]);
13
+ export const HOST_OPERATING_SYSTEMS = Object.freeze(['linux', 'macos', 'windows']);
14
+
15
+ const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
16
+ const ordered = (values) => [...values].sort((a, b) => a.localeCompare(b));
17
+ const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
18
+
19
+ function trackedFile(root, relative, label) {
20
+ if (typeof relative !== 'string' || !relative || path.isAbsolute(relative)
21
+ || relative.split(/[\\/]/).includes('..')) throw new Error(`${label} is not a safe tracked file`);
22
+ const absolute = path.resolve(root, relative);
23
+ let stat;
24
+ try { stat = fs.lstatSync(absolute); } catch { throw new Error(`${label} is not a tracked file: ${relative}`); }
25
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`${label} is not a tracked file: ${relative}`);
26
+ // Windows may expose the same temp path through 8.3 and long-name aliases. Bind receipts to
27
+ // the filesystem's canonical spelling so cross-host verification compares identity, not alias.
28
+ return fs.realpathSync(absolute);
29
+ }
30
+
31
+ function expectedModes(id) {
32
+ return HOST_MODES.filter((mode) => MODE_HOSTS[mode].includes(id));
33
+ }
34
+
35
+ function readAdapter(root, descriptorFile) {
36
+ const absolute = trackedFile(root, descriptorFile, 'host adapter descriptor');
37
+ const bytes = fs.readFileSync(absolute);
38
+ let value;
39
+ try { value = JSON.parse(bytes); } catch (error) { throw new Error(`host adapter descriptor is invalid JSON: ${error.message}`); }
40
+ const expectedKeys = ['displayName', 'hooks', 'id', 'kind', 'loader', 'manifest', 'modes', 'os', 'schemaVersion'];
41
+ if (canonicalJson(Object.keys(value).sort()) !== canonicalJson(expectedKeys)
42
+ || value.schemaVersion !== 1 || value.kind !== 'ruvnet-brain-host-adapter'
43
+ || !['claude', 'codex'].includes(value.id) || typeof value.displayName !== 'string' || !value.displayName
44
+ || canonicalJson(value.os) !== canonicalJson(HOST_OPERATING_SYSTEMS)
45
+ || canonicalJson(value.modes) !== canonicalJson(expectedModes(value.id))) {
46
+ throw new Error(`${descriptorFile} host adapter identity, OS, or mode ownership is invalid`);
47
+ }
48
+ for (const field of ['manifest', 'hooks', 'loader']) trackedFile(root, value[field], `${value.id} ${field}`);
49
+ return { ...value, descriptorFile, descriptorSha256: sha256(bytes) };
50
+ }
51
+
52
+ function assemble(root) {
53
+ const adapters = HOST_ADAPTER_FILES.map((file) => readAdapter(root, file))
54
+ .sort((a, b) => a.id.localeCompare(b.id));
55
+ const payload = {
56
+ schemaVersion: 1,
57
+ kind: 'ruvnet-brain-host-registry',
58
+ os: [...HOST_OPERATING_SYSTEMS],
59
+ modes: [...HOST_MODES],
60
+ adapters,
61
+ };
62
+ return { ...payload, registrySha256: digest(payload) };
63
+ }
64
+
65
+ export function validateHostRegistry(registry, { root = ROOT } = {}) {
66
+ if (registry?.schemaVersion !== 1 || registry?.kind !== 'ruvnet-brain-host-registry') {
67
+ throw new Error('host registry identity is invalid');
68
+ }
69
+ if (canonicalJson(registry.os) !== canonicalJson(HOST_OPERATING_SYSTEMS)) {
70
+ throw new Error('host registry operating systems are incomplete');
71
+ }
72
+ if (canonicalJson(registry.modes) !== canonicalJson(HOST_MODES)) {
73
+ throw new Error('host registry modes are incomplete');
74
+ }
75
+ if (!Array.isArray(registry.adapters)
76
+ || canonicalJson(registry.adapters.map(({ id }) => id)) !== canonicalJson(['claude', 'codex'])) {
77
+ throw new Error('host registry adapter set is incomplete');
78
+ }
79
+ for (const adapter of registry.adapters) {
80
+ if (canonicalJson(adapter.modes) !== canonicalJson(expectedModes(adapter.id))) {
81
+ throw new Error(`${adapter.id} host mode ownership is invalid`);
82
+ }
83
+ for (const field of ['manifest', 'hooks', 'loader']) trackedFile(root, adapter[field], `${adapter.id} ${field}`);
84
+ }
85
+ const expected = assemble(root);
86
+ const { registrySha256: _registrySha256, ...payload } = registry;
87
+ if (registry.registrySha256 !== digest(payload)) throw new Error('host registry digest mismatch');
88
+ if (canonicalJson(registry) !== canonicalJson(expected)) throw new Error('host registry differs from tracked descriptors');
89
+ return registry;
90
+ }
91
+
92
+ export function buildHostRegistry({ root = ROOT } = {}) {
93
+ return validateHostRegistry(assemble(path.resolve(root)), { root: path.resolve(root) });
94
+ }
95
+
96
+ function argument(args, name) {
97
+ const index = args.indexOf(name);
98
+ return index >= 0 ? args[index + 1] : null;
99
+ }
100
+
101
+ export function main(args = process.argv.slice(2)) {
102
+ try {
103
+ const out = argument(args, '--out');
104
+ if (!out) throw new Error('--out is required');
105
+ const output = path.resolve(out);
106
+ if (fs.existsSync(output)) throw new Error(`refusing to overwrite existing host registry: ${output}`);
107
+ const registry = buildHostRegistry({ root: process.cwd() });
108
+ fs.mkdirSync(path.dirname(output), { recursive: true });
109
+ fs.writeFileSync(output, `${JSON.stringify(registry, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
110
+ process.stdout.write(`${JSON.stringify({ ok: true, registrySha256: registry.registrySha256 })}\n`);
111
+ return 0;
112
+ } catch (error) {
113
+ console.error(`host-registry: ${error.message}`);
114
+ return 1;
115
+ }
116
+ }
117
+
118
+ if (path.resolve(process.argv[1] || '') === fileURLToPath(import.meta.url)) process.exitCode = main();