ruvnet-brain 4.3.33 → 4.3.35

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.33 — updated 2026-07-30 03:24 EDT](https://img.shields.io/badge/version_4.3.33-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.35 — updated 2026-07-30 03:24 EDT](https://img.shields.io/badge/version_4.3.35-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.33",
3
+ "version": "4.3.35",
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.33",
4
+ "version": "4.3.35",
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.33",
3
+ "version": "4.3.35",
4
4
  "description": "Source-grounded RuvNet knowledge, lifecycle enforcement, and learning for Codex.",
5
5
  "author": {
6
6
  "name": "Stuart Kerr"
@@ -49,9 +49,13 @@ async function doctor(proj) {
49
49
  const hasSchema = sql(db, 'SELECT count(*) FROM episodes;') !== null;
50
50
  if (!hasSchema) row.notes.push('OLD SCHEMA — needs export/init/import migration (see migrate-agentdb-schema.sh)');
51
51
 
52
- // What can the session-start hook actually see?
53
- row.visible = Number(sql(db, `SELECT count(*) FROM memory_entries WHERE namespace='${name}';`) ?? 0);
54
- row.checkpoint = Number(sql(db, `SELECT count(*) FROM memory_entries WHERE namespace='${name}' AND key LIKE 'project-state-current%';`) ?? 0) > 0;
52
+ // What can the session-start hook actually see? `name` is a directory basename the operator
53
+ // chose on the CLI, not adversarial input -- but a project folder with an apostrophe in its name
54
+ // would otherwise break this query's syntax, so it gets the same escaping every other SQL
55
+ // interpolation in this codebase already uses.
56
+ const escapedName = name.replace(/'/g, "''");
57
+ row.visible = Number(sql(db, `SELECT count(*) FROM memory_entries WHERE namespace='${escapedName}';`) ?? 0);
58
+ row.checkpoint = Number(sql(db, `SELECT count(*) FROM memory_entries WHERE namespace='${escapedName}' AND key LIKE 'project-state-current%';`) ?? 0) > 0;
55
59
 
56
60
  // Foreign-project namespaces: REPORT, never migrate.
57
61
  const nsRows = (sql(db, 'SELECT namespace, count(*) FROM memory_entries GROUP BY namespace;') || '')
@@ -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
  }