ruvnet-brain 4.3.34 → 4.3.36

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.
package/README.md CHANGED
@@ -7,7 +7,7 @@ Created: 2026-06-29 22:36:38 EDT
7
7
 
8
8
  # 🧠 RuvNet Brain
9
9
 
10
- ### 🧠 RuvNet Brain — [![RuvNet Brain version 4.3.34 — updated 2026-07-30 03:24 EDT](https://img.shields.io/badge/version_4.3.34-updated_2026--07--30_03:24_EDT-1E90FF?style=for-the-badge&labelColor=0757BA)](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
10
+ ### 🧠 RuvNet Brain — [![RuvNet Brain version 4.3.36 — updated 2026-07-30 03:24 EDT](https://img.shields.io/badge/version_4.3.36-updated_2026--07--30_03:24_EDT-1E90FF?style=for-the-badge&labelColor=0757BA)](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
11
11
 
12
12
  **A portable, source-grounded brain over Reuven Cohen's (rUv's) RuvNet stack — delivered as a Claude Code plugin that makes Claude _use_ the stack instead of fighting it.**
13
13
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
- "version": "4.3.34",
3
+ "version": "4.3.36",
4
4
  "description": "One-command installer for RuvNet Brain \u2014 a portable, source-grounded brain over rUv's RuvNet building blocks, delivered as a Claude Code plugin so Claude uses the stack instead of fighting it.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
3
  "description": "RuvNet brain transplant for Claude Code — grounds every RuvNet decision in real source across 77 rUv repositories, prefers Ruflo / RuVector-RVF / AgentDB over training-prior defaults (pgvector, Pinecone, hand-rolled cosine), and can pull in any RuvNet repo on demand. Ships a UserPromptSubmit retrieve-and-inject grounding hook and a PreToolUse write gate that refuses ungrounded rUv-product code until search_ruvnet has been consulted (ADR-0012 / ADR-067).",
