ruvnet-brain 4.3.9 → 4.3.11

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 (87) hide show
  1. package/README.md +26 -9
  2. package/bin/install.mjs +646 -366
  3. package/bin/nightly-refresh.mjs +115 -0
  4. package/kb/brain-profile.mjs +118 -32
  5. package/kb/lifecycle-evidence-retention.mjs +237 -0
  6. package/kb/refresh-run.mjs +367 -0
  7. package/kb/retrieval-result.mjs +39 -0
  8. package/kb/update-storage-transaction.mjs +439 -0
  9. package/package.json +12 -3
  10. package/plugin/.claude-plugin/plugin.json +1 -1
  11. package/plugin/.codex-plugin/plugin.json +1 -1
  12. package/plugin/hooks/codex-hooks.json +2 -165
  13. package/plugin/hooks/hook-contracts.json +4 -65
  14. package/plugin/hooks/hooks.json +2 -208
  15. package/plugin/scripts/adr-currency-gate.mjs +29 -19
  16. package/plugin/scripts/capability-registry.mjs +10 -83
  17. package/plugin/scripts/codex-hook-adapter.mjs +62 -9
  18. package/plugin/scripts/codex-hook-wrapper.mjs +19 -1
  19. package/plugin/scripts/continuation-gate.mjs +30 -28
  20. package/plugin/scripts/continuation-objective.mjs +34 -0
  21. package/plugin/scripts/development-maintenance.mjs +49 -0
  22. package/plugin/scripts/hook-shim.mjs +3 -0
  23. package/plugin/scripts/learn-flush.mjs +6 -1
  24. package/plugin/scripts/lesson-gate.mjs +5 -2
  25. package/plugin/scripts/lesson-presentation.mjs +5 -0
  26. package/plugin/scripts/md-stamp.mjs +109 -6
  27. package/plugin/scripts/memory-doctor.mjs +3 -7
  28. package/plugin/scripts/nightly-controller.mjs +9 -27
  29. package/plugin/scripts/nightly-scheduler.mjs +494 -0
  30. package/plugin/scripts/project-progression-contract.mjs +4 -4
  31. package/plugin/scripts/project-progression-store.mjs +35 -6
  32. package/plugin/scripts/ruflo-bin.mjs +20 -0
  33. package/plugin/scripts/session-snapshot-contract.mjs +6 -1
  34. package/plugin/scripts/version-bump-gate.sh +14 -2
  35. package/plugin/skills/release-proof/SKILL.md +6 -2
  36. package/plugin/skills/release-proof/references/receipt-contract.md +31 -2
  37. package/plugin/skills/release-proof/scripts/release-proof.mjs +11 -2
  38. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +15 -23
  39. package/scripts/build-bundle.mjs +8 -0
  40. package/scripts/build-primer.mjs +2 -3
  41. package/scripts/candidate-host-evidence.mjs +59 -41
  42. package/scripts/ci/build-fixture-kb.mjs +59 -2
  43. package/scripts/ci/mutate-hook-timeout.mjs +24 -47
  44. package/scripts/ci/stranger-scenario.mjs +13 -28
  45. package/scripts/claims-verify.mjs +219 -21
  46. package/scripts/console-engine.mjs +4 -18
  47. package/scripts/console-runtime-identity.mjs +6 -0
  48. package/scripts/development-maintenance.mjs +47 -0
  49. package/scripts/development-push-check.mjs +35 -0
  50. package/scripts/distill-project.mjs +24 -47
  51. package/scripts/doc-currency.mjs +91 -19
  52. package/scripts/git-hooks/pre-push +4 -162
  53. package/scripts/health-repair.mjs +22 -4
  54. package/scripts/hook-retirement-check.mjs +22 -0
  55. package/scripts/host-install-matrix.mjs +125 -61
  56. package/scripts/integration-evidence.mjs +11 -6
  57. package/scripts/learning-replay-fixture.mjs +23 -2
  58. package/scripts/nightly-package-observation.mjs +67 -0
  59. package/scripts/nightly-two-run-proof.mjs +489 -0
  60. package/scripts/nightly-watchdog.mjs +17 -2
  61. package/scripts/npm-invocation.mjs +20 -0
  62. package/scripts/prepublication-evidence.mjs +42 -6
  63. package/scripts/primer-grounding.mjs +20 -0
  64. package/scripts/product-integrity-contract.mjs +22 -2
  65. package/scripts/public-verification-abandon.mjs +101 -0
  66. package/scripts/public-verification-aggregate.mjs +61 -19
  67. package/scripts/public-verification-finalizer.mjs +8 -2
  68. package/scripts/public-verification-lane.mjs +64 -8
  69. package/scripts/publication-receipt.mjs +156 -50
  70. package/scripts/published-surface-probe.mjs +10 -2
  71. package/scripts/qa-contract.mjs +57 -0
  72. package/scripts/qa-lanes.mjs +40 -0
  73. package/scripts/qa-runner.mjs +54 -64
  74. package/scripts/qe/ux-suite.mjs +10 -34
  75. package/scripts/qualified-candidate-check.mjs +124 -0
  76. package/scripts/release-abort-stale.mjs +2 -2
  77. package/scripts/release-projection.mjs +20 -1
  78. package/scripts/release-qualification-contract.mjs +87 -0
  79. package/scripts/release-qualification.mjs +135 -0
  80. package/scripts/release-transaction.mjs +81 -1
  81. package/scripts/remedy-registry.mjs +6 -13
  82. package/scripts/retrieval-canary.mjs +91 -18
  83. package/scripts/selfcheck.mjs +12 -4
  84. package/scripts/snapshot-freshness.mjs +58 -0
  85. package/scripts/stack-sync.mjs +41 -43
  86. package/scripts/staged-host-verifier.mjs +63 -14
  87. package/scripts/wired-check.mjs +31 -2
@@ -5,7 +5,10 @@ import path from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { aggregateEvidence } from './release-evidence-aggregate.mjs';
7
7
  import { canonicalJson } from './release-transaction.mjs';
