ruvnet-brain 4.0.5 → 4.0.7

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 (38) hide show
  1. package/README.md +3 -3
  2. package/bin/install.mjs +112 -7
  3. package/config/model-router/catalog.template.json +17 -2
  4. package/console/app.js +32 -14
  5. package/console/architecture.html +1 -1
  6. package/data/model-catalog.json +54 -0
  7. package/package.json +2 -1
  8. package/plugin/.claude-plugin/plugin.json +1 -1
  9. package/plugin/.codex-plugin/plugin.json +1 -1
  10. package/plugin/hooks/codex-hooks.json +17 -17
  11. package/plugin/hooks/hooks.json +34 -1
  12. package/plugin/scripts/codex-hook-wrapper.mjs +36 -4
  13. package/plugin/scripts/ground-ruvnet.sh +2 -1
  14. package/plugin/scripts/hook-shim.mjs +13 -6
  15. package/plugin/scripts/lesson-provenance.mjs +21 -0
  16. package/plugin/scripts/lesson-store.mjs +33 -3
  17. package/plugin/scripts/route-dispatch.sh +23 -49
  18. package/plugin/scripts/session-snapshot-contract.mjs +100 -0
  19. package/plugin/scripts/session-snapshot-hook.mjs +34 -0
  20. package/plugin/scripts/session-start-core.mjs +25 -7
  21. package/plugin/scripts/swarm-slot-recycler.mjs +89 -0
  22. package/plugin/skills/release-proof/SKILL.md +4 -4
  23. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +19 -4
  24. package/scripts/capability-registry.mjs +3 -3
  25. package/scripts/lesson-lifecycle.mjs +2 -1
  26. package/scripts/lesson-ratify.mjs +6 -3
  27. package/scripts/lesson-seed.mjs +21 -16
  28. package/scripts/model-router-catalog.mjs +16 -0
  29. package/scripts/model-router-engine.mjs +4 -1
  30. package/scripts/onboarding-console.mjs +52 -14
  31. package/scripts/provider-availability.mjs +12 -0
  32. package/scripts/release-authority.mjs +14 -2
  33. package/scripts/release-transaction-provider.mjs +236 -0
  34. package/scripts/release-transaction.mjs +214 -0
  35. package/scripts/release.mjs +67 -279
  36. package/scripts/session-snapshot-contract.mjs +1 -0
  37. package/scripts/stabilization-receipt.mjs +1 -1
  38. package/scripts/staged-host-verifier.mjs +106 -0
