ruvnet-brain 4.2.2-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 (73) hide show
  1. package/README.md +15 -14
  2. package/bin/install.mjs +11 -2
  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/ground-ruvnet.sh +2 -2
  23. package/plugin/scripts/nightly-controller.mjs +19 -2
  24. package/plugin/scripts/project-progression-contract.mjs +409 -0
  25. package/plugin/scripts/project-progression-hook.mjs +196 -0
  26. package/plugin/scripts/project-progression-outbox.mjs +95 -0
  27. package/plugin/scripts/project-progression-session-start.mjs +183 -0
  28. package/plugin/scripts/project-progression-store.mjs +248 -0
  29. package/plugin/scripts/project-store-resolver.mjs +106 -0
  30. package/plugin/scripts/session-snapshot-hook.mjs +29 -1
  31. package/plugin/scripts/session-start-core.mjs +22 -18
  32. package/plugin/scripts/spend-guard.mjs +6 -1
  33. package/scripts/adr-072-completion.mjs +98 -0
  34. package/scripts/behavioral-l1-l4.mjs +11 -8
  35. package/scripts/brain-score.mjs +7 -2
  36. package/scripts/build-bundle.mjs +82 -6
  37. package/scripts/card-from-source.mjs +32 -6
  38. package/scripts/corpus-aggregates.mjs +194 -0
  39. package/scripts/corpus-candidate.mjs +56 -36
  40. package/scripts/corpus-reconcile.mjs +295 -28
  41. package/scripts/corpus-seed-publish.mjs +3 -3
  42. package/scripts/coverage-integrity.mjs +3 -0
  43. package/scripts/gist-receipts.mjs +183 -0
  44. package/scripts/git-hooks/pre-push +21 -0
  45. package/scripts/host-registry.mjs +118 -0
  46. package/scripts/independent-review-receipt.mjs +499 -0
  47. package/scripts/ingest-gists.mjs +34 -5
  48. package/scripts/ingest-new-repos.mjs +5 -0
  49. package/scripts/metaharness-gate.mjs +58 -0
  50. package/scripts/nightly-gists.sh +3 -3
  51. package/scripts/nightly-wrapper.sh +75 -69
  52. package/scripts/product-integrity-contract.mjs +156 -0
  53. package/scripts/public-inventory.mjs +4 -0
  54. package/scripts/public-verification-aggregate.mjs +342 -0
  55. package/scripts/public-verification-finalizer.mjs +59 -0
  56. package/scripts/public-verification-inputs.mjs +473 -0
  57. package/scripts/public-verification-lane.mjs +244 -0
  58. package/scripts/publication-receipt.mjs +50 -6
  59. package/scripts/rebuild-gists-from-receipts.mjs +50 -5
  60. package/scripts/release-projection.mjs +86 -0
  61. package/scripts/release-transaction-provider.mjs +43 -102
  62. package/scripts/release-transaction.mjs +139 -51
  63. package/scripts/release.mjs +15 -7
  64. package/scripts/restore-local-ingests.mjs +2 -2
  65. package/scripts/retrieval-canary.mjs +533 -0
  66. package/scripts/routing-flywheel.mjs +12 -1
  67. package/scripts/rvf-generation.mjs +9 -3
  68. package/scripts/self-update.mjs +19 -44
  69. package/scripts/source-coverage.mjs +415 -0
  70. package/scripts/source-scope-receipt.mjs +84 -0
  71. package/scripts/sync-census.mjs +0 -0
  72. package/scripts/wired-check.mjs +114 -24
  73. package/scripts/worktree-integrity.mjs +123 -0
@@ -3,8 +3,9 @@ import crypto from 'node:crypto';
3
3
  import fs from 'node:fs';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
+ import { canonicalJson } from './coverage-integrity.mjs';
6
7
  import {
7
- pollObservation, RECEIPT_PREFIX, TERMINAL_STATES, transactionIdFor,
8
+ pollObservation, RECEIPT_PREFIX, receiptDisposition, transactionIdFor,
8
9
  } from './release-transaction.mjs';
9
10
 
10
11
  const REPO = 'stuinfla/ruvnet-brain';
