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,86 @@
1
+ #!/usr/bin/env node
2
+ // Build the release-bound coverage projection from an observed corpus ledger and exact bundle bytes.
3
+ // This is intentionally a producer, not a validator: public-verification-inputs.mjs remains the
4
+ // independent consumer that proves the emitted files agree with the archive.
5
+ import crypto from 'node:crypto';
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { digest, generationLedgerBytes, projectPublicGenerationLedger,
10
+ releaseCoverageGenerationFor } from '../plugin/scripts/coverage-integrity.mjs';
11
+ import { validatePublicInventory } from './public-inventory.mjs';
12
+ import { readRvfGenerations } from './rvf-generation.mjs';
13
+
14
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
15
+ const arg = (name, fallback = null) => { const i = process.argv.indexOf(name); return i >= 0 ? process.argv[i + 1] : fallback; };
16
+ const readJson = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
17
+ const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
18
+ const storeName = (value) => String(value || '').toLowerCase().replace(/\.big\.rvf$|\.rvf$/i, '');
19
+
20
+ export function createReleaseProjection({ corpusCoverage, assetsDir, version, sourceSnapshot,
21
+ corpusSeed, baselineReceiptSha256, outDir }) {
22
+ if (!corpusCoverage || corpusCoverage.kind !== 'ruvnet-brain-corpus-coverage') {
23
+ throw new Error('corpus coverage must be an observed ruvnet-brain-corpus-coverage ledger');
24
+ }
25
+ if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version || '')) throw new Error('release version is invalid');
26
+ if (!/^[a-f0-9]{40}$/.test(sourceSnapshot || '')) throw new Error('release source snapshot is invalid');
27
+ if (!corpusSeed?.tag || !/^[a-f0-9]{64}$/.test(corpusSeed.archiveSha256 || '')
28
+ || !Number.isSafeInteger(corpusSeed.archiveBytes) || !/^[a-f0-9]{64}$/.test(baselineReceiptSha256 || '')) {
29
+ throw new Error('corpus seed identity is incomplete');
30
+ }
31
+ const assets = path.resolve(assetsDir);
32
+ const sourceLedger = readRvfGenerations(assets);
33
+ // The coverage ledger describes the full upstream universe. A release candidate contains
34
+ // only the immutable seed's asset set, so absent-but-eligible upstream rows are not release
35
+ // blockers; shipped stores must still all be CURRENT.
36
+ const assetStores = fs.readdirSync(assets)
37
+ .filter((name) => /\.rvf$/i.test(name)).map(storeName);
38
+ const availableStores = new Set([
39
+ ...Object.keys(sourceLedger.stores || {}).map(storeName),
40
+ ...assetStores,
41
+ ]);
42
+ const eligible = corpusCoverage.rows.filter((row) => row.disposition === 'eligible'
43
+ && availableStores.has(storeName(row.artifact?.store)));
44
+ if (!eligible.length || eligible.some((row) => row.status !== 'CURRENT')) throw new Error('eligible corpus rows are not all CURRENT');
45
+ const publicStores = [...new Set(eligible.map((row) => String(row.artifact.store).toLowerCase()))];
46
+ // Derived public stores are not corpus rows, but they are still part of the
47
+ // exact shipped generation ledger when the candidate asset set contains them.
48
+ if (sourceLedger.stores?.concepts && !publicStores.includes('concepts')) publicStores.push('concepts');
49
+ const ledger = projectPublicGenerationLedger({ ledger: sourceLedger, publicStores, version, sourceSnapshot });
50
+ const releaseBase = {
51
+ schemaVersion: 1, kind: 'ruvnet-brain-release-coverage', owner: corpusCoverage.owner,
52
+ observedAt: corpusCoverage.observedAt, generatorSourceSha: corpusCoverage.generatorSourceSha,
53
+ sourceObservationSha256: corpusCoverage.sourceObservationSha256, snapshotRoot: corpusCoverage.snapshotRoot,
54
+ releaseIdentity: { version, tag: `v${version}`, sourceSnapshot },
55
+ corpusSeed: { tag: corpusSeed.tag, archiveSha256: corpusSeed.archiveSha256,
56
+ archiveBytes: corpusSeed.archiveBytes, receiptSha256: baselineReceiptSha256 },
57
+ corpusCoverage: { sha256: sha256(Buffer.from(`${JSON.stringify(corpusCoverage, null, 2)}\n`)), coverageGeneration: corpusCoverage.coverageGeneration },
58
+ generationLedger: { file: 'PUBLIC-RVF-GENERATIONS.json', sha256: sha256(generationLedgerBytes(ledger)),
59
+ bytes: generationLedgerBytes(ledger).length, storeCount: Object.keys(ledger.stores).length },
60
+ installedProjectionSchema: 2, policy: corpusCoverage.policy, enumerationReceipt: corpusCoverage.enumerationReceipt,
61
+ rows: corpusCoverage.rows, totals: corpusCoverage.totals,
62
+ };
63
+ const inventory = validatePublicInventory({ assetsDir: assets, coverage: releaseBase, ledger });
64
+ releaseBase.publicInventoryPartitionSha256 = inventory.partitionSha256;
65
+ releaseBase.releaseCoverageGeneration = releaseCoverageGenerationFor(releaseBase);
66
+ const out = path.resolve(outDir);
67
+ fs.mkdirSync(out, { recursive: true });
68
+ const corpusBytes = Buffer.from(`${JSON.stringify(corpusCoverage, null, 2)}\n`);
69
+ const coverageBytes = Buffer.from(`${JSON.stringify(releaseBase, null, 2)}\n`);
70
+ fs.writeFileSync(path.join(out, 'CORPUS-COVERAGE.json'), corpusBytes);
71
+ fs.writeFileSync(path.join(out, 'COVERAGE.json'), coverageBytes);
72
+ fs.writeFileSync(path.join(out, 'PUBLIC-RVF-GENERATIONS.json'), generationLedgerBytes(ledger));
73
+ return releaseBase;
74
+ }
75
+
76
+ if (path.resolve(process.argv[1] || '') === fileURLToPath(import.meta.url)) {
77
+ try {
78
+ const corpusFile = path.resolve(arg('--corpus', path.join(ROOT, 'data', 'source-coverage.json')));
79
+ const corpus = readJson(corpusFile);
80
+ const projection = createReleaseProjection({ corpusCoverage: corpus,
81
+ assetsDir: arg('--assets'), outDir: arg('--out', 'release-evidence'), version: arg('--version'),
82
+ sourceSnapshot: arg('--source-snapshot'), baselineReceiptSha256: arg('--baseline-receipt-sha256'),
83
+ corpusSeed: { tag: arg('--seed-tag'), archiveSha256: arg('--seed-sha256'), archiveBytes: Number(arg('--seed-bytes')) } });
84
+ console.log(JSON.stringify({ ok: true, releaseCoverageGeneration: projection.releaseCoverageGeneration }));
85
+ } catch (error) { console.error(`[release-projection] ${error.message}`); process.exitCode = 1; }
86
+ }
@@ -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,18 +69,37 @@ 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
 