@@ -0,0 +1,214 @@
1
+ #!/usr/bin/env node
2
+ import crypto from 'node:crypto';
3
+
4
+ export const RECEIPT_PREFIX = 'release-transaction-';
5
+ export const TERMINAL_STATES = new Set(['channels-converged', 'aborted']);
6
+
7
+ const canonical = (value) => {
8
+ if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
9
+ if (value && typeof value === 'object') {
10
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`;
11
+ }
12
+ return JSON.stringify(value);
13
+ };
14
+
15
+ export const receiptPayload = (receipt) => {
16
+ const { signature: _signature, receiptDigest: _digest, ...payload } = receipt;
17
+ return canonical(payload);
18
+ };
19
+
20
+ export const digestReceipt = (receipt) => crypto.createHash('sha256')
21
+ .update(receiptPayload(receipt)).digest('hex');
22
+
23
+ export const transactionIdFor = (identity) => crypto.createHash('sha256').update(canonical({
24
+ repository: identity.repository,
25
+ package: identity.package,
26
+ version: identity.version,
27
+ tag: identity.tag,
28
+ candidateSha: identity.candidateSha,
29
+ packageIntegrity: identity.packageIntegrity,
30
+ bundleSha256: identity.bundleSha256,
31
+ })).digest('hex');
32
+
33
+ export function signReceipt(receipt, privateKey) {
34
+ const unsigned = { ...receipt, receiptDigest: digestReceipt(receipt) };
35
+ return {
36
+ ...unsigned,
37
+ signature: crypto.sign(null, Buffer.from(canonical(unsigned)), privateKey).toString('base64'),
38
+ };
39
+ }
40
+
41
+ export function verifyReceipt(receipt, publicKey) {
42
+ if (!receipt || receipt.schemaVersion !== 1 || !/^[a-f0-9]{64}$/.test(receipt.transactionId || '')) {
43
+ throw new Error('invalid release transaction receipt');
44
+ }
45
+ const { signature, receiptDigest, ...unsigned } = receipt;
46
+ if (digestReceipt(unsigned) !== receiptDigest) throw new Error('release receipt digest mismatch');
47
+ if (!crypto.verify(null, Buffer.from(canonical({ ...unsigned, receiptDigest })), publicKey, Buffer.from(signature || '', 'base64'))) {
48
+ throw new Error('release receipt signature mismatch');
49
+ }
50
+ return receipt;
51
+ }
52
+
53
+ export function validateReceiptChain(receipts, identity, publicKey) {
54
+ const expectedId = transactionIdFor(identity);
55
+ const sorted = [...receipts].sort((a, b) => a.sequence - b.sequence);
56
+ let previous = null;
57
+ for (let index = 0; index < sorted.length; index += 1) {
58
+ const receipt = verifyReceipt(sorted[index], publicKey);
59
+ if (receipt.transactionId !== expectedId || canonical(receipt.identity) !== canonical(identity)) {
60
+ throw new Error('release receipt identity conflict');
61
+ }
62
+ if (receipt.sequence !== index) throw new Error('release receipt sequence gap or replay');
63
+ if ((receipt.previousReceiptDigest || null) !== (previous?.receiptDigest || null)) {
64
+ throw new Error('release receipt chain conflict');
65
+ }
66
+ previous = receipt;
67
+ }
68
+ return sorted;
69
+ }
70
+
71
+ const stateReceipt = ({ identity, prior, state, fence, observation = {}, privateKey }) => signReceipt({
72
+ schemaVersion: 1,
73
+ transactionId: transactionIdFor(identity),
74
+ sequence: prior ? prior.sequence + 1 : 0,
75
+ previousReceiptDigest: prior?.receiptDigest || null,
76
+ state,
77
+ fence,
78
+ identity,
79
+ observation,
80
+ createdAt: new Date().toISOString(),
81
+ }, privateKey);
82
+
83
+ const exact = (actual, expected, label) => {
84
+ if (actual !== expected) throw new Error(`${label} mismatch: ${actual ?? '(missing)'} != ${expected}`);
85
+ };
86
+
87
+ export async function runReleaseTransaction({ identity, assets, adapter, privateKey, publicKey, hostVerifier }) {
88
+ const expectedId = transactionIdFor(identity);
89
+ const discovered = await adapter.discover(identity);
90
+ const competing = discovered.pending?.filter((item) => item.transactionId !== expectedId) || [];
91
+ if (competing.length) throw new Error(`pending release ${competing[0].transactionId} blocks ${expectedId}`);
92
+ if ((discovered.matchingDrafts || []).length > 1) throw new Error('duplicate matching drafts require reconciliation');
93
+
94
+ let chain = validateReceiptChain(discovered.receipts || [], identity, publicKey);
95
+ let current = chain.at(-1) || null;
96
+ const fence = current?.fence || discovered.fence || crypto.randomUUID();
97
+ const draft = discovered.matchingDrafts?.[0] || await adapter.createDraft(identity, fence);
98
+ const completed = new Set(chain.map(({ state }) => state));
99
+ const append = async (state, observation = {}) => {
100
+ const receipt = stateReceipt({ identity, prior: current, state, fence, observation, privateKey });
101
+ await adapter.appendReceipt(draft, receipt, `${RECEIPT_PREFIX}${String(receipt.sequence).padStart(4, '0')}.json`);
102
+ const observed = await adapter.readReceipt(draft, receipt.sequence);
103
+ verifyReceipt(observed, publicKey);
104
+ exact(observed.receiptDigest, receipt.receiptDigest, 'remote receipt');
105
+ current = receipt;
106
+ completed.add(state);
107
+ return receipt;
108
+ };
109
+ const intend = async (state, observation = {}) => {
110
+ if (!completed.has(state)) await append(state, observation);
111
+ };
112
+
113
+ if (!current) await append('remote-prepared', { draftId: draft.id, prior: discovered.prior });
114
+ if (TERMINAL_STATES.has(current.state)) return current;
115
+ if (current.fence !== fence) throw new Error('stale release transaction fence');
116
+
117
+ if (!completed.has('local-hosts-verified')) {
118
+ await intend('asset-upload-intent');
119
+ await adapter.uploadAssets(draft, assets, identity);
120
+ await intend('host-verification-intent', { source: 'sealed-local-assets' });
121
+ const localHosts = await hostVerifier.verify({ source: 'local', identity, assets });
122
+ if (localHosts.verdict !== 'PASS') throw new Error('local staged host verification failed');
123
+ await append('local-hosts-verified', { hosts: localHosts });
124
+ }
125
+
126
+ if (!completed.has('npm-candidate-staged')) {
127
+ await intend('npm-stage-intent');
128
+ await adapter.stageNpm(identity, assets.packagePath);
129
+ const stagedNpm = await adapter.observeNpmCandidate(identity);
130
+ exact(stagedNpm.version, identity.version, 'npm candidate version');
131
+ exact(stagedNpm.integrity, identity.packageIntegrity, 'npm candidate integrity');
132
+ await append('npm-candidate-staged', { npm: stagedNpm });
133
+ }
134
+
135
+ if (!completed.has('prepared')) {
136
+ await intend('remote-host-verification-intent');
137
+ const staged = await adapter.materializeStagedAssets(draft, identity);
138
+ try {
139
+ const remoteHosts = await hostVerifier.verify({ source: 'staged', identity, assets: staged.assets, draft });
140
+ if (remoteHosts.verdict !== 'PASS') throw new Error('remote staged host verification failed');
141
+ await append('prepared', { hosts: remoteHosts });
142
+ } finally {
143
+ staged.cleanup();
144
+ }
145
+ }
146
+
147
+ if (!completed.has('github-promoted-nonlatest')) {
148
+ await intend('github-promote-intent');
149
+ await adapter.publishDraftNonLatest(draft, identity);
150
+ const github = await adapter.observeGithub(identity);
151
+ exact(github.sha, identity.candidateSha, 'GitHub candidate SHA');
152
+ if (github.latest) throw new Error('GitHub candidate advanced latest before npm convergence');
153
+ await append('github-promoted-nonlatest', { github });
154
+ }
155
+
156
+ if (!completed.has('npm-promoted')) {
157
+ await intend('npm-promote-intent');
158
+ try {
159
+ await adapter.promoteNpm(identity);
160
+ } catch (error) {
161
+ throw new Error(`npm promotion pending for ${identity.version}: ${error.message}`);
162
+ }
163
+ const npmLatest = await adapter.observeNpmLatest();
164
+ exact(npmLatest.version, identity.version, 'npm latest');
165
+ await append('npm-promoted', { npm: npmLatest });
166
+ }
167
+
168
+ // A failed GitHub-latest promotion is compensated back to A. The historical npm-promoted
169
+ // receipt remains true, but is no longer the current external state, so a clean-runner retry
170
+ // must explicitly re-promote B before it may retry GitHub.
171
+ if (current.state === 'compensated') {
172
+ await append('npm-repromote-intent');
173
+ await adapter.promoteNpm(identity);
174
+ const npmLatest = await adapter.observeNpmLatest();
175
+ exact(npmLatest.version, identity.version, 'npm latest after compensation retry');
176
+ await append('npm-repromoted', { npm: npmLatest });
177
+ }
178
+
179
+ if (!completed.has('defaults-promoted')) {
180
+ await intend('github-latest-intent');
181
+ try {
182
+ await adapter.makeGithubLatest(draft, identity);
183
+ } catch (error) {
184
+ const observed = await adapter.observeNpmLatest();
185
+ if (observed.version !== identity.version) throw new Error('npm latest changed during compensation');
186
+ await intend('compensation-intent', { restore: discovered.prior?.npmLatest });
187
+ await adapter.restoreNpmLatest(discovered.prior?.npmLatest, identity.version);
188
+ exact((await adapter.observeNpmLatest()).version, discovered.prior?.npmLatest, 'npm compensation');
189
+ await append('compensated', { reason: error.message });
190
+ throw new Error(`GitHub latest promotion failed; npm compensated: ${error.message}`);
191
+ }
192
+ const githubLatest = await adapter.observeGithubLatest();
193
+ exact(githubLatest.tag, identity.tag, 'GitHub latest');
194
+ await append('defaults-promoted', { github: githubLatest });
195
+ }
196
+
197
+ await intend('finalize-intent');
198
+ const final = await adapter.finalize(identity, current, hostVerifier);
199
+ if (final.verdict !== 'PASS') throw new Error('final release convergence failed');
200
+ return append('channels-converged', final);
201
+ }
202
+
203
+ export async function abortReleaseTransaction({ identity, receipts, reason, authorized, adapter, privateKey, publicKey }) {
204
+ if (!authorized) throw new Error('release abort requires explicit human authorization');
205
+ const chain = validateReceiptChain(receipts, identity, publicKey);
206
+ const current = chain.at(-1);
207
+ if (!current || TERMINAL_STATES.has(current.state)) throw new Error('release transaction is not abortable');
208
+ const receipt = stateReceipt({
209
+ identity, prior: current, state: 'aborted', fence: current.fence,
210
+ observation: { reason, authorized: true }, privateKey,
211
+ });
212
+ await adapter.appendReceipt(null, receipt, `${RECEIPT_PREFIX}${String(receipt.sequence).padStart(4, '0')}.json`);
213
+ return receipt;
214
+ }