4
- "version": "4.3.34",
4
+ "version": "4.3.36",
5
5
  "author": {
6
6
  "name": "Stuart Kerr"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
- "version": "4.3.34",
3
+ "version": "4.3.36",
4
4
  "description": "Source-grounded RuvNet knowledge, lifecycle enforcement, and learning for Codex.",
5
5
  "author": {
6
6
  "name": "Stuart Kerr"
@@ -225,6 +225,41 @@ export function generationLedgerBytes(ledger) {
225
225
  return Buffer.from(`${JSON.stringify(ledger, null, 2)}\n`);
226
226
  }
227
227
 
228
+ const isIsoOrNull = (value) => value === null || (typeof value === 'string' && Number.isFinite(Date.parse(value)));
229
+ const isAttemptCount = (value) => Number.isSafeInteger(value) && value >= 1;
230
+
231
+ /**
232
+ * ADR-0091 D5: how an ELIGIBLE repository row stands in a bundle. It returns
233
+ * 'shipped' — the store's bytes ship: CURRENT, or STALE with a verified `carry` record (the refresh
234
+ * failed and the previous generation's bytes, re-hashed against the ledger, were kept);
235
+ * 'absent' — MISSING with a `failure` record: a store that failed with no prior bytes, excluded
236
+ * from the repository store set;
237
+ * null — anything else. A STALE/MISSING row WITHOUT its record, FAILED and UNVERIFIED stay
238
+ * rejected, exactly as before.
239
+ * The status token is never new: the distinction lives in the object (0.1.0's STALE_CARRIED and
240
+ * PENDING_FAILED were withdrawn).
241
+ */
242
+ export function eligibleRepositoryStanding(row) {
243
+ const status = row?.status;
244
+ const { carry, failure } = row || {};
245
+ if (status === 'CURRENT') return carry === undefined && failure === undefined ? 'shipped' : null;
246
+ if (status === 'STALE' && carry && typeof carry === 'object' && failure === undefined) {
247
+ const carried = String(carry.carriedSourceCommit || '').toLowerCase();
248
+ const shipped = typeof carry.reason === 'string' && carry.reason.length > 0
249
+ && HEX40.test(carried) && carried === String(row.artifact?.sourceCommit || '').toLowerCase()
250
+ && HEX40.test(String(carry.missedUpstream || '').toLowerCase())
251
+ && String(carry.missedUpstream).toLowerCase() === String(row.upstream?.sha || '').toLowerCase()
252
+ && carried !== String(carry.missedUpstream).toLowerCase()
253
+ && isAttemptCount(carry.attempts) && isIsoOrNull(carry.carriedCommittedAt)
254
+ && row.artifact?.bytesVerified === true && row.artifact?.passagesPresent === true;
255
+ return shipped ? 'shipped' : null;
256
+ }
257
+ if (status === 'MISSING' && failure && typeof failure === 'object' && carry === undefined) {
258
+ return typeof failure.reason === 'string' && failure.reason.length > 0 && isAttemptCount(failure.attempts) ? 'absent' : null;
259
+ }
260
+ return null;
261
+ }
262
+
228
263
  export function validatePublicInventory({ assetsDir, coverage, ledger, installedPublicStores = null, gistReceipt = null }) {
229
264
  const root = path.resolve(assetsDir);
230
265
  const selected = installedPublicStores === null ? null : [...installedPublicStores].map((store) => String(store).toLowerCase());
@@ -248,11 +283,23 @@ export function validatePublicInventory({ assetsDir, coverage, ledger, installed
248
283
  if (stores.some((store) => !store) || new Set(stores).size !== stores.length) throw new Error(`${family} stores are missing or duplicated`);
249
284
  return stores.sort();
250
285
  };
251
- const repos = coverage.rows.filter((row) => row.kind === 'repository' && row.disposition === 'eligible');
252
- if (repos.some((row) => row.status !== 'CURRENT')) throw new Error('an eligible repository is not CURRENT');
286
+ const eligibleRepos = coverage.rows.filter((row) => row.kind === 'repository' && row.disposition === 'eligible');
287
+ const rejected = eligibleRepos.find((row) => eligibleRepositoryStanding(row) === null);
288
+ if (rejected) {
289
+ throw new Error(`an eligible repository is not CURRENT (${rejected.artifact?.store || rejected.name}: ${rejected.status} `
290
+ + 'without a verified carry/failure record)');
291
+ }
292
+ const repos = eligibleRepos.filter((row) => eligibleRepositoryStanding(row) === 'shipped');
253
293
  const repositories = uniqueStores(repos, 'repository');
294
+ // A MISSING-with-failure store ships no bytes: it is in neither the repository set nor the excluded
295
+ // set, so a stray file under its name still fails below as an unclassified public store.
296
+ const absentRepositories = uniqueStores(eligibleRepos.filter((row) => eligibleRepositoryStanding(row) === 'absent'),
297
+ 'absent repository');
254
298
  const excludedRepositories = uniqueStores(coverage.rows.filter((row) => row.kind === 'repository'
255
299
  && row.disposition !== 'eligible'), 'excluded repository');
300
+ if (absentRepositories.some((store) => repositories.includes(store) || excludedRepositories.includes(store))) {
301
+ throw new Error('an absent repository store is also shipped or excluded');
302
+ }
256
303
  const excludedSet = new Set(excludedRepositories);
257
304
  if (repositories.some((store) => excludedSet.has(store))) throw new Error('eligible and excluded repository stores overlap');
258
305
  const gists = coverage.rows.filter((row) => row.kind === 'gist' && row.disposition === 'eligible');
@@ -339,6 +386,15 @@ export function validatePublicInventory({ assetsDir, coverage, ledger, installed
339
386
  throw new Error(`${store} generation record provenance is incomplete`);
340
387
  }
341
388
  }
389
+ // A carried store ships the PREVIOUS generation's bytes: the ledger must name exactly the commit
390
+ // the carry record says was kept, or the row describes bytes this bundle does not carry.
391
+ for (const row of repos) {
392
+ if (!row.carry) continue;
393
+ const store = String(row.artifact.store).toLowerCase();
394
+ if (String(ledger.stores[store]?.sourceCommit || '').toLowerCase() !== String(row.carry.carriedSourceCommit).toLowerCase()) {
395
+ throw new Error(`${store} carry record names a source commit the generation ledger does not carry`);
396
+ }
397
+ }
342
398
  const publicStores = expected;
343
399
  evidenceFiles.sort((a, b) => a.path.localeCompare(b.path) || a.kind.localeCompare(b.kind));
344
400
  return { repositories, excludedRepositories, gistAggregate, derived, publicStores,
@@ -14,25 +14,41 @@
14
14
  // "Pinning survives only through enforced equality to the approved shipped runtime and its
15
15
  // executable hashes. Copying current-main package.json or preserving a version string alone is
16
16
  // insufficient." So this compares every executable/runtime file's sha256 AND byte length against a
17
- // committed inventory produced from the owner-approved shipped code artifact — and, in the other
17
+ // inventory produced from the owner-approved shipped code artifact — and, in the other
18
18
  // direction, refuses any executable-shaped file in the archive that the inventory does not cover, so
19
19
  // a NEW unpinned executable cannot ride along.
20
20
  //
21
- // builderSourceSha stays independent on purpose: the corpus content may be built from a newer main
22
- // than the approved runtime. That is the whole point of separating the two identities.
21
+ // builderSourceSha stays a separately-bound identity in the corpus receipt. Under ADR-0091 D3 the
22
+ // corpus is built at the approved release's own sourceSha, so the two are equal by construction.
23
+ //
24
+ // WHERE THE PIN COMES FROM (ADR-0091 D3, 2026-09-28). It used to be a committed file,
25
+ // data/approved-runtime.json, emitted by hand after a code release reached `install-verified`. That
26
+ // could never stay valid: under "every main commit is a release", the commit carrying a pin for
27
+ // release X is itself release X+1, whose archive then fails the pin for X. The committed file is gone
28
+ // for good. The pin is now RESOLVED AT RUN TIME from evidence GitHub already holds: the newest code
29
+ // release whose signed public-verification-aggregate.json verifies against the committed
30
+ // keys/ruvnet-brain-signing.pub.pem (verdict PASS over the 3-OS x 3-host install matrix), whose
31
+ // identity.bundleSha256 is the exact ruvnet-brain.zip on that release, and whose identity.sourceSha
32
+ // is reachable from origin/main. The pin is emitted from THAT zip's own ARCHIVE-MANIFEST.json, and the
33
+ // corpus is built at identity.sourceSha, so runtime equality holds by construction. It is still
34
+ // enforced byte for byte, both directions, by the unchanged verifyApprovedRuntime.
23
35
  //
24
36
  // Usage:
25
- // node scripts/approved-runtime.mjs --emit --archive-manifest <ARCHIVE-MANIFEST.json> \
26
- // --code-sha <40hex> --out data/approved-runtime.json # owner, during a code release
37
+ // node scripts/approved-runtime.mjs --resolve --repo owner/name [--tag vX.Y.Z] [--main-ref origin/main] \
38
+ // --out <pin.json> # resolve (newest ONLY, or exactly --tag) and write the pin
39
+ // # exit 3 = newest release not yet install-verified
40
+ // # exit 1 = evidence present but invalid (loud)
27
41
  // node scripts/approved-runtime.mjs --verify --archive-manifest <ARCHIVE-MANIFEST.json> \
28
- // [--pin data/approved-runtime.json] # every corpus promotion
42
+ // --pin <pin.json> # every corpus promotion
29
43
 
44
+ import crypto from 'node:crypto';
30
45
  import fs from 'node:fs';
46
+ import os from 'node:os';
31
47
  import path from 'node:path';
48
+ import { spawnSync } from 'node:child_process';
32
49
  import { fileURLToPath, pathToFileURL } from 'node:url';
33
50
 
34
51
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
35
- export const APPROVED_RUNTIME_FILE = 'data/approved-runtime.json';
36
52
  export const APPROVED_RUNTIME_KIND = 'ruvnet-brain-approved-runtime';
37
53
 
38
54
  // Executable/runtime surface. Extensions cover every interpretable artifact; the three exact
@@ -127,11 +143,13 @@ export function verifyApprovedRuntime({ manifest, pin } = {}) {
127
143
  }
128
144
 
129
145
  export function readApprovedRuntime(pinFile) {
130
- const resolved = path.resolve(pinFile || path.join(ROOT, APPROVED_RUNTIME_FILE));
146
+ // No committed default (ADR-0091 D3): a pin is only ever this run's own --resolve output.
147
+ const remedy = 'node scripts/approved-runtime.mjs --resolve --repo <owner/name> --out <pin.json>';
148
+ if (!pinFile) throw new Error(`no approved runtime pin supplied; resolve one from the newest install-verified code release: ${remedy}`);
149
+ const resolved = path.resolve(pinFile);
131
150
  if (!fs.existsSync(resolved)) {
132
- throw new Error(`no approved runtime pin at ${resolved}. Unattended corpus promotion is refused until an `
133
- + `owner-gated code release emits it: node scripts/approved-runtime.mjs --emit --archive-manifest `
134
- + `<ARCHIVE-MANIFEST.json> --code-sha <sha> --out ${APPROVED_RUNTIME_FILE}`);
151
+ throw new Error(`no approved runtime pin at ${resolved}. Unattended corpus promotion is refused until one is `
152
+ + `resolved from an install-verified code release: ${remedy}`);
135
153
  }
136
154
  return JSON.parse(fs.readFileSync(resolved, 'utf8'));
137
155
  }
@@ -156,29 +174,232 @@ export function emitApprovedRuntime({ manifest, approvedCodeSha } = {}) {
156
174
  };
157
175
  }
158
176
 
177
+
178
+ // ---------------------------------------------------------------------------------------------
179
+ // Run-time resolution (ADR-0091 D3)
180
+ // ---------------------------------------------------------------------------------------------
181
+
182
+ export const AGGREGATE_ASSET = 'public-verification-aggregate.json';
183
+ export const ARCHIVE_ASSET = 'ruvnet-brain.zip';
184
+ export const SIGNING_PUBLIC_KEY_FILE = 'keys/ruvnet-brain-signing.pub.pem';
185
+ const CODE_TAG = /^v(\d+)\.(\d+)\.(\d+)$/;
186
+
187
+ const semverDescending = (left, right) => {
188
+ const a = CODE_TAG.exec(left).slice(1).map(Number);
189
+ const b = CODE_TAG.exec(right).slice(1).map(Number);
190
+ return b[0] - a[0] || b[1] - a[1] || b[2] - a[2];
191
+ };
192
+
193
+ function defaultGh(args) {
194
+ const result = spawnSync(process.env.RUVNET_GH_COMMAND || 'gh', args,
195
+ { encoding: 'utf8', maxBuffer: 256 * 1024 * 1024, timeout: 15 * 60_000 });
196
+ if (result.error || result.status !== 0) {
197
+ throw new Error(`gh ${args.slice(0, 3).join(' ')} failed: ${String(result.error?.message || result.stderr || `exit ${result.status}`).trim().slice(0, 400)}`);
198
+ }
199
+ return result.stdout;
200
+ }
201
+
202
+ function defaultGit(args, { cwd = ROOT } = {}) {
203
+ const result = spawnSync('git', args, { cwd, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 });
204
+ return { status: result.status, stdout: String(result.stdout || ''), stderr: String(result.stderr || '') };
205
+ }
206
+
207
+ function sha256File(file) {
208
+ const hash = crypto.createHash('sha256');
209
+ const fd = fs.openSync(file, 'r');
210
+ try {
211
+ const buffer = Buffer.alloc(8 * 1024 * 1024);
212
+ let read;
213
+ while ((read = fs.readSync(fd, buffer, 0, buffer.length, null)) > 0) hash.update(buffer.subarray(0, read));
214
+ } finally { fs.closeSync(fd); }
215
+ return hash.digest('hex');
216
+ }
217
+
218
+ /** Read the ONE ARCHIVE-MANIFEST.json out of a release zip without extracting 500 MB of corpus. */
219
+ export function readArchiveManifestFromZip(zipFile) {
220
+ const listing = spawnSync('unzip', ['-Z1', zipFile], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
221
+ if (listing.status !== 0) throw new Error(`cannot list ${path.basename(zipFile)}: ${String(listing.stderr).trim().slice(0, 200)}`);
222
+ const entries = listing.stdout.split('\n').filter((entry) => /(^|\/)ARCHIVE-MANIFEST\.json$/.test(entry));
223
+ if (entries.length !== 1) throw new Error(`${path.basename(zipFile)} carries ${entries.length} ARCHIVE-MANIFEST.json entries, expected exactly 1`);
224
+ const body = spawnSync('unzip', ['-p', zipFile, entries[0]], { encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
225
+ if (body.status !== 0) throw new Error(`cannot read ${entries[0]}: ${String(body.stderr).trim().slice(0, 200)}`);
226
+ return JSON.parse(body.stdout);
227
+ }
228
+
229
+ /**
230
+ * The newest code release exists but carries NO public-verification aggregate yet: install
231
+ * verification has not finished (or never ran). That is "not yet verified", not a fault — the nightly
232
+ * stands down cleanly on it. It is the ONLY resolution failure that is not loud.
233
+ */
234
+ export class ApprovedRuntimeNotYetVerified extends Error {
235
+ constructor(tag, message) {
236
+ super(message);
237
+ this.name = 'ApprovedRuntimeNotYetVerified';
238
+ this.code = 'APPROVED_RUNTIME_NOT_YET_VERIFIED';
239
+ this.tag = tag;
240
+ }
241
+ }
242
+ /** CLI exit code for ApprovedRuntimeNotYetVerified; every other resolution failure exits 1. */
243
+ export const EXIT_NOT_YET_VERIFIED = 3;
244
+
245
+ /**
246
+ * Judge ONE code release. Returns the resolution, or throws a reason that names exactly which piece of
247
+ * evidence is missing or wrong. Order is cheapest-first: the 1 MB signed aggregate and git ancestry are
248
+ * checked before the ~555 MB archive is downloaded at all.
249
+ */
250
+ function judgeCodeRelease({ repo, tag, gh, git, mainRef, publicKey, verifyAggregate, downloadAsset, readArchiveManifest, scratch }) {
251
+ const release = JSON.parse(gh(['api', `repos/${repo}/releases/tags/${tag}`]));
252
+ if (release.draft || release.prerelease) throw new Error('release is a draft or prerelease');
253
+ const assets = new Map((release.assets || []).map((asset) => [asset.name, asset]));
254
+ if (!assets.has(AGGREGATE_ASSET)) {
255
+ throw new ApprovedRuntimeNotYetVerified(tag, `code release ${tag} has no ${AGGREGATE_ASSET} yet — `
256
+ + 'it has not reached install-verified, so there is no approved runtime to build at');
257
+ }
258
+ if (!assets.has(ARCHIVE_ASSET)) throw new Error(`no ${ARCHIVE_ASSET} asset`);
259
+
260
+ const aggregateFile = downloadAsset({ repo, tag, name: AGGREGATE_ASSET, dir: scratch });
261
+ let aggregate;
262
+ try { aggregate = JSON.parse(fs.readFileSync(aggregateFile, 'utf8')); }
263
+ catch (error) { throw new Error(`${AGGREGATE_ASSET} is not JSON (${error.message})`); }
264
+ // Signature over canonical JSON with the COMMITTED trust root, digest, and a full rebuild of the
265
+ // aggregate from its nine raw leaves — the same verifier the release finalizer uses.
266
+ verifyAggregate(aggregate, publicKey);
267
+ const identity = aggregate.identity || {};
268
+ if (aggregate.verdict !== 'PASS') throw new Error(`aggregate verdict is ${aggregate.verdict}, not PASS`);
269
+ if (identity.tag !== tag || identity.version !== tag.slice(1)) {
270
+ throw new Error(`aggregate identity ${identity.tag}/${identity.version} does not describe release ${tag}`);
271
+ }
272
+ const sourceSha = String(identity.sourceSha || '').toLowerCase();
273
+ const bundleSha256 = String(identity.bundleSha256 || '').toLowerCase();
274
+ if (!HEX40.test(sourceSha) || !HEX64.test(bundleSha256)) throw new Error('aggregate identity sourceSha/bundleSha256 malformed');
275
+
276
+ const apiDigest = String(assets.get(ARCHIVE_ASSET).digest || '');
277
+ if (apiDigest && apiDigest !== `sha256:${bundleSha256}`) {
278
+ throw new Error(`${ARCHIVE_ASSET} on ${tag} (${apiDigest}) is not the archive the aggregate verified (sha256:${bundleSha256})`);
279
+ }
280
+ const ancestry = git(['merge-base', '--is-ancestor', sourceSha, mainRef]);
281
+ if (ancestry.status === 1) throw new Error(`aggregate sourceSha ${sourceSha} is not reachable from ${mainRef}`);
282
+ if (ancestry.status !== 0) {
283
+ throw new Error(`aggregate sourceSha ${sourceSha} cannot be checked against ${mainRef} (${ancestry.stderr.trim().slice(0, 200) || `exit ${ancestry.status}`}); is the clone complete?`);
284
+ }
285
+ // The corpus is built AT sourceSha, and build-bundle.mjs stamps the archive with package.json's
286
+ // version there. Prove that equals the approved release, or the pin could never match the build.
287
+ const pkg = git(['show', `${sourceSha}:package.json`]);
288
+ let pkgVersion = null;
289
+ try { pkgVersion = JSON.parse(pkg.stdout).version; } catch { /* reported below */ }
290
+ if (pkg.status !== 0 || pkgVersion !== identity.version) {
291
+ throw new Error(`package.json at ${sourceSha.slice(0, 12)} is ${pkgVersion ?? 'unreadable'}, not ${identity.version}`);
292
+ }
293
+
294
+ const zipFile = downloadAsset({ repo, tag, name: ARCHIVE_ASSET, dir: scratch });
295
+ const actual = sha256File(zipFile);
296
+ if (actual !== bundleSha256) throw new Error(`downloaded ${ARCHIVE_ASSET} is ${actual}, not the verified ${bundleSha256}`);
297
+ const manifest = readArchiveManifest(zipFile);
298
+ if (manifest.version !== identity.version || manifest.releaseTag !== tag) {
299
+ throw new Error(`archive manifest is ${manifest.releaseTag}, not ${tag}`);
300
+ }
301
+ const pin = emitApprovedRuntime({ manifest, approvedCodeSha: sourceSha });
302
+ return {
303
+ pin,
304
+ release: { tag, version: identity.version, sourceSha, bundleSha256, aggregateSha256: aggregate.aggregateSha256 },
305
+ };
306
+ }
307
+
308
+ /**
309
+ * Resolve the approved runtime pin from the NEWEST published code release (or exactly `tag`).
310
+ *
311
+ * NO FALLBACK (independent review of ADR-0091 D3, 2026-09-28). The newest vX.Y.Z must itself carry a
312
+ * PASS aggregate. It never walks back to an older release: an older runtime promoted as the corpus
313
+ * `releases/latest` over a newer live code release breaks fresh installs (version mismatch) and is
314
+ * refused by already-updated clients as incompatible — a self-inflicted outage, and a deliberate
315
+ * downgrade path for anyone able to withhold or corrupt the newest aggregate. So:
316
+ * - newest release has NO aggregate asset -> throws ApprovedRuntimeNotYetVerified (clean stand-down)
317
+ * - newest release has an aggregate that does not verify, is not PASS, or any other evidence fails
318
+ * -> throws a plain Error (loud failure; never skipped)
319
+ */
320
+ export async function resolveApprovedRuntime({
321
+ repo, tag = null, mainRef = 'origin/main', root = ROOT,
322
+ gh = defaultGh, git = (args) => defaultGit(args, { cwd: root }),
323
+ publicKey = null,
324
+ // Loaded lazily: bin/install.mjs reaches this module (via installed-brain-health.mjs ->
325
+ // isRuntimeFile) on every customer install, and must not drag the release-verification graph in.
326
+ verifyAggregate = null,
327
+ downloadAsset = ({ repo: slug, tag: releaseTag, name, dir }) => {
328
+ gh(['release', 'download', releaseTag, '--repo', slug, '--pattern', name, '--dir', dir, '--clobber']);
329
+ return path.join(dir, name);
330
+ },
331
+ readArchiveManifest = readArchiveManifestFromZip,
332
+ scratchDir = null,
333
+ } = {}) {
334
+ if (!/^[^/\s]+\/[^/\s]+$/.test(String(repo || ''))) throw new Error('--repo must be owner/name');
335
+ if (tag !== null && !CODE_TAG.test(String(tag))) throw new Error('--tag must be vX.Y.Z');
336
+ const key = publicKey || crypto.createPublicKey(fs.readFileSync(path.join(root, SIGNING_PUBLIC_KEY_FILE), 'utf8'));
337
+ const verify = verifyAggregate
338
+ || (await import('./public-verification-aggregate.mjs')).verifyPublicVerificationAggregate;
339
+
340
+ let candidate = tag;
341
+ if (!candidate) {
342
+ const listed = JSON.parse(gh(['release', 'list', '--repo', repo, '--limit', '200',
343
+ '--json', 'tagName,isDraft,isPrerelease']) || '[]');
344
+ [candidate] = listed.filter((row) => !row.isDraft && !row.isPrerelease && CODE_TAG.test(String(row.tagName || '')))
345
+ .map((row) => row.tagName).sort(semverDescending);
346
+ }
347
+ if (!candidate) throw new Error(`no code releases (vX.Y.Z) listed on ${repo}`);
348
+
349
+ const scratch = scratchDir || fs.mkdtempSync(path.join(os.tmpdir(), 'approved-runtime-resolve-'));
350
+ const dir = path.join(scratch, candidate);
351
+ fs.mkdirSync(dir, { recursive: true });
352
+ try {
353
+ return judgeCodeRelease({ repo, tag: candidate, gh, git, mainRef, publicKey: key,
354
+ verifyAggregate: verify, downloadAsset, readArchiveManifest, scratch: dir });
355
+ } catch (error) {
356
+ if (error instanceof ApprovedRuntimeNotYetVerified) throw error;
357
+ throw new Error(`code release ${candidate}${tag ? '' : ' (the newest)'} carries install-verification evidence that does not hold `
358
+ + `(refusing it, and refusing to fall back to an older release): ${error.message}`);
359
+ } finally {
360
+ fs.rmSync(dir, { recursive: true, force: true });
361
+ if (!scratchDir) fs.rmSync(scratch, { recursive: true, force: true });
362
+ }
363
+ }
364
+
159
365
  const arg = (name, fallback) => {
160
366
  const index = process.argv.indexOf(name);
161
367
  return index >= 0 && process.argv[index + 1] ? process.argv[index + 1] : fallback;
162
368
  };
163
369
 
164
- function main() {
370
+ async function main() {
371
+ if (process.argv.includes('--resolve')) {
372
+ let result;
373
+ try {
374
+ result = await resolveApprovedRuntime({
375
+ repo: arg('--repo', process.env.GITHUB_REPOSITORY),
376
+ tag: arg('--tag', null),
377
+ mainRef: arg('--main-ref', 'origin/main'),
378
+ });
379
+ } catch (error) {
380
+ console.error(`[approved-runtime] ${error.message}`);
381
+ return error instanceof ApprovedRuntimeNotYetVerified ? EXIT_NOT_YET_VERIFIED : 1;
382
+ }
383
+ const out = arg('--out');
384
+ if (out) {
385
+ fs.mkdirSync(path.dirname(path.resolve(out)), { recursive: true });
386
+ fs.writeFileSync(path.resolve(out), `${JSON.stringify(result.pin, null, 2)}\n`);
387
+ }
388
+ process.stdout.write(`${JSON.stringify({ ...result.release, fileCount: result.pin.fileCount })}\n`);
389
+ console.error(`[approved-runtime] resolved ${result.release.tag} @ ${result.release.sourceSha} `
390
+ + `(${result.pin.fileCount} executable/runtime file(s) pinned from its own archive)`);
391
+ return 0;
392
+ }
393
+
165
394
  const manifestFile = arg('--archive-manifest');
166
- if (!manifestFile) { console.error('usage: approved-runtime.mjs --emit|--verify --archive-manifest <file> [...]'); return 2; }
395
+ if (!process.argv.includes('--verify') || !manifestFile) {
396
+ console.error('usage: approved-runtime.mjs --resolve --repo <owner/name> [--tag vX.Y.Z] [--main-ref <ref>] --out <pin.json>\n'
397
+ + ' approved-runtime.mjs --verify --archive-manifest <file> --pin <pin.json>');
398
+ return 2;
399
+ }
167
400
  let manifest;
168
401
  try { manifest = JSON.parse(fs.readFileSync(path.resolve(manifestFile), 'utf8')); }
169
402
  catch (error) { console.error(`[approved-runtime] cannot read archive manifest: ${error.message}`); return 1; }
170
-
171
- if (process.argv.includes('--emit')) {
172
- let pin;
173
- try { pin = emitApprovedRuntime({ manifest, approvedCodeSha: arg('--code-sha') }); }
174
- catch (error) { console.error(`[approved-runtime] ${error.message}`); return 1; }
175
- const out = path.resolve(arg('--out', path.join(ROOT, APPROVED_RUNTIME_FILE)));
176
- fs.mkdirSync(path.dirname(out), { recursive: true });
177
- fs.writeFileSync(out, `${JSON.stringify(pin, null, 2)}\n`);
178
- console.log(`[approved-runtime] pinned ${pin.fileCount} executable/runtime file(s) at ${pin.releaseTag} → ${path.relative(ROOT, out)}`);
179
- return 0;
180
- }
181
-
182
403
  let pin;
183
404
  try { pin = readApprovedRuntime(arg('--pin')); }
184
405
  catch (error) { console.error(`[approved-runtime] ${error.message}`); return 1; }
@@ -193,5 +414,5 @@ function main() {
193
414
  }
194
415
 
195
416
  if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
196
- process.exitCode = main();
417
+ main().then((code) => { process.exitCode = code; });
197
418
  }
@@ -62,11 +62,13 @@ import { validatePublicInventory } from './public-inventory.mjs';
62
62
  import { bindAssembledReleaseProjection, createReleaseProjection } from './release-projection.mjs';
63
63
  import { materializePublicInputs, SELECTION_FILE, validateSelectionReceipt } from './public-inputs.mjs';
64
64
  import { isPrivate, loadPrivateSlugs, shouldFenceL2 } from './private-fence.mjs';
65
- import { validateCoverageLedger } from './coverage-integrity.mjs';
65
+ import { eligibleRepositoryStanding, validateCoverageLedger } from './coverage-integrity.mjs';
66
66
  // The org total is DERIVED, never a literal: it was hardcoded 248 in this file and in its
67
67
  // sibling while the account actually had 200 — one stale fact, restated twice (2026-08-12).
68
68
  import { orgRepoCount } from './org-repo-count.mjs';
69
69
  import { assertCapabilityOnlyStore } from '../kb/capability-only.mjs';
70
+ import { CURRENCY_BASIS, corpusCurrencyBlock } from './corpus-currency.mjs';
71
+ import { loadFixture } from './oracle/repo-recall.mjs';
70
72
 
71
73
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
72
74
 
@@ -566,6 +568,8 @@ async function assembleBundleImpl({ corpusDir, runtimeRoot, outDir, identity = {
566
568
  // and is never rewritten here, so they are declared to validateSelectedRvfGenerations as explicitly
567
569
  // EXCLUDED rather than left looking like unexplained extra generation records.
568
570
  const legacyExcluded = [];
571
+ // The bootstrap tag the legacy pass-2 projection was produced from (manifest.corpus.generationTag).
572
+ let legacySeedTag = null;
569
573
  // LEGACY SEED pass 2 (retires at plan step 11): the externally-produced release projection is the
570
574
  // selection authority for this mode, exactly as it was before the step-5 consolidation. Scope the
571
575
  // discovered set to the stores that projection actually bound, so the assembled tree and
@@ -597,6 +601,7 @@ async function assembleBundleImpl({ corpusDir, runtimeRoot, outDir, identity = {
597
601
  if (legacyCoverage?.kind !== 'ruvnet-brain-release-coverage' || !Array.isArray(legacyCoverage.rows)) {
598
602
  fail('legacy release projection coverage is not a ruvnet-brain-release-coverage ledger');
599
603
  }
604
+ legacySeedTag = typeof legacyCoverage.corpusSeed?.tag === 'string' ? legacyCoverage.corpusSeed.tag : null;
600
605
  const projectedClasses = path.join(path.resolve(legacySeedProjection.projectionDir), 'public-store-classes.json');
601
606
  const projectedDerived = fs.existsSync(projectedClasses)
602
607
  ? (JSON.parse(fs.readFileSync(projectedClasses, 'utf8')).derived || []).map((e) => String(e?.store || '').toLowerCase())
@@ -672,6 +677,11 @@ async function assembleBundleImpl({ corpusDir, runtimeRoot, outDir, identity = {
672
677
  // validatePublicInventory has already bound that ledger to the bytes on disk.
673
678
  for (const row of corpusCoverage.rows) {
674
679
  if (row.disposition !== 'eligible') continue;
680
+ // ADR-0091 D5: a MISSING row with a `failure` record ships no store at all (validatePublicInventory
681
+ // already proved no bytes exist under its name), so there is no ledger generation to bind. A
682
+ // STALE row with a `carry` record binds below exactly like a CURRENT one: its artifact digest and
683
+ // source generation ARE the carried bytes this ledger carries.
684
+ if (row.kind === 'repository' && eligibleRepositoryStanding(row) === 'absent') continue;
675
685
  const store = String(row.artifact?.store || '');
676
686
  const generation = ledgerIn.stores?.[store]
677
687
  || Object.entries(ledgerIn.stores || {}).find(([key]) => key.toLowerCase() === store.toLowerCase())?.[1];
@@ -886,10 +896,26 @@ async function assembleBundleImpl({ corpusDir, runtimeRoot, outDir, identity = {
886
896
  // sealed bytes never depends on, or varies with, network availability.
887
897
  const ORG = orgRepoCount({ fetch: () => null });
888
898
  const hasConcepts = selectedResults.some((r) => r.kind === 'derived' && r.name.toLowerCase() === 'concepts');
899
+ // ADR-0091 D7.1: `generated` is the ASSEMBLY time and must never be read as freshness. `corpus` says
900
+ // what is actually known about content currency, from the sealed coverage this archive was bound to
901
+ // above (null fields where nothing was observed -- never estimated). See scripts/corpus-currency.mjs.
902
+ const fixtureFile = path.join(dataDir, 'retrieval-query-evidence.json');
903
+ let fixtureStores = null;
904
+ if (corpusCoverage && fs.existsSync(fixtureFile)) {
905
+ try { fixtureStores = loadFixture(fixtureFile).questions.map((question) => question.store); }
906
+ catch (error) { fail(`frozen retrieval fixture is unreadable (${error.message})`); }
907
+ }
908
+ const corpusCurrency = corpusCurrencyBlock({
909
+ coverage: corpusCoverage,
910
+ basis: corpusCoverage ? CURRENCY_BASIS.SEALED : legacySeed ? CURRENCY_BASIS.LEGACY : CURRENCY_BASIS.NONE,
911
+ generationTag: seedIdentity?.tag ?? legacySeedTag,
912
+ fixtureStores,
913
+ });
889
914
  const manifest = {
890
915
  brainVersion: version, // FIELD = bare literal; versionTag stays the v-prefixed Release tag
891
916
  generated: now.toISOString(),
892
917
  generatedHuman: now.toUTCString(),
918
+ corpus: corpusCurrency,
893
919
  coverage: { built: manifestEntries.length, catalogued: regFlat.length, orgTotalApprox: ORG.count, orgTotalSource: ORG.source, orgTotalAt: ORG.at, pending: pendingRepos.length },
894
920
  crossRepoTool: { mcp: 'forge-mcp-all.mjs', cli: 'forge-ask-all.mjs', tool: 'search_ruvnet' },
895
921
  conceptsStore: hasConcepts ? { store: 'concepts.big.rvf', note: 'L2 synthesis + per-repo primers embedded as prose; unioned by search_ruvnet so code-implemented capabilities are retrievable as high-confidence prose.' } : null,