58
- export const canonicalJson = (value) => {
59
- if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
60
- if (value && typeof value === 'object') {
61
- return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
62
- }
63
- return JSON.stringify(value);
64
- };
65
-
79
+ /**
80
+ * THE DIGEST SERIALISER MUST AGREE WITH THE ONE THAT WRITES THE FILE.
81
+ *
82
+ * It did not, and it silently corrupted the terminal receipt of EVERY successful release. Measured
83
+ * 2026-08-20 on v4.0.36 and v4.0.90-dev — both `channels-converged` receipts, both digest=BAD, both
84
+ * with the same missing key:
85
+ *
86
+ * canonicalJson({a: undefined}) -> {"a":undefined} key KEPT (and not even valid JSON)
87
+ * JSON.stringify({a: undefined}) -> {} key DROPPED
88
+ *
89
+ * The converge observation carries optional fields — `hosts.verifier.error`, `claudeOnly`,
90
+ * `codexOnly`, `dual` — and on a clean run `verified.error` is `undefined`. So the digest was
91
+ * computed over `…"error":undefined…` while the bytes written omitted `error` entirely: a digest
92
+ * over a shape that can never be read back. `runReleaseTransaction` appends a receipt, reads it
93
+ * back and verifies it, so the publish then died with `release receipt digest mismatch` AFTER npm
94
+ * and GitHub had both been promoted — the release shipped and the rail reported failure.
95
+ *
96
+ * Proven by reconstruction: restoring `observation.hosts.verifier.error = undefined` on the stored
97
+ * receipt makes `digestReceipt` reproduce the stored digest exactly.
98
+ *
99
+ * Arrays have the same asymmetry (`JSON.stringify([undefined])` is `[null]`), so both are matched
100
+ * here. This does not change the digest of any receipt that never held an undefined value, which is
101
+ * every receipt that currently verifies.
102
+ */
66
103
  export const receiptPayload = (receipt) => {
67
104
  const { signature: _signature, receiptDigest: _digest, ...payload } = receipt;
68
105
  return canonicalJson(payload);
@@ -86,6 +123,8 @@ export const transactionIdFor = (identity) => crypto.createHash('sha256').update
86
123
  bundleSha256: identity.bundleSha256,
87
124
  bundleSignatureSha256: identity.bundleSignatureSha256 || null,
88
125
  bundleDigestSha256: identity.bundleDigestSha256 || null,
126
+ ...(identity.corpusSeedSha256 ? { corpusSeedSha256: identity.corpusSeedSha256 } : {}),
127
+ ...(identity.generationLedgerSha256 ? { generationLedgerSha256: identity.generationLedgerSha256 } : {}),
89
128
  })).digest('hex');