8
- import { EXCLUSION_POLICY } from './integration-evidence.mjs';
8
+ import { RELEASE_QUALIFICATION_POLICY } from './integration-evidence.mjs';
9
+ import { readCandidateRetrieval } from './staged-host-verifier.mjs';
10
+ import { validateRetrievalCanaryReceipt } from './retrieval-canary.mjs';
11
+ import { payloadIdFor } from './release-payload.mjs';
9
12
 
10
13
  const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
11
14
 
@@ -44,10 +47,13 @@ export function buildPrepublicationEvidence({
44
47
  manifestFile,
45
48
  payloadProofFile,
46
49
  hostFile,
50
+ runtimeCensusFile,
47
51
  ciFile,
48
52
  integrationFile,
49
53
  uxFiles,
50
54
  strangerFile,
55
+ planFile,
56
+ coverageFile,
51
57
  }) {
52
58
  if (!/^[a-f0-9]{40}$/.test(sha || '') || !/^\d+\.\d+\.\d+$/.test(version || '') || !Number.isSafeInteger(runId) || runId <= 0) {
53
59
  throw new Error('prepublication identity is malformed');
@@ -60,10 +66,30 @@ export function buildPrepublicationEvidence({
60
66
  throw new Error('candidate manifest identity mismatch');
61
67
  }
62
68
  if (!/^[a-f0-9]{64}$/.test(payload.payloadId || '')) throw new Error('payload identity is malformed');
69
+ if (payload.payloadId !== payloadIdFor(manifest)) throw new Error('candidate manifest payload identity mismatch');
70
+ const retrieval = readCandidateRetrieval({ manifest, planFile, coverageFile });
71
+
72
+ const runtimeCensus = readJson(runtimeCensusFile);
73
+ const census = runtimeCensus.value;
74
+ const result = census.result;
75
+ const artifacts = result?.provenance?.artifacts;
76
+ const candidate = result?.provenance?.candidate;
77
+ const memberHash = (role) => manifest.members.find((member) => member.role === role)?.sha256;
78
+ if (census.schemaVersion !== 1 || census.kind !== 'ruvnet-brain-runtime-census-evidence'
79
+ || census.sourceSha !== sha || census.version !== version || census.runId !== runId
80
+ || census.payloadId !== payload.payloadId || census.payloadManifestSha256 !== sha256(manifestBytes)
81
+ || result?.status !== 'PASS' || result.diagnostic !== false || result.qualification !== 'staged-candidate'
82
+ || result.provenance?.projectionVerified !== true || candidate?.sourceSha !== sha || candidate?.version !== version
83
+ || candidate?.coverageSha256 !== sha256(fs.readFileSync(coverageFile))
84
+ || artifacts?.payloadId !== payload.payloadId || artifacts.claimSourceSha !== sha
85
+ || artifacts.packageSha256 !== memberHash('npm') || artifacts.bundleSha256 !== memberHash('bundle')
86
+ || !(artifacts.sidecarsVerified > 0) || !(artifacts.surfacesVerified > 0)) {
87
+ throw new Error('runtime census receipt is not an exact candidate-bound PASS');
88
+ }
63
89
 
64
90
  const host = readJson(hostFile);
65
91
  if (host.value.schemaVersion !== 1 || host.value.sha !== sha || host.value.payloadId !== payload.payloadId
66
- || !/^[a-f0-9]{64}$/.test(host.value.artifactSha256 || '') || host.value.leaves?.length !== 3) {
92
+ || host.value.artifactSha256 !== retrieval.artifactSha256 || host.value.leaves?.length !== 3) {
67
93
  throw new Error('candidate host evidence identity or shape mismatch');
68
94
  }
69
95
  requireExactSet(host.value.leaves.map(({ name }) => name), ['claude-only', 'codex-only', 'dual-host'], 'candidate host leaves');
@@ -77,10 +103,11 @@ export function buildPrepublicationEvidence({
77
103
  || leaf.artifactSha256 !== host.value.artifactSha256) {
78
104
  throw new Error(`candidate host leaf is not an exact PASS: ${leaf.name || '(missing)'}`);
79
105
  }
106
+ validateRetrievalCanaryReceipt(leaf.retrieval, { plan: retrieval.plan });
80
107
  }
81
108
 
82
109
  const ci = readJson(ciFile);
83
- const requiredCiJobs = ['candidate-preflight', 'check', 'windows-unit', 'warm-brain', 'release-qe'];
110
+ const requiredCiJobs = ['candidate-preflight', 'release-acceptance-linux', 'release-acceptance-windows', 'release-acceptance-macos', 'release-qe'];
84
111
  if (ci.value.schemaVersion !== 1 || ci.value.kind !== 'ruvnet-brain-candidate-ci-evidence'
85
112
  || ci.value.sourceSha !== sha || ci.value.version !== version || ci.value.payloadId !== payload.payloadId
86
113
  || ci.value.payloadManifestSha256 !== sha256(manifestBytes) || ci.value.workflow !== 'ci'
@@ -89,6 +116,10 @@ export function buildPrepublicationEvidence({
89
116
  }
90
117
  requireExactSet(ci.value.jobs?.map(({ name }) => name) || [], requiredCiJobs, 'candidate CI jobs');
91
118
  if (ci.value.jobs.some(({ conclusion }) => conclusion !== 'success')) throw new Error('candidate CI receipt contains a non-success job');
119
+ requireExactSet(ci.value.acceptanceReceipts?.map(({ platform }) => platform) || [], ['linux', 'macos', 'windows'], 'release acceptance platforms');
120
+ if (ci.value.acceptanceReceipts.some(row => row.sourceSha !== sha || !(row.passed > 0)
121
+ || !/^[a-f0-9]{64}$/.test(row.receiptSha256 || ''))) throw new Error('release acceptance receipt identity mismatch');
122
+
92
123
 
93
124
  const integration = readJson(integrationFile);
94
125
  if (integration.value.schemaVersion !== 1 || integration.value.kind !== 'ruvnet-brain-integration-evidence'
@@ -102,7 +133,9 @@ export function buildPrepublicationEvidence({
102
133
  || integration.value.skippedTests.some((name) => typeof name !== 'string' || !name.trim())) {
103
134
  throw new Error('integration receipt is not an exact, fully accounted PASS');
104
135
  }
105
- if (integration.value.exclusionPolicy !== EXCLUSION_POLICY
136
+ if (integration.value.exclusionPolicy !== RELEASE_QUALIFICATION_POLICY
137
+ || integration.value.skipped !== 0 || integration.value.todo !== 0
138
+ || !/^[a-f0-9]{64}$/.test(integration.value.qualificationReceiptSha256 || '')
106
139
  || !Array.isArray(integration.value.todoTests)
107
140
  || integration.value.todoTests.length !== Number(integration.value.todo || 0)
108
141
  || !/^[a-f0-9]{64}$/.test(integration.value.exclusionsSha256 || '')) {
@@ -133,9 +166,9 @@ export function buildPrepublicationEvidence({
133
166
 
134
167
  const common = { sha, payloadId: payload.payloadId, runId };
135
168
  const leaves = [
136
- passLeaf({ ...common, name: 'source-quality', source: 'candidate-ci-receipt:check', receiptSha256: ci.digest }),
169
+ passLeaf({ ...common, name: 'source-quality', source: 'candidate-ci-receipt:release-acceptance-linux', receiptSha256: ci.digest }),
137
170
  passLeaf({ ...common, name: 'ux-qe', source: 'ux-qe-receipts:darwin,linux,win32', receiptSha256: uxDigest }),
138
- passLeaf({ ...common, name: 'release-qe', source: 'candidate-ci-receipt:release-qe', receiptSha256: ci.digest }),
171
+ passLeaf({ ...common, name: 'release-qe', source: 'candidate-ci-receipt:release-qe', receiptSha256: ci.digest, runtimeCensusSha256: runtimeCensus.digest }),
139
172
  passLeaf({ ...common, name: 'integration-linux', source: 'integration-receipt', receiptSha256: integration.digest,
140
173
  skipped: integration.value.skipped, todo: integration.value.todo }),
141
174
  passLeaf({ ...common, name: 'stranger-linux', source: 'stranger-receipt:ubuntu', receiptSha256: stranger.digest }),
@@ -161,6 +194,9 @@ if (isCli) {
161
194
  manifestFile: path.resolve(arg('--manifest')),
162
195
  payloadProofFile: path.resolve(arg('--payload-proof')),
163
196
  hostFile: path.resolve(arg('--hosts')),
197
+ runtimeCensusFile: path.resolve(arg('--runtime-census')),
198
+ planFile: arg('--plan'),
199
+ coverageFile: arg('--coverage'),
164
200
  ciFile: path.resolve(arg('--ci')),
165
201
  integrationFile: path.resolve(arg('--integration')),
166
202
  uxFiles,
@@ -0,0 +1,20 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ // Citation presence is an admission check, not semantic verification of each generated claim.
5
+ export function countPrimerReferences(primer, sourcePaths) {
6
+ const paths = [...new Set(sourcePaths)].filter((value) => typeof value === 'string' && value);
7
+ const citations = new Set([...primer.matchAll(/`([^`\n]+)`/g)].map((match) => match[1]));
8
+ const basenames = new Map();
9
+ for (const file of paths) basenames.set(path.basename(file), (basenames.get(path.basename(file)) || 0) + 1);
10
+ return paths.filter((file) => citations.has(file)
11
+ || (basenames.get(path.basename(file)) === 1 && citations.has(path.basename(file))));
12
+ }
13
+
14
+ export function writeGroundedPrimer({ primer, sourcePaths, output, minimum = 6 }) {
15
+ if (!Number.isInteger(minimum) || minimum < 1) throw new Error('invalid primer citation minimum');
16
+ const refs = countPrimerReferences(primer, sourcePaths);
17
+ if (refs.length < minimum) throw new Error(`THIN primer: ${refs.length} distinct source citations; requires ${minimum}. Existing primer preserved.`);
18
+ fs.writeFileSync(output, primer);
19
+ return refs;
20
+ }
@@ -43,7 +43,7 @@ const make = (id, statement, owner, contributors, implementation, tests, receipt
43
43
  export const PRODUCT_INTEGRITY_OBLIGATIONS = Object.freeze([
44
44
  make('S-1', 'One complete public corpus', 'CorpusGeneration', ['SourceCoverage'], ['scripts/source-coverage.mjs', 'scripts/corpus-reconcile.mjs', 'scripts/corpus-candidate.mjs'], ['tests/unit/source-coverage.test.mjs', 'tests/unit/corpus-reconcile.test.mjs'], ['ruvnet-brain-source-observation', 'ruvnet-brain-corpus-candidate']),
45
45
  make('S-2', 'One immutable public release projection', 'ReleaseProjection', ['CorpusGeneration'], ['scripts/build-bundle.mjs', 'scripts/coverage-integrity.mjs', 'scripts/public-inventory.mjs'], ['tests/integration/build-bundle-fence.test.mjs', 'tests/unit/public-inventory.test.mjs'], ['ruvnet-brain-release-coverage']),
46
- make('S-3', 'Recall at 10 is at least 98 percent and delta citations are complete', 'PublicVerification', ['SourceCoverage', 'CorpusGeneration'], ['scripts/retrieval-canary.mjs', 'scripts/packed-retrieval-canary.mjs'], ['tests/unit/retrieval-canary.test.mjs', 'tests/unit/packed-retrieval-canary.test.mjs'], ['ruvnet-brain-retrieval-canary-plan', 'ruvnet-brain-retrieval-canary-receipt']),
46
+ make('S-3', 'Recall at 10 is at least 98 percent and delta citations are complete', 'PublicVerification', ['SourceCoverage', 'CorpusGeneration'], ['scripts/retrieval-canary.mjs', 'scripts/candidate-host-evidence.mjs', 'scripts/prepublication-evidence.mjs', 'scripts/host-install-matrix.mjs', 'kb/retrieval-result.mjs'], ['tests/unit/retrieval-canary.test.mjs', 'tests/unit/candidate-host-evidence.test.mjs', 'tests/unit/candidate-retrieval-matrix.test.mjs', 'tests/unit/retrieval-result-boundary.test.mjs'], ['ruvnet-brain-retrieval-canary-plan', 'ruvnet-brain-retrieval-canary-receipt']),
47
47
  make('S-4', 'Native nightly runs are ordered, idempotent, and retained within budget', 'RefreshLifecycle', [], ['bin/nightly-refresh.mjs', 'plugin/scripts/nightly-scheduler.mjs', 'scripts/nightly-two-run-proof.mjs', 'kb/refresh-run.mjs'], ['tests/unit/nightly-scheduler.test.mjs', 'tests/unit/nightly-two-run-proof.test.mjs'], ['ruvnet-brain-refresh-run', 'ruvnet-brain-native-two-run-nightly-proof']),
48
48
  make('S-5', 'One active corpus preserves private stores and bounded recovery', 'RefreshLifecycle', ['CorpusGeneration'], ['kb/update-storage-transaction.mjs', 'kb/forge-update.mjs', 'kb/brain-profile.mjs', 'bin/install.mjs'], ['tests/unit/update-storage-transaction.test.mjs', 'tests/unit/brain-profile.test.mjs'], ['ruvnet-brain-update-storage-transaction', 'ruvnet-brain-installed-profile']),
49
49
  make('S-6', 'Every supported OS and host loader converges on exact public bytes', 'HostConvergence', ['PublicVerification'], ['scripts/host-registry.mjs', 'scripts/host-install-matrix.mjs', 'scripts/public-verification-aggregate.mjs'], ['tests/unit/host-registry.test.mjs', 'tests/integration/dual-host-install.test.mjs'], ['ruvnet-brain-host-registry', 'ruvnet-brain-public-verification-leaf']),
@@ -141,9 +141,29 @@ export function renderProductIntegrityTraceMarkdown(contract = validateProductIn
141
141
  }
142
142
  const valueAfter = (argv, flag) => { const index = argv.indexOf(flag); return index >= 0 ? argv[index + 1] : null; };
143
143
  export function runProductIntegrityCli(argv = process.argv.slice(2), io = { stdout: process.stdout }) {
144
- const known = new Set(['--markdown', '--check-markdown', '--trace', '--source-sha', '--out', '--verify-trace']);
144
+ const known = new Set(['--markdown', '--check-markdown', '--check-source', '--trace', '--source-sha', '--out', '--verify-trace']);
145
145
  if (argv.some((arg) => arg.startsWith('--') && !known.has(arg))) throw new Error('unknown product integrity argument');
146
146
  if (!argv.length) { io.stdout.write(`${canonicalJson(validateProductIntegrityContract())}\n`); return 0; }
147
+ if (argv.length === 1 && argv[0] === '--check-source') {
148
+ const root = fs.realpathSync(io.root || fileURLToPath(new URL('..', import.meta.url)));
149
+ const missing = [], invalid = [];
150
+ const files = productIntegrityGovernedPaths();
151
+ for (const file of files) {
152
+ const absolute = path.join(root, file);
153
+ try {
154
+ const stat = fs.lstatSync(absolute);
155
+ const relative = path.relative(root, fs.realpathSync(absolute));
156
+ if (!stat.isFile() || stat.isSymbolicLink() || relative.startsWith(`..${path.sep}`)
157
+ || relative === '..' || path.isAbsolute(relative)) invalid.push(file);
158
+ } catch (error) {
159
+ (error.code === 'ENOENT' ? missing : invalid).push(file);
160
+ }
161
+ }
162
+ const verdict = missing.length || invalid.length ? 'FAIL' : 'PASS';
163
+ io.stdout.write(`${canonicalJson({ verdict, scope: 'source-presence', behaviorVerified: false,
164
+ checked: files.length, missing, invalid })}\n`);
165
+ return verdict === 'PASS' ? 0 : 1;
166
+ }
147
167
  if (argv.length === 1 && argv[0] === '--markdown') { io.stdout.write(renderProductIntegrityTraceMarkdown()); return 0; }
148
168
  const check = valueAfter(argv, '--check-markdown');
149
169
  if (check) { if (fs.readFileSync(path.resolve(check), 'utf8') !== renderProductIntegrityTraceMarkdown()) throw new Error('generated traceability Markdown differs'); io.stdout.write('product integrity Markdown: PASS\n'); return 0; }
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { execFileSync } from 'node:child_process';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { extractZip } from '../kb/zip-extract.mjs';
9
+ import { abandonPublicVerificationTransaction } from './release-transaction.mjs';
10
+ import { liveReleaseProvider } from './release-transaction-provider.mjs';
11
+
12
+ function regularJson(file, max = 131072) {
13
+ const stat = fs.lstatSync(file);
14
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > max) throw new Error('untrusted or oversized closure input');
15
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
16
+ }
17
+ const api = (endpoint) => JSON.parse(execFileSync('gh', ['api', endpoint], { encoding: 'utf8', timeout: 30000 }));
18
+
19
+ export function closureProvenance({ event, env, identity, failure, run, artifact, workflow }) {
20
+ const input = event.client_payload;
21
+ if (env.GITHUB_EVENT_NAME !== 'repository_dispatch' || event.action !== 'abandon-public-verification'
22
+ || event.sender?.login !== env.GITHUB_ACTOR || event.repository?.full_name !== identity.repository
23
+ || env.GITHUB_REPOSITORY !== identity.repository || input?.authorization !== 'abandon-published-not-verified') {
24
+ throw new Error('closure requires the explicitly authorized abandonment dispatch');
25
+ }
26
+ if (run.event !== 'repository_dispatch' || !Number.isSafeInteger(run.repository?.id)
27
+ || run.repository.id !== event.repository.id || run.head_repository?.id !== run.repository.id
28
+ || run.head_repository?.full_name !== identity.repository
29
+ || !Number.isSafeInteger(workflow?.id) || run.workflow_id !== workflow.id || workflow.path !== run.path
30
+ || run.id !== input.recovery_run_id || run.status !== 'completed' || run.conclusion !== 'failure'
31
+ || run.path !== '.github/workflows/recover-public-verification.yml'
32
+ || run.repository?.full_name !== identity.repository || !/^[a-f0-9]{40}$/.test(run.head_sha || '')
33
+ || artifact.id !== input.failure_artifact_id || artifact.expired !== false
34
+ || artifact.workflow_run?.id !== run.id || artifact.workflow_run?.head_sha !== run.head_sha
35
+ || artifact.name !== `recovered-public-verification-${failure.os}-${identity.candidateSha}-${run.id}`) {
36
+ throw new Error('GitHub failed-run artifact provenance mismatch');
37
+ }
38
+ return {
39
+ expected: input.expected,
40
+ reason: input.reason,
41
+ authorization: { actor: env.GITHUB_ACTOR, reference: input.authorization_reference,
42
+ event: env.GITHUB_EVENT_NAME, action: event.action,
43
+ dispatchRunId: Number(env.GITHUB_RUN_ID), verifierSha: env.GITHUB_SHA },
44
+ recovery: { repository: identity.repository, workflow: run.path, workflowId: workflow.id, repositoryId: run.repository.id, event: run.event, runId: run.id,
45
+ verifierSha: run.head_sha, conclusion: run.conclusion, artifactId: artifact.id, artifactName: artifact.name },
46
+ };
47
+ }
48
+
49
+ // Download the named GitHub artifact independently; a local JSON lookalike is not provenance.
50
+ async function failureFromArtifact(repository, artifactId) {
51
+ if (!Number.isSafeInteger(artifactId) || artifactId <= 0) throw new Error('invalid failed artifact ID');
52
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'abandon-failed-artifact-'));
53
+ try {
54
+ const zip = path.join(temp, 'failure.zip');
55
+ fs.writeFileSync(zip, execFileSync('gh', ['api', `repos/${repository}/actions/artifacts/${artifactId}/zip`],
56
+ { encoding: null, timeout: 60000, maxBuffer: 2 * 1024 * 1024 }), { flag: 'wx' });
57
+ const output = path.join(temp, 'contents');
58
+ const extracted = await extractZip(zip, output);
59
+ if (extracted.entryNames.length !== 1 || !/^(linux|macos|windows)\.json$/.test(extracted.entryNames[0])) {
60
+ throw new Error('failure artifact must contain exactly one OS failure receipt');
61
+ }
62
+ return regularJson(path.join(output, extracted.entryNames[0]));
63
+ } finally { fs.rmSync(temp, { recursive: true, force: true }); }
64
+ }
65
+
66
+ export async function abandonPublicVerification({ identityFile, outputFile, env = process.env,
67
+ readApi = api, readFailure = failureFromArtifact, adapter = liveReleaseProvider({ root: process.cwd() }),
68
+ publicKeyFile = 'keys/ruvnet-brain-signing.pub.pem' } = {}) {
69
+ if (!identityFile || !outputFile || !env.GITHUB_EVENT_PATH || !env.RUVNET_SIGNING_KEY) {
70
+ throw new Error('identity, output, dispatch event, and signing key are required');
71
+ }
72
+ if (fs.existsSync(outputFile)) throw new Error('refusing to overwrite closure output');
73
+ const identity = regularJson(identityFile);
74
+ if (identity.repository !== 'stuinfla/ruvnet-brain' || identity.package !== 'ruvnet-brain') throw new Error('unexpected release repository');
75
+ const event = regularJson(env.GITHUB_EVENT_PATH);
76
+ const input = event.client_payload;
77
+ if (![input?.recovery_run_id, input?.failure_artifact_id].every((id) => Number.isSafeInteger(id) && id > 0)) {
78
+ throw new Error('explicit recovery run and failed artifact IDs are required');
79
+ }
80
+ const run = await readApi(`repos/${identity.repository}/actions/runs/${input.recovery_run_id}`);
81
+ const artifact = await readApi(`repos/${identity.repository}/actions/artifacts/${input.failure_artifact_id}`);
82
+ const workflow = await readApi(`repos/${identity.repository}/actions/workflows/recover-public-verification.yml`);
83
+ const failure = await readFailure(identity.repository, input.failure_artifact_id);
84
+ const provenance = closureProvenance({ event, env, identity, failure, run, artifact, workflow });
85
+ const receipt = await abandonPublicVerificationTransaction({ identity, failure, ...provenance, adapter,
86
+ privateKey: crypto.createPrivateKey(env.RUVNET_SIGNING_KEY),
87
+ publicKey: crypto.createPublicKey(fs.readFileSync(publicKeyFile, 'utf8')) });
88
+ fs.mkdirSync(path.dirname(path.resolve(outputFile)), { recursive: true });
89
+ fs.writeFileSync(outputFile, `${JSON.stringify(receipt, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
90
+ return receipt;
91
+ }
92
+
93
+ const direct = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
94
+ if (direct) {
95
+ const arg = (name) => { const index = process.argv.indexOf(name); return index < 0 ? null : process.argv[index + 1]; };
96
+ try {
97
+ const receipt = await abandonPublicVerification({ identityFile: arg('--identity'), outputFile: arg('--out') });
98
+ console.log(JSON.stringify({ state: receipt.state, verdict: receipt.observation.verdict,
99
+ transactionId: receipt.transactionId, sequence: receipt.sequence, receiptDigest: receipt.receiptDigest }));
100
+ } catch (error) { console.error(`public-verification-abandon: ${error.message}`); process.exitCode = 1; }
101
+ }
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
6
6
  import { canonicalJson, digest } from './coverage-integrity.mjs';
7
7
  import { retrievalOracleExpectationFromPlan, validateRetrievalOracleReview } from './independent-review-receipt.mjs';
8
8
  import { validateRetrievalCanaryPlan, validateRetrievalCanaryReceipt } from './retrieval-canary.mjs';
9
+ import { validateNightlyProofReceipt } from './nightly-two-run-proof.mjs';
9
10
 
10
11
  export const PUBLIC_VERIFICATION_OS = Object.freeze(['linux', 'macos', 'windows']);
11
12
  export const PUBLIC_VERIFICATION_MODES = Object.freeze(['claude', 'codex', 'dual']);
@@ -19,7 +20,10 @@ function unsignedLeaf(leaf) {
19
20
  return payload;
20
21
  }
21
22
 
22
- export function validatePublicVerificationLeaf(leaf) {
23
+ export function validatePublicVerificationLeaf(leaf, { publicKey } = {}) {
24
+ if (Object.hasOwn(leaf || {}, 'verifierSha') && !HEX40.test(String(leaf.verifierSha))) {
25
+ throw new Error('public verification leaf verifier SHA is invalid');
26
+ }
23
27
  if (leaf?.schemaVersion !== 1 || leaf?.kind !== 'ruvnet-brain-public-verification-leaf'
24
28
  || !PUBLIC_VERIFICATION_OS.includes(leaf.os) || !PUBLIC_VERIFICATION_MODES.includes(leaf.mode)
25
29
  || !HEX40.test(String(leaf.sourceSha || '')) || !HEX64.test(String(leaf.artifactSha256 || ''))
@@ -33,6 +37,14 @@ export function validatePublicVerificationLeaf(leaf) {
33
37
  if (leaf.publicBytes?.npmExact !== true || leaf.publicBytes?.githubExact !== true
34
38
  || leaf.publicBytes?.bundleExact !== true || leaf.installed?.version !== leaf.version
35
39
  || leaf.installed?.loaderVerified !== true) throw new Error(`${leaf.os}/${leaf.mode} public bytes or loader are unverified`);
40
+ if (leaf.acceptancePolicy !== undefined || leaf.searchTiming !== undefined) {
41
+ const timing = leaf.searchTiming;
42
+ if (leaf.acceptancePolicy !== 'stabilization-public-deadline-v1' || timing?.deadlineMs !== 30_000
43
+ || ![timing.firstSearchMs, timing.broadMs].every((value) => typeof value === 'number'
44
+ && Number.isFinite(value) && value >= 0 && value <= timing.deadlineMs)) {
45
+ throw new Error(`${leaf.os}/${leaf.mode} public search acceptance policy or timing is invalid`);
46
+ }
47
+ }
36
48
  if (leaf.coverage?.verified !== true || leaf.coverage.eligibleCurrent !== leaf.coverage.eligibleTotal
37
49
  || leaf.coverage.gistCurrent !== leaf.coverage.gistTotal) throw new Error(`${leaf.os}/${leaf.mode} coverage is incomplete`);
38
50
  if (!Array.isArray(leaf.untested) || leaf.untested.length || leaf.skipped !== 0 || leaf.unknown !== 0) {
@@ -49,12 +61,23 @@ export function validatePublicVerificationLeaf(leaf) {
49
61
  || retrieval.coverageGeneration !== leaf.coverageGeneration || retrieval.planSha256 !== leaf.canaryPlanSha256) {
50
62
  throw new Error(`${leaf.os}/${leaf.mode} retrieval identity differs`);
51
63
  }
64
+ if (leaf.mode === 'dual') {
65
+ if (!/^[1-9][0-9]*$/.test(String(leaf.workflowRunId || ''))) throw new Error('native nightly workflow run identity is missing');
66
+ const native = validateNightlyProofReceipt(leaf.nativeNightly, {
67
+ platform: { linux: 'linux', macos: 'darwin', windows: 'win32' }[leaf.os],
68
+ version: leaf.version, sourceSha: leaf.sourceSha, workflowRunId: leaf.workflowRunId,
69
+ packageSha256: leaf.artifactSha256, bundleSha256: leaf.bundleSha256, publicKey,
70
+ });
71
+ if (!native.ok) throw new Error(`${leaf.os}/dual native nightly proof failed: ${native.failures.join('; ')}`);
72
+ } else if (leaf.nativeNightly !== undefined) {
73
+ throw new Error('native nightly proof belongs only to the dual-host leaf');
74
+ }
52
75
  return leaf;
53
76
  }
54
77
 
55
- export function createPublicVerificationLeaf(input) {
78
+ export function createPublicVerificationLeaf(input, options = {}) {
56
79
  const payload = { schemaVersion: 1, kind: 'ruvnet-brain-public-verification-leaf', ...input };
57
- return validatePublicVerificationLeaf({ ...payload, leafSha256: digest(payload) });
80
+ return validatePublicVerificationLeaf({ ...payload, leafSha256: digest(payload) }, options);
58
81
  }
59
82
 
60
83
  function aggregatePayload(aggregate) {
@@ -122,6 +145,7 @@ function validateReviewOracleAgainstPlan(review, retrievalPlan) {
122
145
 
123
146
  const identityOf = (leaf) => ({
124
147
  sourceSha: leaf.sourceSha,
148
+ ...(Object.hasOwn(leaf, 'verifierSha') ? { verifierSha: leaf.verifierSha } : {}),
125
149
  version: leaf.version,
126
150
  tag: leaf.tag,
127
151
  artifactSha256: leaf.artifactSha256,
@@ -133,11 +157,13 @@ const identityOf = (leaf) => ({
133
157
  releaseTransactionId: leaf.releaseTransactionId,
134
158
  });
135
159
 
136
- export function buildPublicVerificationAggregate(leaves) {
160
+ export function buildPublicVerificationAggregate(leaves, options = {}) {
137
161
  if (!Array.isArray(leaves) || leaves.length !== PUBLIC_VERIFICATION_OS.length * PUBLIC_VERIFICATION_MODES.length) {
138
162
  throw new Error('public verification requires exactly nine leaves');
139
163
  }
140
- leaves.forEach(validatePublicVerificationLeaf);
164
+ leaves.forEach((leaf) => validatePublicVerificationLeaf(leaf, options));
165
+ const nativeRunIds = new Set(leaves.filter((leaf) => leaf.mode === 'dual').map((leaf) => String(leaf.workflowRunId)));
166
+ if (nativeRunIds.size !== 1) throw new Error('native nightly proofs span different workflow runs');
141
167
  const byLane = new Map(leaves.map((leaf) => [`${leaf.os}/${leaf.mode}`, leaf]));
142
168
  const required = PUBLIC_VERIFICATION_OS.flatMap((os) => PUBLIC_VERIFICATION_MODES.map((mode) => `${os}/${mode}`));
143
169
  if (byLane.size !== required.length || required.some((lane) => !byLane.has(lane))) {
@@ -178,6 +204,7 @@ export function buildPublicVerificationAggregate(leaves) {
178
204
  schemaVersion: 1,
179
205
  kind: 'ruvnet-brain-public-verification-aggregate',
180
206
  identity,
207
+ workflowRunId: [...nativeRunIds][0],
181
208
  coverage: first.coverage,
182
209
  lanes: required.map((lane) => ({ lane, leafSha256: byLane.get(lane).leafSha256 })),
183
210
  evidence: { leaves: required.map((lane) => structuredClone(byLane.get(lane))) },
@@ -193,7 +220,7 @@ export function buildPublicVerificationAggregate(leaves) {
193
220
  }
194
221
 
195
222
  export function signPublicVerificationAggregate({ leaves }, privateKey) {
196
- const payload = buildPublicVerificationAggregate(leaves);
223
+ const payload = buildPublicVerificationAggregate(leaves, { publicKey: crypto.createPublicKey(privateKey) });
197
224
  const aggregateSha256 = digest(payload);
198
225
  const signed = { ...payload, aggregateSha256 };
199
226
  return { ...signed, signature: crypto.sign(null, Buffer.from(canonicalJson(signed)), privateKey).toString('base64') };
@@ -210,14 +237,14 @@ export function verifyPublicVerificationAggregate(aggregate, publicKey, expected
210
237
  if (!Array.isArray(aggregate.evidence?.leaves)) {
211
238
  throw new Error('public verification aggregate lacks raw leaf evidence');
212
239
  }
213
- const rebuilt = buildPublicVerificationAggregate(aggregate.evidence.leaves);
214
- if (canonicalJson(rebuilt) !== canonicalJson(payload)) {
215
- throw new Error('public verification aggregate differs from rebuilt raw evidence');
216
- }
217
240
  const signed = { ...payload, aggregateSha256: aggregate.aggregateSha256 };
218
241
  if (!crypto.verify(null, Buffer.from(canonicalJson(signed)), publicKey, Buffer.from(aggregate.signature, 'base64'))) {
219
242
  throw new Error('public verification aggregate signature mismatch');
220
243
  }
244
+ const rebuilt = buildPublicVerificationAggregate(aggregate.evidence.leaves, { publicKey });
245
+ if (canonicalJson(rebuilt) !== canonicalJson(payload)) {
246
+ throw new Error('public verification aggregate differs from rebuilt raw evidence');
247
+ }
221
248
  const required = PUBLIC_VERIFICATION_OS.flatMap((os) => PUBLIC_VERIFICATION_MODES.map((mode) => `${os}/${mode}`));
222
249
  if (aggregate.lanes?.length !== required.length || new Set(aggregate.lanes.map(({ lane }) => lane)).size !== required.length
223
250
  || required.some((lane) => !aggregate.lanes.some((row) => row.lane === lane && HEX64.test(String(row.leafSha256 || ''))))) {
@@ -234,7 +261,11 @@ export function verifyPublicVerificationAggregate(aggregate, publicKey, expected
234
261
  || aggregate.retrievalOracle?.recordCount !== aggregate.evidence.leaves[0].retrievalPlan.denominator.eligibleStores.length) {
235
262
  throw new Error('public verification aggregate retrieval oracle identity is incomplete');
236
263
  }
237
- if (expectedIdentity && canonicalJson(aggregate.identity) !== canonicalJson(expectedIdentity)) {
264
+ if (expectedIdentity?.workflowRunId !== undefined && String(expectedIdentity.workflowRunId) !== aggregate.workflowRunId) {
265
+ throw new Error('public verification aggregate workflow run differs from the expected run');
266
+ }
267
+ const { workflowRunId: _expectedRun, ...expectedReleaseIdentity } = expectedIdentity || {};
268
+ if (expectedIdentity && canonicalJson(aggregate.identity) !== canonicalJson(expectedReleaseIdentity)) {
238
269
  throw new Error('public verification aggregate identity differs from the release transaction');
239
270
  }
240
271
  return aggregate;
@@ -245,13 +276,13 @@ function parseCliArgs(argv) {
245
276
  for (let index = 0; index < argv.length; index += 2) {
246
277
  const flag = argv[index];
247
278
  const value = argv[index + 1];
248
- if (!['--lanes', '--out'].includes(flag) || !value) {
249
- throw new Error('usage: public-verification-aggregate.mjs --lanes <dir> --out <file>');
279
+ if (!['--lanes', '--out', '--verifier-sha', '--workflow-run-id'].includes(flag) || !value) {
280
+ throw new Error('usage: public-verification-aggregate.mjs --lanes <dir> --out <file> --workflow-run-id <id>');
250
281
  }
251
282
  parsed[flag.slice(2)] = value;
252
283
  }
253
- if (!parsed.lanes || !parsed.out || Object.keys(parsed).length !== 2) {
254
- throw new Error('usage: public-verification-aggregate.mjs --lanes <dir> --out <file>');
284
+ if (!parsed.lanes || !parsed.out || !/^[1-9][0-9]*$/.test(parsed['workflow-run-id'] || '')) {
285
+ throw new Error('usage: public-verification-aggregate.mjs --lanes <dir> --out <file> --workflow-run-id <id>');
255
286
  }
256
287
  return parsed;
257
288
  }
@@ -265,20 +296,24 @@ function readExactJsonFiles(directory, count, label) {
265
296
  .map((entry) => JSON.parse(fs.readFileSync(path.join(directory, entry.name), 'utf8')));
266
297
  }
267
298
 
268
- function readLaneLeaves(directory) {
299
+ function readLaneLeaves(directory, options = {}) {
269
300
  const wrappers = readExactJsonFiles(directory, PUBLIC_VERIFICATION_OS.length, 'lane');
270
301
  const byOs = new Map();
271
302
  for (const wrapper of wrappers) {
272
303
  const payload = { schemaVersion: wrapper?.schemaVersion, kind: wrapper?.kind, os: wrapper?.os,
304
+ ...(Object.hasOwn(wrapper || {}, 'verifierSha') ? { verifierSha: wrapper.verifierSha, sourceSha: wrapper.sourceSha } : {}),
273
305
  leaves: wrapper?.leaves };
274
306
  if (payload.schemaVersion !== 1 || payload.kind !== 'ruvnet-brain-public-verification-os-lane'
275
307
  || !PUBLIC_VERIFICATION_OS.includes(payload.os) || byOs.has(payload.os)
276
308
  || !Array.isArray(payload.leaves) || payload.leaves.length !== PUBLIC_VERIFICATION_MODES.length
277
309
  || payload.leaves.some((leaf) => leaf?.os !== payload.os)
310
+ || (Object.hasOwn(payload, 'verifierSha') && (!HEX40.test(String(payload.verifierSha))
311
+ || !HEX40.test(String(payload.sourceSha))
312
+ || payload.leaves.some((leaf) => leaf.verifierSha !== payload.verifierSha || leaf.sourceSha !== payload.sourceSha)))
278
313
  || digest(payload) !== wrapper.laneSha256) {
279
314
  throw new Error('public verification OS lane wrapper is malformed, duplicated, or tampered');
280
315
  }
281
- payload.leaves.forEach(validatePublicVerificationLeaf);
316
+ payload.leaves.forEach((leaf) => validatePublicVerificationLeaf(leaf, options));
282
317
  byOs.set(payload.os, payload.leaves);
283
318
  }
284
319
  if (PUBLIC_VERIFICATION_OS.some((osName) => !byOs.has(osName))) {
@@ -287,11 +322,16 @@ function readLaneLeaves(directory) {
287
322
  return PUBLIC_VERIFICATION_OS.flatMap((osName) => byOs.get(osName));
288
323
  }
289
324
 
290
- export function generatePublicVerificationAggregate({ lanesDirectory, outputFile, privateKey }) {
325
+ export function generatePublicVerificationAggregate({ lanesDirectory, outputFile, privateKey, verifierSha, workflowRunId }) {
326
+ if (!/^[1-9][0-9]*$/.test(String(workflowRunId || ''))) throw new Error('expected workflow run ID is required');
291
327
  if (!privateKey) throw new Error('RUVNET_SIGNING_KEY is required');
292
328
  if (fs.existsSync(outputFile)) throw new Error(`refusing to overwrite existing aggregate: ${outputFile}`);
293
- const leaves = readLaneLeaves(lanesDirectory);
329
+ const leaves = readLaneLeaves(lanesDirectory, { publicKey: crypto.createPublicKey(privateKey) });
294
330
  const aggregate = signPublicVerificationAggregate({ leaves }, privateKey);
331
+ if (aggregate.workflowRunId !== String(workflowRunId)) throw new Error('public verification aggregate workflow run differs from the expected run');
332
+ if (verifierSha !== undefined && (!HEX40.test(String(verifierSha)) || aggregate.identity.verifierSha !== verifierSha)) {
333
+ throw new Error('public verification aggregate verifier SHA differs from the expected verifier');
334
+ }
295
335
  fs.writeFileSync(outputFile, `${JSON.stringify(aggregate, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
296
336
  return aggregate;
297
337
  }
@@ -301,6 +341,8 @@ async function main() {
301
341
  const aggregate = generatePublicVerificationAggregate({
302
342
  lanesDirectory: options.lanes,
303
343
  outputFile: options.out,
344
+ verifierSha: options['verifier-sha'],
345
+ workflowRunId: options['workflow-run-id'],
304
346
  privateKey: process.env.RUVNET_SIGNING_KEY,
305
347
  });
306
348
  process.stdout.write(`${JSON.stringify({ ok: true, aggregateSha256: aggregate.aggregateSha256,
@@ -2,6 +2,7 @@
2
2
  import crypto from 'node:crypto';
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
5
6
  import { finalizeReleaseTransaction } from './release-transaction.mjs';
6
7
  import { liveReleaseProvider } from './release-transaction-provider.mjs';
7
8
 
@@ -22,6 +23,8 @@ export async function finalizePublicVerification({
22
23
  identityFile,
23
24
  aggregateFile,
24
25
  outputFile,
26
+ verifierSha,
27
+ workflowRunId,
25
28
  privatePem = process.env.RUVNET_SIGNING_KEY,
26
29
  publicKeyFile = 'keys/ruvnet-brain-signing.pub.pem',
27
30
  adapter = liveReleaseProvider({ root: process.cwd() }),
@@ -33,7 +36,7 @@ export async function finalizePublicVerification({
33
36
  const identity = regularJson(identityFile, 'release identity');
34
37
  const aggregate = regularJson(aggregateFile, 'public verification aggregate');
35
38
  const publicKey = crypto.createPublicKey(fs.readFileSync(path.resolve(publicKeyFile), 'utf8'));
36
- const receipt = await finalizeReleaseTransaction({ identity, aggregate, adapter,
39
+ const receipt = await finalizeReleaseTransaction({ identity, aggregate, adapter, verifierSha, workflowRunId,
37
40
  privateKey: crypto.createPrivateKey(privatePem), publicKey, aggregatePublicKey: publicKey });
38
41
  fs.mkdirSync(path.dirname(output), { recursive: true });
39
42
  fs.writeFileSync(output, `${JSON.stringify(receipt, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
@@ -46,6 +49,8 @@ export async function main(args = process.argv.slice(2)) {
46
49
  identityFile: argument(args, '--identity'),
47
50
  aggregateFile: argument(args, '--aggregate'),
48
51
  outputFile: argument(args, '--out'),
52
+ verifierSha: argument(args, '--verifier-sha') ?? undefined,
53
+ workflowRunId: argument(args, '--workflow-run-id'),
49
54
  });
50
55
  process.stdout.write(`${JSON.stringify({ state: receipt.state, transactionId: receipt.transactionId,
51
56
  receiptDigest: receipt.receiptDigest }, null, 2)}\n`);
@@ -56,4 +61,5 @@ export async function main(args = process.argv.slice(2)) {
56
61
  }
57
62
  }
58
63
 
59
- if (path.resolve(process.argv[1] || '') === path.resolve(new URL(import.meta.url).pathname)) process.exitCode = await main();
64
+ const invokedDirectly = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
65
+ if (invokedDirectly) process.exitCode = await main();