@@ -94,9 +95,7 @@ export function selectCurrentReleaseBytes({ remoteBytes, localBytes, generatedBy
94
95
  return Buffer.from(bytes);
95
96
  }
96
97
 
97
- export function liveReleaseProvider({
98
- root = process.cwd(), candidateReceipt = null, publicationReceipt = null,
99
- } = {}) {
98
+ export function liveReleaseProvider({ root = process.cwd() } = {}) {
100
99
  let releases = [];
101
100
  let activeDraft = null;
102
101
  const assetDigestCache = new Map();
@@ -215,7 +214,8 @@ export function liveReleaseProvider({
215
214
  const legacySettled = [];
216
215
  const pending = [];
217
216
  for (const { receipt, release } of latestByTransaction.values()) {
218
- if (TERMINAL_STATES.has(receipt.state)) continue;
217
+ const disposition = receiptDisposition(receipt);
218
+ if (['verified', 'closed-unsuccessful', 'legacy-closed'].includes(disposition)) continue;
219
219
  // CONVERGENCE IS A FACT ABOUT THE CHANNELS, NOT ABOUT THE RECEIPT FORMAT (fixed 2026-08-07).
220
220
  //
221
221
  // This required `receipt.schemaVersion === 1`. Receipts are written as schemaVersion 2
@@ -230,7 +230,7 @@ export function liveReleaseProvider({
230
230
  // all three hold, the transaction reached its goal whatever schema its receipts use. The
231
231
  // check is not weakened — the schema clause was never load-bearing for that question, it
232
232
  // just silently expired when the schema moved on.
233
- const settled = release.draft === false
233
+ const settled = receipt.schemaVersion <= 2 && release.draft === false
234
234
  && receipt.identity?.version === npmLatest && receipt.identity?.tag === githubLatest;
235
235
  if (settled) legacySettled.push(receipt.transactionId);
236
236
  else pending.push(receipt);
@@ -318,6 +318,43 @@ export function liveReleaseProvider({
318
318
  }
319
319
  },
320
320
 
321
+ async materializePublicVerificationAggregate({ aggregate }) {
322
+ const anchor = activeDraft;
323
+ if (!anchor) throw new Error('no release anchor for public verification aggregate');
324
+ const release = hydratedRelease(releaseById(anchor.id));
325
+ const aggregateAsset = Buffer.from(canonicalJson(aggregate));
326
+ const signatureAsset = Buffer.from(aggregate.signature, 'base64');
327
+ const expected = {
328
+ 'public-verification-aggregate.json': aggregateAsset,
329
+ 'public-verification-aggregate.sig': signatureAsset,
330
+ };
331
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-public-aggregate-'));
332
+ try {
333
+ for (const [name, bytes] of Object.entries(expected)) {
334
+ const existing = release.assets?.find((asset) => asset.name === name);
335
+ if (existing) {
336
+ if (!assetBytes(existing).equals(bytes)) throw new Error(`public verification asset conflict: ${name}`);
337
+ continue;
338
+ }
339
+ const file = path.join(temp, name);
340
+ fs.writeFileSync(file, bytes, { flag: 'wx', mode: 0o600 });
341
+ command('gh', ['release', 'upload', anchor.tag, file, '--repo', REPO]);
342
+ }
343
+ const observed = hydratedRelease(releaseById(anchor.id));
344
+ for (const [name, bytes] of Object.entries(expected)) {
345
+ const asset = observed.assets?.find((item) => item.name === name);
346
+ if (!asset || !assetBytes(asset).equals(bytes)) throw new Error(`public verification asset readback differs: ${name}`);
347
+ }
348
+ return {
349
+ aggregateSha256: aggregate.aggregateSha256,
350
+ aggregateAssetSha256: sha256(aggregateAsset),
351
+ signatureAssetSha256: sha256(signatureAsset),
352
+ };
353
+ } finally {
354
+ fs.rmSync(temp, { recursive: true, force: true });
355
+ }
356
+ },
357
+
321
358
  async readReceipt(draft, sequence) {
322
359
  const name = `${RECEIPT_PREFIX}${String(sequence).padStart(4, '0')}.json`;
323
360
  const release = hydratedRelease(releaseById((draft || activeDraft).id));
@@ -395,101 +432,5 @@ export function liveReleaseProvider({
395
432
  if (current !== expected) throw new Error(`refusing compensation: npm latest is ${current}, expected ${expected}`);
396
433
  command('npm', ['dist-tag', 'add', `${PACKAGE}@${prior}`, 'latest']);
397
434
  },
398
- async finalize(identity, receipt, hostVerifier) {
399
- if (!candidateReceipt || !publicationReceipt) {
400
- throw new Error('final convergence requires candidate and publication receipt paths');
401
- }
402
- // THE HOST VERDICT IS MEASURED, NOT DECLARED — and it is measured FIRST.
403
- //
404
- // finalize() used to accept the verifier as `_hostVerifier` and ignore it, then build
405
- // `hosts = { verdict: 'PASS', … }` as a literal. Every release therefore asserted host
406
- // convergence with no host verified, while tests/helpers/release-transaction-fixture.mjs:153
407
- // called the seam faithfully — a fault-injection suite certifying wiring the real publisher
408
- // never ran. Fable 5 and GPT-5.6-Sol independently made this their #1 finding.
409
- //
410
- // It runs BEFORE publication and sealing because the order encodes the meaning: you seal a
411
- // release you have verified, not the reverse. It is also the cheapest way to fail — an
412
- // unusable artifact stops here instead of after a 20-minute publish.
413
- if (!hostVerifier || typeof hostVerifier.verify !== 'function') {
414
- return { verdict: 'FAIL', hostVerifierError: 'no host verifier was supplied to finalize' };
415
- }
416
- // No `assets` override: stagedHostVerifier is constructed in release.mjs:238 with the exact
417
- // staged bytes for this candidate and defaults to them (staged-host-verifier.mjs:57), so
418
- // passing nothing is what keeps production verifying the artifact actually being shipped.
419
- const verified = await hostVerifier.verify({ source: 'final', identity });
420
- if (verified.verdict !== 'PASS') {
421
- return {
422
- verdict: 'FAIL',
423
- hosts: { verdict: verified.verdict, verifier: { error: verified.error, fixtures: verified.fixtures } },
424
- previousReceiptDigest: receipt.receiptDigest,
425
- };
426
- }
427
- if (!fs.existsSync(path.resolve(root, publicationReceipt))) {
428
- const publication = spawnSync(process.execPath, [
429
- 'scripts/publication-receipt.mjs', '--candidate', candidateReceipt, '--out', publicationReceipt,
430
- ], { cwd: root, encoding: 'utf8', timeout: 1_200_000 });
431
- if (publication.error || publication.status !== 0) {
432
- return { verdict: 'FAIL', publicationError: String(publication.stderr || publication.error?.message) };
433
- }
434
- }
435
- const seal = spawnSync(process.execPath, [
436
- 'scripts/release-proof.mjs', '--candidate', candidateReceipt, '--publication', publicationReceipt,
437
- ], { cwd: root, encoding: 'utf8', timeout: 300_000 });
438
- if (seal.error || seal.status !== 0) {
439
- return { verdict: 'FAIL', sealError: String(seal.stderr || seal.error?.message) };
440
- }
441
- const publication = JSON.parse(fs.readFileSync(path.resolve(root, publicationReceipt), 'utf8'));
442
-
443
- // `verified` was measured at the top of finalize, before publication and sealing.
444
- const hosts = {
445
- verdict: verified.verdict,
446
- claudeOnly: publication.installed?.claudeOnly,
447
- codexOnly: publication.installed?.codexOnly,
448
- dual: publication.installed?.dual,
449
- verifier: { artifactSha256: verified.artifactSha256, fixtures: verified.fixtures, error: verified.error },
450
- };
451
- const observed = publication.postPublicationChecks?.find(({ name }) => name === 'published-surface-probe');
452
- const result = {
453
- verdict: observed?.status === 'completed' && observed?.conclusion === 'success' ? 'PASS' : 'FAIL',
454
- hosts, surface: observed, publicationReceipt, previousReceiptDigest: receipt.receiptDigest,
455
- };
456
- if (result.verdict === 'PASS') {
457
- const currentRelease = path.join(path.dirname(path.resolve(root, publicationReceipt)), 'current-release.json');
458
- const generatedCurrentRelease = Buffer.from(`${JSON.stringify({
459
- schemaVersion: 1,
460
- transactionId: receipt.transactionId,
461
- version: identity.version,
462
- tag: identity.tag,
463
- candidateSha: identity.candidateSha,
464
- verdict: 'PASS',
465
- publicationReceiptSha256: sha256(fs.readFileSync(path.resolve(root, publicationReceipt))),
466
- hosts,
467
- surface: observed,
468
- }, null, 2)}\n`);
469
- let remote = hydratedRelease(releaseById(activeDraft.id)).assets
470
- ?.find((asset) => asset.name === 'current-release.json');
471
- let currentReleaseBytes;
472
- try {
473
- currentReleaseBytes = selectCurrentReleaseBytes({
474
- remoteBytes: remote ? assetBytes(remote) : null,
475
- localBytes: !remote && fs.existsSync(currentRelease) ? fs.readFileSync(currentRelease) : null,
476
- generatedBytes: generatedCurrentRelease,
477
- identity: { transactionId: receipt.transactionId, version: identity.version, candidateSha: identity.candidateSha },
478
- });
479
- } catch (error) {
480
- return { ...result, verdict: 'FAIL', currentReleaseError: error.message };
481
- }
482
- if (!fs.existsSync(currentRelease)) fs.writeFileSync(currentRelease, currentReleaseBytes, { flag: 'wx', mode: 0o600 });
483
- if (!remote) {
484
- command('gh', ['release', 'upload', activeDraft.tag, currentRelease, '--repo', REPO]);
485
- remote = hydratedRelease(releaseById(activeDraft.id)).assets
486
- ?.find((asset) => asset.name === 'current-release.json');
487
- }
488
- if (!remote || sha256(assetBytes(remote)) !== sha256(currentReleaseBytes)) {
489
- return { ...result, verdict: 'FAIL', currentReleaseError: 'public receipt verification failed' };
490
- }
491
- }
492
- return result;
493
- },
494
435
  };
495
436
  }
@@ -1,12 +1,30 @@
1
1
  #!/usr/bin/env node
2
2
  import crypto from 'node:crypto';
3
+ import { canonicalJson } from './coverage-integrity.mjs';
4
+ import { verifyPublicVerificationAggregate } from './public-verification-aggregate.mjs';
5
+
6
+ export { canonicalJson };
3
7
 
4
8
  export const RECEIPT_PREFIX = 'release-transaction-';
5
- export const TERMINAL_STATES = new Set(['channels-converged', 'aborted']);
9
+ export const CURRENT_RECEIPT_SCHEMA_VERSION = 3;
10
+ export const SUCCESS_CLOSED_STATES = new Set(['install-verified']);
11
+ export const UNSUCCESSFUL_CLOSED_STATES = new Set(['aborted', 'abandoned', 'superseded']);
12
+
13
+ export function receiptDisposition(receipt) {
14
+ if (!receipt || !Number.isInteger(receipt.schemaVersion)) return 'invalid';
15
+ if (receipt.schemaVersion <= 2 && receipt.state === 'channels-converged') return 'legacy-closed';
16
+ if (receipt.schemaVersion >= 3 && SUCCESS_CLOSED_STATES.has(receipt.state)) return 'verified';
17
+ if (UNSUCCESSFUL_CLOSED_STATES.has(receipt.state)) return 'closed-unsuccessful';
18
+ if (receipt.schemaVersion >= 3 && receipt.state === 'channels-converged') return 'pending-public-verification';
19
+ return 'pending';
20
+ }
21
+
22
+ export const isClosedReceipt = (receipt) => ['verified', 'closed-unsuccessful', 'legacy-closed']
23
+ .includes(receiptDisposition(receipt));
6
24
 
7
25
  // `aborted` WAS UNREACHABLE, AND THAT BRICKED THE RELEASE RAIL (found 2026-08-07, issue #77).
8
26
  //
9
- // `aborted` has always been in TERMINAL_STATES, but no state below listed it as a target — so it
27
+ // `aborted` was historically declared terminal, but no state below listed it as a target — so it
10
28
  // was a terminal state nothing could ever enter. Combined with `manual-intervention-required`
11
29
  // (which has NO outgoing transitions and is NOT terminal), an interrupted release had exactly two
12
30
  // destinations and both were permanent non-terminal dead ends.
@@ -33,7 +51,7 @@ export const ALLOWED_TRANSITIONS = Object.freeze({
33
51
  'npm-candidate-staged': ['remote-materialization-intent', 'manual-intervention-required', ...ABORTABLE],
34
52
  'remote-materialization-intent': ['prepared', 'manual-intervention-required', ...ABORTABLE],
35
53
  prepared: ['github-promote-intent', 'manual-intervention-required', ...ABORTABLE],
36
- 'github-promote-intent': ['github-promoted-nonlatest', 'manual-intervention-required', ...ABORTABLE],
54
+ 'github-promote-intent': ['github-promoted-nonlatest', 'compensation-intent', 'manual-intervention-required', ...ABORTABLE],
37
55
  'github-promoted-nonlatest': ['npm-promote-intent', 'manual-intervention-required', ...ABORTABLE],
38
56
  'npm-promote-intent': ['npm-promoted', 'compensation-intent', 'manual-intervention-required', ...ABORTABLE],
39
57
  // `defaults-promoted` added 2026-08-07: when GitHub is ALREADY latest at the moment npm is
@@ -51,7 +69,10 @@ export const ALLOWED_TRANSITIONS = Object.freeze({
51
69
  'defaults-promoted': ['finalize-intent', 'manual-intervention-required', ...ABORTABLE],
52
70
  'finalize-intent': ['channels-converged', 'manual-intervention-required', ...ABORTABLE],
53
71
  'manual-intervention-required': [],
54
- 'channels-converged': [],
72
+ 'channels-converged': ['install-verified', 'abandoned', 'superseded'],
73
+ 'install-verified': [],
74
+ abandoned: [],
75
+ superseded: [],
55
76
  aborted: [],
56
77
  });
57
78
 
@@ -79,16 +100,6 @@ export const ALLOWED_TRANSITIONS = Object.freeze({
79
100
  * here. This does not change the digest of any receipt that never held an undefined value, which is
80
101
  * every receipt that currently verifies.
81
102
  */
82
- export const canonicalJson = (value) => {
83
- if (Array.isArray(value)) return `[${value.map((item) => (item === undefined ? 'null' : canonicalJson(item))).join(',')}]`;
84
- if (value && typeof value === 'object') {
85
- return `{${Object.keys(value).sort()
86
- .filter((key) => value[key] !== undefined)
87
- .map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
88
- }
89
- return JSON.stringify(value);
90
- };
91
-
92
103
  export const receiptPayload = (receipt) => {
93
104
  const { signature: _signature, receiptDigest: _digest, ...payload } = receipt;
94
105
  return canonicalJson(payload);
@@ -125,7 +136,8 @@ export function signReceipt(receipt, privateKey) {
125
136
  }
126
137
 
127
138
  export function verifyReceipt(receipt, publicKey) {
128
- if (!receipt || ![1, 2].includes(receipt.schemaVersion) || !/^[a-f0-9]{64}$/.test(receipt.transactionId || '')) {
139
+ if (!receipt || ![1, 2, CURRENT_RECEIPT_SCHEMA_VERSION].includes(receipt.schemaVersion)
140
+ || !/^[a-f0-9]{64}$/.test(receipt.transactionId || '')) {
129
141
  throw new Error('invalid release transaction receipt');
130
142
  }
131
143
  const { signature, receiptDigest, ...unsigned } = receipt;
@@ -149,13 +161,20 @@ export function validateReceiptChain(receipts, identity, publicKey) {
149
161
  if ((receipt.previousReceiptDigest || null) !== (previous?.receiptDigest || null)) {
150
162
  throw new Error('release receipt chain conflict');
151
163
  }
164
+ if (previous && receipt.schemaVersion >= 3 && receiptDisposition(previous) === 'legacy-closed') {
165
+ throw new Error('legacy-closed release receipt cannot be promoted into schema 3');
166
+ }
167
+ if (previous && receipt.schemaVersion >= 3
168
+ && !ALLOWED_TRANSITIONS[previous.state]?.includes(receipt.state)) {
169
+ throw new Error(`illegal release transition ${previous.state} -> ${receipt.state}`);
170
+ }
152
171
  previous = receipt;
153
172
  }
154
173
  return sorted;
155
174
  }
156
175
 
157
176
  const stateReceipt = ({ identity, prior, state, observation = {}, privateKey }) => signReceipt({
158
- schemaVersion: 2,
177
+ schemaVersion: CURRENT_RECEIPT_SCHEMA_VERSION,
159
178
  transactionId: transactionIdFor(identity),
160
179
  sequence: prior ? prior.sequence + 1 : 0,
161
180
  previousReceiptDigest: prior?.receiptDigest || null,
@@ -203,11 +222,15 @@ export function reduceReleaseState({ lastReceipt, snapshot, identity, prior }) {
203
222
  }
204
223
  if (npmB && snapshot.github?.draft === true) return { action: 'compensate-npm' };
205
224
  if (npmB && githubLatest) {
206
- if (lastReceipt.state === 'channels-converged') {
225
+ if (lastReceipt.state === 'install-verified') {
207
226
  return githubPublished && assetsExact && candidateExact
208
- && snapshot.publicReceiptExact && snapshot.publicHostsExact
209
227
  ? { action: 'complete' }
210
- : { action: 'manual', reason: 'terminal public state drifted' };
228
+ : { action: 'manual', reason: 'verified public channels drifted' };
229
+ }
230
+ if (lastReceipt.state === 'channels-converged') {
231
+ return githubPublished && assetsExact && candidateExact
232
+ ? { action: 'await-install-verification' }
233
+ : { action: 'manual', reason: 'channel-converged state drifted' };
211
234
  }
212
235
  return { action: 'finalize' };
213
236
  }
@@ -257,7 +280,7 @@ export async function pollObservation(read, predicate, {
257
280
  throw new Error(`visibility deadline exceeded after ${now() - started}ms: ${detail}`);
258
281
  }
259
282
 
260
- export async function runReleaseTransaction({ identity, assets, adapter, privateKey, publicKey, hostVerifier }) {
283
+ export async function runReleaseTransaction({ identity, assets, adapter, privateKey, publicKey }) {
261
284
  const expectedId = transactionIdFor(identity);
262
285
  const discovered = await adapter.discover(identity);
263
286
  const competing = discovered.pending?.filter((item) => item.transactionId !== expectedId) || [];
@@ -269,7 +292,8 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
269
292
  const draft = discovered.matchingDrafts?.[0] || await adapter.createDraft(identity);
270
293
  const append = async (state, observation = {}) => {
271
294
  const recoveryCompensation = state === 'compensation-intent' && current
272
- && !TERMINAL_STATES.has(current.state) && current.state !== 'manual-intervention-required';
295
+ && !isClosedReceipt(current) && current.state !== 'manual-intervention-required'
296
+ && current.state !== 'channels-converged';
273
297
  if (current && !ALLOWED_TRANSITIONS[current.state]?.includes(state) && current.state !== state
274
298
  && !recoveryCompensation) {
275
299
  throw new Error(`illegal release transition ${current.state} -> ${state}`);
@@ -298,10 +322,10 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
298
322
  const snapshot = await adapter.observeSnapshot(identity, draft, {
299
323
  forceAssets: current.state === 'finalize-intent' || current.state === 'channels-converged',
300
324
  });
301
- if (current.state === 'channels-converged') {
302
- const terminal = reduceReleaseState({ lastReceipt: current, snapshot, identity, prior });
303
- if (terminal.action === 'complete') return current;
304
- throw new Error(`terminal release drift: ${terminal.reason || terminal.action}`);
325
+ if (current.state === 'channels-converged' || current.state === 'install-verified') {
326
+ const disposition = reduceReleaseState({ lastReceipt: current, snapshot, identity, prior });
327
+ if (['complete', 'await-install-verification'].includes(disposition.action)) return current;
328
+ throw new Error(`release state drift: ${disposition.reason || disposition.action}`);
305
329
  }
306
330
  // A process may die after the provider side effect and before its observation receipt. Rebuild
307
331
  // that missing receipt from fresh state before asking the reducer for the next command.
@@ -332,11 +356,11 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
332
356
  await transition('compensated', { npm: snapshot.npm, github: snapshot.github, recovered: true });
333
357
  throw new Error('npm compensation recovered; resume the same transaction');
334
358
  }
335
- if (current.state === 'finalize-intent' && snapshot.publicReceiptExact && snapshot.publicHostsExact
336
- && npmCandidateExact(snapshot, identity) && npmLatestIsB(snapshot, identity)
359
+ if (current.state === 'finalize-intent' && npmCandidateExact(snapshot, identity) && npmLatestIsB(snapshot, identity)
337
360
  && githubIsB(snapshot, identity) && snapshot.github?.published && snapshot.github?.latest
338
361
  && snapshot.github?.assetsExact) {
339
- return append('channels-converged', { verdict: 'PASS', recovered: true });
362
+ return append('channels-converged', { verdict: 'PUBLISHED_NOT_VERIFIED', recovered: true,
363
+ npm: snapshot.npm, github: snapshot.github });
340
364
  }
341
365
  const decision = reduceReleaseState({ lastReceipt: current, snapshot, identity, prior });
342
366
  const stageRecord = {
@@ -351,6 +375,7 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
351
375
  ? `::notice title=Release stage ${decision.action}::${JSON.stringify(stageRecord)}`
352
376
  : JSON.stringify(stageRecord));
353
377
  if (decision.action === 'complete') return current;
378
+ if (decision.action === 'await-install-verification') return current;
354
379
  if (decision.action === 'manual') {
355
380
  await transition('manual-intervention-required', { reason: decision.reason, snapshot });
356
381
  throw new Error(`release requires manual intervention: ${decision.reason}`);
@@ -443,32 +468,15 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
443
468
  await transition('defaults-promoted', { npm: snapshot.npm, github: snapshot.github });
444
469
  }
445
470
  await transition('finalize-intent');
446
- const final = await adapter.finalize(identity, current, hostVerifier);
447
- // SAY WHICH CONVERGENCE FAILED, AND WHY (2026-08-07). This threw the bare sentence
448
- // `final release convergence failed` while `finalize` had already returned exactly the reason
449
- // — hostVerifierError, hosts.verifier.error, publicationError or sealError — and the throw
450
- // discarded every one of them. That is the same defect that let `spawnSync ENOBUFS` masquerade
451
- // as `staged GitHub payload mismatch` for three days: an error that reports its category and
452
- // withholds its evidence. Publication had ALREADY succeeded on both channels here, so the
453
- // operator is reading this line while npm and GitHub are correct, with nothing to act on.
454
- if (final.verdict !== 'PASS') {
455
- const why = final.hostVerifierError
456
- || final.hosts?.verifier?.error
457
- || final.publicationError
458
- || final.sealError
459
- || `verdict=${final.verdict ?? '(none)'}`;
460
- const fixtures = final.hosts?.verifier?.fixtures;
461
- const detail = fixtures ? ` | fixtures: ${JSON.stringify(fixtures).slice(0, 400)}` : '';
462
- throw new Error(`final release convergence failed: ${why}${detail}`);
463
- }
464
471
  const reobserved = await adapter.observeSnapshot(identity, draft, { forceAssets: true });
465
472
  if (!(npmLatestIsB(reobserved, identity) && reobserved.github?.latest && githubIsB(reobserved, identity))) {
466
- throw new Error('provider defaults drifted during finalization');
473
+ throw new Error('provider defaults drifted before channel convergence');
467
474
  }
468
- if (!reobserved.github?.assetsExact || !reobserved.publicReceiptExact || !reobserved.publicHostsExact) {
469
- throw new Error('public receipt, host, or artifact evidence drifted during finalization');
475
+ if (!reobserved.github?.assetsExact || !npmCandidateExact(reobserved, identity)) {
476
+ throw new Error('provider artifacts drifted before channel convergence');
470
477
  }
471
- return append('channels-converged', final);
478
+ return append('channels-converged', { verdict: 'PUBLISHED_NOT_VERIFIED',
479
+ npm: reobserved.npm, github: reobserved.github });
472
480
  }
473
481
  }
474
482
  throw new Error('release reducer exceeded its bounded transition count');
@@ -478,7 +486,9 @@ export async function abortReleaseTransaction({ identity, receipts, reason, auth
478
486
  if (!authorized) throw new Error('release abort requires explicit human authorization');
479
487
  const chain = validateReceiptChain(receipts, identity, publicKey);
480
488
  const current = chain.at(-1);
481
- if (!current || TERMINAL_STATES.has(current.state)) throw new Error('release transaction is not abortable');
489
+ if (!current || isClosedReceipt(current) || !ALLOWED_TRANSITIONS[current.state]?.includes('aborted')) {
490
+ throw new Error('release transaction is not abortable');
491
+ }
482
492
  const snapshot = await adapter.observeSnapshot(identity);
483
493
  const prior = chain[0]?.observation?.prior;
484
494
  if (!prior || snapshot.npm?.latestVersion === identity.version || snapshot.github?.latest === true
@@ -492,3 +502,81 @@ export async function abortReleaseTransaction({ identity, receipts, reason, auth
492
502
  await adapter.appendReceipt(null, receipt, `${RECEIPT_PREFIX}${String(receipt.sequence).padStart(4, '0')}.json`);
493
503
  return receipt;
494
504
  }
505
+
506
+ export async function finalizeReleaseTransaction({
507
+ identity,
508
+ aggregate,
509
+ adapter,
510
+ privateKey,
511
+ publicKey,
512
+ aggregatePublicKey,
513
+ } = {}) {
514
+ if (!adapter || typeof adapter.discover !== 'function' || typeof adapter.observeSnapshot !== 'function'
515
+ || typeof adapter.materializePublicVerificationAggregate !== 'function'
516
+ || typeof adapter.appendReceipt !== 'function' || typeof adapter.readReceipt !== 'function') {
517
+ throw new Error('public verification finalizer adapter is incomplete');
518
+ }
519
+ const expectedAggregateIdentity = {
520
+ ...aggregate?.identity,
521
+ sourceSha: identity?.candidateSha,
522
+ version: identity?.version,
523
+ tag: identity?.tag,
524
+ artifactSha256: identity?.packageSha256,
525
+ bundleSha256: identity?.bundleSha256,
526
+ payloadId: identity?.payloadId,
527
+ releaseTransactionId: transactionIdFor(identity),
528
+ };
529
+ verifyPublicVerificationAggregate(aggregate, aggregatePublicKey, expectedAggregateIdentity);
530
+ const discovered = await adapter.discover(identity);
531
+ const chain = validateReceiptChain(discovered.receipts || [], identity, publicKey);
532
+ const current = chain.at(-1);
533
+ if (!current) throw new Error('release transaction has no receipt chain');
534
+ if (current.state === 'install-verified') {
535
+ if (current.observation?.publicVerification?.aggregateSha256 !== aggregate.aggregateSha256) {
536
+ throw new Error('release is already install-verified by a different aggregate');
537
+ }
538
+ return current;
539
+ }
540
+ if (current.schemaVersion < 3 || current.state !== 'channels-converged') {
541
+ throw new Error(`public verification finalizer requires schema-3 channels-converged, found ${current.state}`);
542
+ }
543
+ const snapshot = await adapter.observeSnapshot(identity, discovered.matchingDrafts?.[0], { forceAssets: true });
544
+ if (snapshot.readError || !npmLatestIsB(snapshot, identity) || !npmCandidateExact(snapshot, identity)
545
+ || !githubIsB(snapshot, identity) || snapshot.github?.published !== true || snapshot.github?.latest !== true
546
+ || snapshot.github?.assetsExact !== true) throw new Error('public channels drifted before install verification');
547
+ const materialized = await adapter.materializePublicVerificationAggregate({ identity, aggregate });
548
+ const aggregateAssetSha256 = crypto.createHash('sha256').update(canonicalJson(aggregate)).digest('hex');
549
+ const signatureAssetSha256 = crypto.createHash('sha256').update(Buffer.from(aggregate.signature, 'base64')).digest('hex');
550
+ if (materialized?.aggregateSha256 !== aggregate.aggregateSha256
551
+ || materialized?.aggregateAssetSha256 !== aggregateAssetSha256
552
+ || materialized?.signatureAssetSha256 !== signatureAssetSha256) {
553
+ throw new Error('materialized public verification aggregate differs from signed evidence');
554
+ }
555
+ const receipt = stateReceipt({
556
+ identity,
557
+ prior: current,
558
+ state: 'install-verified',
559
+ observation: {
560
+ verdict: 'INSTALL_VERIFIED',
561
+ publicVerification: {
562
+ aggregateSha256: aggregate.aggregateSha256,
563
+ aggregateAssetSha256,
564
+ signatureAssetSha256,
565
+ lanes: aggregate.metrics.leaves,
566
+ recallAt10: aggregate.metrics.recallAt10,
567
+ deltaCitationRate: aggregate.metrics.deltaCitationRate,
568
+ reviewReceipts: aggregate.reviews,
569
+ },
570
+ npm: snapshot.npm,
571
+ github: snapshot.github,
572
+ },
573
+ privateKey,
574
+ });
575
+ const name = `${RECEIPT_PREFIX}${String(receipt.sequence).padStart(4, '0')}.json`;
576
+ await adapter.appendReceipt(discovered.matchingDrafts?.[0] || null, receipt, name);
577
+ const persisted = await adapter.readReceipt(discovered.matchingDrafts?.[0] || null, receipt.sequence);
578
+ verifyReceipt(persisted, publicKey);
579
+ if (canonicalJson(persisted) !== canonicalJson(receipt)) throw new Error('install-verified receipt readback differs');
580
+ validateReceiptChain([...chain, persisted], identity, publicKey);
581
+ return persisted;
582
+ }
@@ -32,10 +32,10 @@ import crypto from 'node:crypto';
32
32
  import { validateProtectedPublishInvocation } from './protected-release-invocation.mjs';
33
33
  import { runReleaseTransaction } from './release-transaction.mjs';
34
34
  import { liveReleaseProvider } from './release-transaction-provider.mjs';
35
- import { stagedHostVerifier } from './staged-host-verifier.mjs';
36
35
  import { verifyPayload } from './release-payload.mjs';
37
36
 
38
37
  const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
38
+ const IS_MAIN = path.resolve(process.argv[1] || '') === fileURLToPath(import.meta.url);
39
39
  const PUBLISH = process.argv.includes('--publish');
40
40
  const CORPUS_SEED = process.argv.includes('--corpus-seed');
41
41
  let protectedCandidate = null;
@@ -159,7 +159,9 @@ export function runProtectedCorpusSeed({
159
159
  if (digestMatch[1] !== archiveSha256) corpusFailure('corpus tag digest does not match the receipt and archive');
160
160
 
161
161
  const viewArgs = ['release', 'view', tag, '--json', 'tagName', '--repo', repo];
162
- const view = run('gh', viewArgs, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
162
+ const ghCommand = env.RUVNET_GH_COMMAND || 'gh';
163
+ const ghPrefix = env.RUVNET_GH_SCRIPT ? [env.RUVNET_GH_SCRIPT] : [];
164
+ const view = run(ghCommand, [...ghPrefix, ...viewArgs], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
163
165
  if (!view.error && view.status === 0) corpusFailure(`release ${tag} already exists; refusing to overwrite immutable corpus seed`);
164
166
  const viewError = String(view.error?.message || view.stderr || view.stdout || '');
165
167
  if (!/(release not found|no release found)/i.test(viewError)) corpusFailure(`cannot prove ${tag} is absent (${viewError.trim() || `gh exited ${view.status}`})`);
@@ -181,14 +183,14 @@ export function runProtectedCorpusSeed({
181
183
  '--notes', notes,
182
184
  bundleFile, receiptFile,
183
185
  ];
184
- const create = run('gh', createArgs, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
186
+ const create = run(ghCommand, [...ghPrefix, ...createArgs], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
185
187
  if (create.error || create.status !== 0) {
186
188
  corpusFailure(`protected corpus publication failed (${String(create.error?.message || create.stderr || create.stdout || '').trim()})`);
187
189
  }
188
190
  return { tag, target, repository: repo, archiveSha256, receiptSha256: sha256File(receiptFile) };
189
191
  }
190
192
 
191
- if (CORPUS_SEED) {
193
+ if (IS_MAIN && CORPUS_SEED) {
192
194
  try {
193
195
  const result = runProtectedCorpusSeed();
194
196
  console.log(JSON.stringify({ ok: true, mode: 'corpus-seed', ...result }, null, 2));
@@ -196,7 +198,7 @@ if (CORPUS_SEED) {
196
198
  console.error(error.message);
197
199
  process.exitCode = 1;
198
200
  }
199
- } else {
201
+ } else if (IS_MAIN) {
200
202
 
201
203
  console.log(`\n${c.b('RuvNet Brain — release / definition-of-done')} ${c.dim('· ' + (PUBLISH ? 'PUBLISH' : 'check-only') + ' · shipping ' + V())}\n`);
202
204
 
@@ -371,6 +373,9 @@ if (PUBLISH) {
371
373
  corpusSeedSha256: crypto.createHash('sha256').update(fs.readFileSync(assets.corpusSeedPath)).digest('hex'),
372
374
  generationLedgerSha256: crypto.createHash('sha256').update(fs.readFileSync(assets.generationLedgerPath)).digest('hex'),
373
375
  };
376
+ const identityPath = path.resolve(ROOT, process.env.RUVNET_RELEASE_IDENTITY || 'release-evidence/release-identity.json');
377
+ if (fs.existsSync(identityPath)) throw new Error(`refusing to overwrite release identity: ${identityPath}`);
378
+ fs.writeFileSync(identityPath, `${JSON.stringify(identity, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
374
379
  const privatePem = process.env.RUVNET_SIGNING_KEY;
375
380
  if (!privatePem) throw new Error('RUVNET_SIGNING_KEY is required for signed transaction receipts');
376
381
  const finalReceipt = await runReleaseTransaction({
@@ -381,9 +386,12 @@ if (PUBLISH) {
381
386
  }),
382
387
  privateKey: crypto.createPrivateKey(privatePem),
383
388
  publicKey: crypto.createPublicKey(fs.readFileSync(path.join(ROOT, 'keys/ruvnet-brain-signing.pub.pem'), 'utf8')),
384
- hostVerifier: stagedHostVerifier({ assets, identity }),
385
389
  });
386
390
  if (finalReceipt.state !== 'channels-converged') throw new Error(`release transaction stopped at ${finalReceipt.state}`);
391
+ const channelReceiptPath = path.resolve(ROOT,
392
+ process.env.RUVNET_CHANNEL_RECEIPT || 'release-evidence/channels-converged-receipt.json');
393
+ if (fs.existsSync(channelReceiptPath)) throw new Error(`refusing to overwrite channel receipt: ${channelReceiptPath}`);
394
+ fs.writeFileSync(channelReceiptPath, `${JSON.stringify(finalReceipt, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
387
395
  } else {
388
396
  step('D', 'remote staged release transaction — SKIPPED (check-only; pass --publish to publish)');
389
397
  }
@@ -396,7 +404,7 @@ if (!PUBLISH) {
396
404
  }
397
405
 
398
406
  if (PUBLISH) {
399
- console.log(`\n${c.g(c.b('✓✓✓ SHIPPED'))} — every gate passed and every live channel is current. ${c.dim('A user on any path (npm, npx, explainer, --update) gets the working, current build.')}\n`);
407
+ console.log(`\n${c.y(c.b('PUBLISHED, NOT VERIFIED'))} — npm and GitHub expose the sealed candidate; public install verification is still required.\n`);
400
408
  } else {
401
409
  console.log(`\n${c.g(c.b('✓✓✓ PREFLIGHT PASS — NOT PUBLISHED'))} — the committed candidate passed every check-only gate.\n`);
402
410
  }
@@ -42,7 +42,7 @@ import fs from 'node:fs';
42
42
  import path from 'node:path';
43
43
  import { execFileSync } from 'node:child_process';
44
44
  import { fileURLToPath } from 'node:url';
45
- import { storeRoot, storesAt } from '../kb/store-root.mjs';
45
+ import { storeRoot, storesAt, rootNeverMaterialized } from '../kb/store-root.mjs';
46
46
 
47
47
  const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
48
48
  const LEDGER = path.join(ROOT, 'kb', 'local-ingests.json');
@@ -71,7 +71,7 @@ export function missingIngests({ ledgerFile = LEDGER, root = storeRoot() } = {})
71
71
  export function classify({ ledgerFile = LEDGER, root = storeRoot() } = {}) {
72
72
  const missing = missingIngests({ ledgerFile, root });
73
73
  if (!missing.length) return { state: OK, missing };
74
- if (!fs.existsSync(root)) return { state: NEVER_MATERIALIZED, missing };
74
+ if (rootNeverMaterialized(root)) return { state: NEVER_MATERIALIZED, missing };
75
75
  return { state: WIPED, missing };
76
76
  }
77
77