90
129
 
91
130
  export function signReceipt(receipt, privateKey) {
@@ -97,7 +136,8 @@ export function signReceipt(receipt, privateKey) {
97
136
  }
98
137
 
99
138
  export function verifyReceipt(receipt, publicKey) {
100
- 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 || '')) {
101
141
  throw new Error('invalid release transaction receipt');
102
142
  }
103
143
  const { signature, receiptDigest, ...unsigned } = receipt;
@@ -121,13 +161,20 @@ export function validateReceiptChain(receipts, identity, publicKey) {
121
161
  if ((receipt.previousReceiptDigest || null) !== (previous?.receiptDigest || null)) {
122
162
  throw new Error('release receipt chain conflict');
123
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
+ }
124
171
  previous = receipt;
125
172
  }
126
173
  return sorted;
127
174
  }
128
175
 
129
176
  const stateReceipt = ({ identity, prior, state, observation = {}, privateKey }) => signReceipt({
130
- schemaVersion: 2,
177
+ schemaVersion: CURRENT_RECEIPT_SCHEMA_VERSION,
131
178
  transactionId: transactionIdFor(identity),
132
179
  sequence: prior ? prior.sequence + 1 : 0,
133
180
  previousReceiptDigest: prior?.receiptDigest || null,
@@ -175,11 +222,15 @@ export function reduceReleaseState({ lastReceipt, snapshot, identity, prior }) {
175
222
  }
176
223
  if (npmB && snapshot.github?.draft === true) return { action: 'compensate-npm' };
177
224
  if (npmB && githubLatest) {
178
- if (lastReceipt.state === 'channels-converged') {
225
+ if (lastReceipt.state === 'install-verified') {
179
226
  return githubPublished && assetsExact && candidateExact
180
- && snapshot.publicReceiptExact && snapshot.publicHostsExact
181
227
  ? { action: 'complete' }
182
- : { 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' };
183
234
  }
184
235
  return { action: 'finalize' };
185
236
  }
@@ -229,7 +280,7 @@ export async function pollObservation(read, predicate, {
229
280
  throw new Error(`visibility deadline exceeded after ${now() - started}ms: ${detail}`);
230
281
  }
231
282
 
232
- export async function runReleaseTransaction({ identity, assets, adapter, privateKey, publicKey, hostVerifier }) {
283
+ export async function runReleaseTransaction({ identity, assets, adapter, privateKey, publicKey }) {
233
284
  const expectedId = transactionIdFor(identity);
234
285
  const discovered = await adapter.discover(identity);
235
286
  const competing = discovered.pending?.filter((item) => item.transactionId !== expectedId) || [];
@@ -241,7 +292,8 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
241
292
  const draft = discovered.matchingDrafts?.[0] || await adapter.createDraft(identity);
242
293
  const append = async (state, observation = {}) => {
243
294
  const recoveryCompensation = state === 'compensation-intent' && current
244
- && !TERMINAL_STATES.has(current.state) && current.state !== 'manual-intervention-required';
295
+ && !isClosedReceipt(current) && current.state !== 'manual-intervention-required'
296
+ && current.state !== 'channels-converged';
245
297
  if (current && !ALLOWED_TRANSITIONS[current.state]?.includes(state) && current.state !== state
246
298
  && !recoveryCompensation) {
247
299
  throw new Error(`illegal release transition ${current.state} -> ${state}`);
@@ -270,10 +322,10 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
270
322
  const snapshot = await adapter.observeSnapshot(identity, draft, {
271
323
  forceAssets: current.state === 'finalize-intent' || current.state === 'channels-converged',
272
324
  });
273
- if (current.state === 'channels-converged') {
274
- const terminal = reduceReleaseState({ lastReceipt: current, snapshot, identity, prior });
275
- if (terminal.action === 'complete') return current;
276
- 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}`);
277
329
  }
278
330
  // A process may die after the provider side effect and before its observation receipt. Rebuild
279
331
  // that missing receipt from fresh state before asking the reducer for the next command.
@@ -304,11 +356,11 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
304
356
  await transition('compensated', { npm: snapshot.npm, github: snapshot.github, recovered: true });
305
357
  throw new Error('npm compensation recovered; resume the same transaction');
306
358
  }
307
- if (current.state === 'finalize-intent' && snapshot.publicReceiptExact && snapshot.publicHostsExact
308
- && npmCandidateExact(snapshot, identity) && npmLatestIsB(snapshot, identity)
359
+ if (current.state === 'finalize-intent' && npmCandidateExact(snapshot, identity) && npmLatestIsB(snapshot, identity)
309
360
  && githubIsB(snapshot, identity) && snapshot.github?.published && snapshot.github?.latest
310
361
  && snapshot.github?.assetsExact) {
311
- 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 });
312
364
  }
313
365
  const decision = reduceReleaseState({ lastReceipt: current, snapshot, identity, prior });
314
366
  const stageRecord = {
@@ -323,6 +375,7 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
323
375
  ? `::notice title=Release stage ${decision.action}::${JSON.stringify(stageRecord)}`
324
376
  : JSON.stringify(stageRecord));
325
377
  if (decision.action === 'complete') return current;
378
+ if (decision.action === 'await-install-verification') return current;
326
379
  if (decision.action === 'manual') {
327
380
  await transition('manual-intervention-required', { reason: decision.reason, snapshot });
328
381
  throw new Error(`release requires manual intervention: ${decision.reason}`);
@@ -415,32 +468,15 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
415
468
  await transition('defaults-promoted', { npm: snapshot.npm, github: snapshot.github });
416
469
  }
417
470
  await transition('finalize-intent');
418
- const final = await adapter.finalize(identity, current, hostVerifier);
419
- // SAY WHICH CONVERGENCE FAILED, AND WHY (2026-08-07). This threw the bare sentence
420
- // `final release convergence failed` while `finalize` had already returned exactly the reason
421
- // — hostVerifierError, hosts.verifier.error, publicationError or sealError — and the throw
422
- // discarded every one of them. That is the same defect that let `spawnSync ENOBUFS` masquerade
423
- // as `staged GitHub payload mismatch` for three days: an error that reports its category and
424
- // withholds its evidence. Publication had ALREADY succeeded on both channels here, so the
425
- // operator is reading this line while npm and GitHub are correct, with nothing to act on.
426
- if (final.verdict !== 'PASS') {
427
- const why = final.hostVerifierError
428
- || final.hosts?.verifier?.error
429
- || final.publicationError
430
- || final.sealError
431
- || `verdict=${final.verdict ?? '(none)'}`;
432
- const fixtures = final.hosts?.verifier?.fixtures;
433
- const detail = fixtures ? ` | fixtures: ${JSON.stringify(fixtures).slice(0, 400)}` : '';
434
- throw new Error(`final release convergence failed: ${why}${detail}`);
435
- }
436
471
  const reobserved = await adapter.observeSnapshot(identity, draft, { forceAssets: true });
437
472
  if (!(npmLatestIsB(reobserved, identity) && reobserved.github?.latest && githubIsB(reobserved, identity))) {
438
- throw new Error('provider defaults drifted during finalization');
473
+ throw new Error('provider defaults drifted before channel convergence');
439
474
  }
440
- if (!reobserved.github?.assetsExact || !reobserved.publicReceiptExact || !reobserved.publicHostsExact) {
441
- 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');
442
477
  }
443
- return append('channels-converged', final);
478
+ return append('channels-converged', { verdict: 'PUBLISHED_NOT_VERIFIED',
479
+ npm: reobserved.npm, github: reobserved.github });
444
480
  }
445
481
  }
446
482
  throw new Error('release reducer exceeded its bounded transition count');
@@ -450,7 +486,9 @@ export async function abortReleaseTransaction({ identity, receipts, reason, auth
450
486
  if (!authorized) throw new Error('release abort requires explicit human authorization');
451
487
  const chain = validateReceiptChain(receipts, identity, publicKey);
452
488
  const current = chain.at(-1);
453
- 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
+ }
454
492
  const snapshot = await adapter.observeSnapshot(identity);
455
493
  const prior = chain[0]?.observation?.prior;
456
494
  if (!prior || snapshot.npm?.latestVersion === identity.version || snapshot.github?.latest === true
@@ -464,3 +502,81 @@ export async function abortReleaseTransaction({ identity, receipts, reason, auth
464
502
  await adapter.appendReceipt(null, receipt, `${RECEIPT_PREFIX}${String(receipt.sequence).padStart(4, '0')}.json`);
465
503
  return receipt;
466
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
+ }