ruvnet-brain 4.3.14 → 4.3.16

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.14 — updated 2026-07-30 03:24 EDT](https://img.shields.io/badge/version_4.3.14-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.16 — updated 2026-07-30 03:24 EDT](https://img.shields.io/badge/version_4.3.16-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/bin/install.mjs CHANGED
@@ -1722,8 +1722,27 @@ export function wireCodexPlugin({
1722
1722
  }
1723
1723
  if (announce) {
1724
1724
  ok(`Codex Brain plugin installed and enabled (${after.version || 'version unknown'}).`);
1725
+ warn('Existing Codex app-server sessions may retain the previous plugin path; restart Codex before using the updated plugin.');
1725
1726
  }
1726
- return { host: true, action: before.installed ? 'updated' : 'installed', ...after };
1727
+ return {
1728
+ host: true,
1729
+ action: before.installed ? 'updated' : 'installed',
1730
+ ...after,
1731
+ // Codex currently exposes no generation/session lease API. This explicit guard prevents the
1732
+ // installer from implying that a native cache update is safe for an already-running session.
1733
+ sessionSafety: 'restart-required',
1734
+ restartRequired: true,
1735
+ sessionSafetyReason: 'Codex host cache generations are owned by Codex and have no lease API',
1736
+ };
1737
+ }
1738
+
1739
+ export function codexSessionSafety(status) {
1740
+ if (status?.restartRequired === true) return status;
1741
+ return {
1742
+ sessionSafety: 'unknown',
1743
+ restartRequired: false,
1744
+ sessionSafetyReason: 'No Codex plugin update occurred',
1745
+ };
1727
1746
  }
1728
1747
 
1729
1748
  function codexHooksList({
@@ -2857,6 +2876,31 @@ function missingUpdaterHelp(kbDir) {
2857
2876
  console.error(` — the current bundle ships forge-update.mjs; then this command will work.`);
2858
2877
  }
2859
2878
 
2879
+ function acquireHostConvergenceLock(lockPath) {
2880
+ try {
2881
+ fs.mkdirSync(lockPath);
2882
+ fs.writeFileSync(path.join(lockPath, 'owner.json'), JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }));
2883
+ return { acquired: true, release: () => fs.rmSync(lockPath, { recursive: true, force: true }) };
2884
+ } catch (error) {
2885
+ if (error?.code !== 'EEXIST') return { acquired: false, error: error.message };
2886
+ try {
2887
+ const owner = JSON.parse(fs.readFileSync(path.join(lockPath, 'owner.json'), 'utf8'));
2888
+ if (Number.isInteger(owner.pid) && owner.pid !== process.pid) {
2889
+ try { process.kill(owner.pid, 0); return { acquired: false, error: `another host synchronization is running (pid ${owner.pid})` }; }
2890
+ catch (probeError) { if (probeError?.code !== 'ESRCH') return { acquired: false, error: 'another host synchronization owns the lock' }; }
2891
+ }
2892
+ } catch { /* an incomplete lock is safe to reclaim */ }
2893
+ fs.rmSync(lockPath, { recursive: true, force: true });
2894
+ try {
2895
+ fs.mkdirSync(lockPath);
2896
+ fs.writeFileSync(path.join(lockPath, 'owner.json'), JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }));
2897
+ return { acquired: true, release: () => fs.rmSync(lockPath, { recursive: true, force: true }) };
2898
+ } catch (retryError) {
2899
+ return { acquired: false, error: retryError.message };
2900
+ }
2901
+ }
2902
+ }
2903
+
2860
2904
  export function syncHostsAfterUpdate(cacheDir = resolvedKbDir(), {
2861
2905
  sourceRoot = REPO_ROOT,
2862
2906
  brainHome = process.env.RUVNET_BRAIN_HOME || path.join(os.homedir(), '.cache', 'ruvnet-brain'),
@@ -2864,6 +2908,7 @@ export function syncHostsAfterUpdate(cacheDir = resolvedKbDir(), {
2864
2908
  wireClaude = wirePlugin,
2865
2909
  wireCodexHost: detectCodexHost = wireCodexHost,
2866
2910
  wireCodexPlugin: installCodexPlugin = wireCodexPlugin,
2911
+ hostLockPath = path.join(brainHome, 'host-convergence.lock'),
2867
2912
  runStableSpine = (apply) => spawnSync(
2868
2913
  process.execPath,
2869
2914
  [apply, '--auto', '--expected-version', PACKAGE_VERSION],
@@ -2871,14 +2916,18 @@ export function syncHostsAfterUpdate(cacheDir = resolvedKbDir(), {
2871
2916
  ),
2872
2917
  } = {}) {
2873
2918
  const results = {};
2919
+ const hostLock = acquireHostConvergenceLock(hostLockPath);
2920
+ if (!hostLock.acquired) return { ok: false, results, error: `host synchronization lock unavailable: ${hostLock.error}` };
2874
2921
  let runtimeTransaction;
2875
2922
  try {
2876
2923
  runtimeTransaction = beginConsoleRuntimeTransaction(cacheDir, sourceRoot);
2877
2924
  } catch (error) {
2925
+ hostLock.release();
2878
2926
  return { ok: false, results, error: `Console runtime staging failed: ${error.message}` };
2879
2927
  }
2880
2928
  const fail = (detail = {}) => {
2881
2929
  runtimeTransaction.rollback();
2930
+ hostLock.release();
2882
2931
  return { ok: false, results, ...detail };
2883
2932
  };
2884
2933
 
@@ -2959,6 +3008,7 @@ export function syncHostsAfterUpdate(cacheDir = resolvedKbDir(), {
2959
3008
  },
2960
3009
  consoleRuntime: results.consoleRuntime,
2961
3010
  });
3011
+ hostLock.release();
2962
3012
  return { ok: convergence.healthy === true, convergence, results, applyStatus: applied.status,
2963
3013
  error: convergence.healthy === true ? null : `host convergence is ${convergence.state}: ${convergence.action}` };
2964
3014
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
- "version": "4.3.14",
3
+ "version": "4.3.16",
4
4
  "description": "One-command installer for RuvNet Brain — 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": {
@@ -32,6 +32,7 @@
32
32
  "test:regression": "vitest run tests/regression",
33
33
  "qe:ux": "node scripts/qe/ux-suite.mjs",
34
34
  "test:cov": "vitest run tests/unit --coverage",
35
+ "coverage:source": "node scripts/source-coverage.mjs --check",
35
36
  "test:all": "npm run test:unit && npm run test:mesh && npm run test:mutation && npm run test:regression && npm run test:integration && npm test",
36
37
  "metaharness:receipts": "node scripts/metaharness-receipts.mjs",
37
38
  "route:cheap": "node scripts/route-cheap.mjs",
@@ -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 an enforced UserPromptSubmit retrieve-and-inject grounding hook that sharply reduces drift.",
4
- "version": "4.3.14",
4
+ "version": "4.3.16",
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.14",
3
+ "version": "4.3.16",
4
4
  "description": "Source-grounded RuvNet knowledge, lifecycle enforcement, and learning for Codex.",
5
5
  "author": {
6
6
  "name": "Stuart Kerr"
@@ -343,7 +343,7 @@ async function handleClient(msg) {
343
343
  });
344
344
  }
345
345
  if (!c) {
346
- return clientOk(id, { content: [{ type: 'text', text: `search_ruvnet error: the brain bundle is not installed at ${KB}. Install it with: npx github:stuinfla/ruvnet-brain (or set RUVNET_BRAIN_KB to your brain's kb dir).` }], isError: true });
346
+ return clientOk(id, { content: [{ type: 'text', text: `search_ruvnet error: the brain bundle is unavailable at ${KB}. Diagnose the active installation and KB path before choosing a repair.` }], isError: true });
347
347
  }
348
348
  pendingCount++;
349
349
  try {
@@ -106,6 +106,11 @@ verification → at most one revision. Both hosts design the ADR, DDD and intend
106
106
  Agentic-QE then generates and executes the measurable test artifacts. A host merely completing is
107
107
  not an accepted outcome.
108
108
 
109
+ When the duel returns `learningPersistenceRequest`, pass that request to Ruflo's structured
110
+ `memory_store` tool and verify the exact returned key. The coordinator never starts a second Ruflo
111
+ memory driver itself; without an MCP-aware caller it leaves `learningPersisted: false` and prints
112
+ the pending request so the completed duel is not mistaken for a persisted learning event.
113
+
109
114
  Subscription-only is a billing boundary: verify with `claude auth status --json` and
110
115
  `codex login status`, strip provider API-key variables from both child processes, and never fall back
111
116
  to an API when a subscription is absent or capacity-limited. Subscription use consumes plan
@@ -0,0 +1,115 @@
1
+ // Dream Cycle issue disposition gate.
2
+ //
3
+ // The nightly engine may discover hypotheses, failed probes, or already-fixed findings. None of
4
+ // those is sufficient justification for creating a public issue. This module is deliberately pure:
5
+ // the caller supplies the finding and the current open issues, and receives a disposition before
6
+ // invoking any GitHub write operation.
7
+
8
+ const HEX_SHA = /^[0-9a-f]{7,64}$/i;
9
+ const ISSUE_MARKER = 'dream-fingerprint:';
10
+
11
+ function text(value) {
12
+ return typeof value === 'string' ? value.trim() : '';
13
+ }
14
+
15
+ /** Stable identity used by the ledger, issue body, and open-issue deduplication. */
16
+ export function stableFingerprint(finding = {}) {
17
+ const parts = ['deep', 'scan', 'path', 'signature'].map((key) => text(finding[key]));
18
+ return parts.every(Boolean) ? parts.join('|') : '';
19
+ }
20
+
21
+ export function issueBodyMarker(fingerprint) {
22
+ return fingerprint ? `${ISSUE_MARKER} ${fingerprint}` : '';
23
+ }
24
+
25
+ function sourceBacked(finding) {
26
+ const sourceSha = text(finding.sourceSha || finding.sourceCommit || finding.source?.commit);
27
+ const sourcePaths = finding.sourcePaths || finding.paths || finding.source?.paths;
28
+ const reproduction = finding.reproduction || finding.repro;
29
+ const evidence = Array.isArray(finding.evidence) ? finding.evidence : [];
30
+ if (!HEX_SHA.test(sourceSha)) return false;
31
+ if (!Array.isArray(sourcePaths) || sourcePaths.length === 0 || sourcePaths.some((p) => !text(p))) return false;
32
+ if (!reproduction || !text(reproduction.command) || !text(reproduction.output)) return false;
33
+ return evidence.length > 0 && evidence.every((item) => item && text(item.observed)
34
+ && (text(item.path) || text(item.source) || text(item.sourcePath)));
35
+ }
36
+
37
+ function openIssueFor(fingerprint, openIssues) {
38
+ if (!fingerprint || !Array.isArray(openIssues)) return null;
39
+ const marker = issueBodyMarker(fingerprint);
40
+ return openIssues.find((issue) => text(issue.body).includes(marker)
41
+ || text(issue.title).includes(marker)
42
+ || text(issue.fingerprint) === fingerprint) || null;
43
+ }
44
+
45
+ /**
46
+ * Decide whether a finding may become a GitHub issue.
47
+ *
48
+ * `report` is the safe destination for hypotheses, missing evidence, resolved findings, and
49
+ * findings that are not yet actionable. `dedupe` points at the existing open issue. Only `create`
50
+ * is permission for the caller to invoke `gh issue create`.
51
+ */
52
+ export function assessFinding(finding = {}, { openIssues = [] } = {}) {
53
+ const fingerprint = stableFingerprint(finding);
54
+ const common = { fingerprint: fingerprint || null };
55
+ const status = text(finding.status || finding.state).toLowerCase();
56
+ const skip = new Set((finding.skipIf || []).map((value) => text(value).toLowerCase()));
57
+
58
+ if (!fingerprint) return { ...common, action: 'report', reason: 'missing-stable-fingerprint' };
59
+ if (status === 'hypothesis' || status === 'unreproduced' || status === 'unknown') {
60
+ return { ...common, action: 'report', reason: 'finding-not-reproduced' };
61
+ }
62
+ if (!sourceBacked(finding)) return { ...common, action: 'report', reason: 'missing-source-backed-reproduction' };
63
+ if (skip.has('already-fixed') || skip.has('verified-local-fix') || skip.has('environment-only')
64
+ || finding.resolved === true || finding.fixed === true) {
65
+ return { ...common, action: 'report', reason: 'resolved-or-non-defect' };
66
+ }
67
+ if (status !== 'reproduced' && status !== 'reproducible' && status !== 'confirmed') {
68
+ return { ...common, action: 'report', reason: 'finding-not-confirmed' };
69
+ }
70
+ if (finding.actionable !== true || finding.unresolved !== true || finding.boundedFixUnsuccessful !== true) {
71
+ return { ...common, action: 'report', reason: 'repair-or-actionability-evidence-missing' };
72
+ }
73
+ const existing = openIssueFor(fingerprint, openIssues);
74
+ if (existing) return { ...common, action: 'dedupe', reason: 'duplicate-open-issue', issue: existing };
75
+ return { ...common, action: 'create', reason: 'reproduced-actionable-unresolved-after-bounded-repair' };
76
+ }
77
+
78
+ export function buildIssueBody(finding, decision = assessFinding(finding)) {
79
+ if (decision.action !== 'create') throw new Error(`cannot build issue body for ${decision.action}`);
80
+ const sourceSha = text(finding.sourceSha || finding.sourceCommit || finding.source?.commit);
81
+ return [
82
+ `<!-- ${issueBodyMarker(decision.fingerprint)} -->`,
83
+ `## ${text(finding.title) || 'Dream Cycle finding'}`,
84
+ '',
85
+ text(finding.summary || finding.description),
86
+ '',
87
+ `Source: ${sourceSha}`,
88
+ `Paths: ${(finding.sourcePaths || finding.paths || []).join(', ')}`,
89
+ `Reproduction: \`${text(finding.reproduction?.command)}\``,
90
+ '',
91
+ 'Observed output:',
92
+ '```',
93
+ text(finding.reproduction?.output),
94
+ '```',
95
+ ].join('\n');
96
+ }
97
+
98
+ // Adapter contract for the Dream Machine runner. Input is JSON on stdin:
99
+ // { "finding": { ... }, "openIssues": [{ "number", "title", "body" }] }
100
+ // Output is the disposition JSON. This command intentionally cannot create or mutate GitHub data.
101
+ if (process.argv[1] === new URL(import.meta.url).pathname) {
102
+ let input = '';
103
+ process.stdin.setEncoding('utf8');
104
+ process.stdin.on('data', (chunk) => { input += chunk; });
105
+ process.stdin.on('end', () => {
106
+ try {
107
+ const payload = JSON.parse(input || '{}');
108
+ const decision = assessFinding(payload.finding || payload, { openIssues: payload.openIssues || [] });
109
+ process.stdout.write(`${JSON.stringify(decision)}\n`);
110
+ } catch (error) {
111
+ process.stderr.write(`dream-issue-gate: invalid JSON input: ${error.message}\n`);
112
+ process.exitCode = 2;
113
+ }
114
+ });
115
+ }
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { createHash } from 'node:crypto';
4
- import { spawn, spawnSync } from 'node:child_process';
4
+ import { spawn } from 'node:child_process';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { probeSubscriptionHosts, subscriptionOnlyEnv } from './subscription-hosts.mjs';
7
7
 
@@ -67,15 +67,34 @@ function parseHostValue(host, stdout) {
67
67
  }
68
68
  }
69
69
 
70
- function spawnHost(binary, args, options) {
70
+ function spawnHost(binary, args, options, input = '') {
71
71
  return new Promise((resolve) => {
72
72
  const child = spawn(binary, args, options);
73
73
  let stdout = '';
74
74
  let stderr = '';
75
+ let inputError = null;
76
+ let settled = false;
77
+ const finish = (result) => {
78
+ if (settled) return;
79
+ settled = true;
80
+ resolve(result);
81
+ };
75
82
  child.stdout.on('data', (chunk) => { stdout += chunk; });
76
83
  child.stderr.on('data', (chunk) => { stderr += chunk; });
77
- child.on('error', (error) => resolve({ status: null, stdout, stderr: error.message }));
78
- child.on('close', (status) => resolve({ status, stdout, stderr }));
84
+ child.stdin.on('error', (error) => {
85
+ inputError = error;
86
+ stderr += `${stderr ? '\n' : ''}${error.message}`;
87
+ try { child.kill(); } catch { /* child may already be gone */ }
88
+ });
89
+ child.on('error', (error) => finish({ status: null, stdout, stderr: error.message, error }));
90
+ try {
91
+ child.stdin.end(input);
92
+ } catch (error) {
93
+ inputError = error;
94
+ stderr += `${stderr ? '\n' : ''}${error.message}`;
95
+ try { child.kill(); } catch { /* child may already be gone */ }
96
+ }
97
+ child.on('close', (status) => finish({ status, stdout, stderr, error: inputError }));
79
98
  });
80
99
  }
81
100
 
@@ -87,20 +106,21 @@ export async function runSubscriptionHost(host, stage, payload, { cwd = process.
87
106
  binary: 'claude',
88
107
  args: [
89
108
  '-p', '--output-format', 'json', '--permission-mode', 'plan',
90
- '--tools', 'Read,Grep,Glob', '--no-session-persistence', '--effort', 'high', prompt,
109
+ '--tools', 'Read,Grep,Glob', '--no-session-persistence', '--effort', 'high',
91
110
  ],
92
111
  }
93
112
  : {
94
113
  binary: 'codex',
95
114
  args: [
96
115
  'exec', '--ephemeral', '--sandbox', 'read-only', '--color', 'never', '--json',
97
- '-m', 'gpt-5.6-sol', '-c', 'model_reasoning_effort="high"', prompt,
116
+ '-m', 'gpt-5.6-sol', '-c', 'model_reasoning_effort="high"',
98
117
  ],
99
118
  };
100
- const result = await spawnHost(command.binary, command.args, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
119
+ const result = await spawnHost(command.binary, command.args, { cwd, env, stdio: ['pipe', 'pipe', 'pipe'] }, prompt);
101
120
  if (result.status !== 0) {
102
121
  return {
103
122
  ok: false,
123
+ error: result.stderr || `host exited without a status (${result.status})`,
104
124
  reason: /limit|quota|capacity|usage/i.test(result.stderr)
105
125
  ? 'capacity-limited'
106
126
  : 'host-failed',
@@ -109,11 +129,7 @@ export async function runSubscriptionHost(host, stage, payload, { cwd = process.
109
129
  return { ok: true, value: parseHostValue(host, result.stdout) };
110
130
  }
111
131
 
112
- export async function persistDeliberationReceipt(receipt, {
113
- cwd = process.cwd(),
114
- now = Date.now,
115
- run = spawnSync,
116
- } = {}) {
132
+ export function deliberationMemoryStoreRequest(receipt, { now = Date.now } = {}) {
117
133
  const recordedAt = now();
118
134
  const value = {
119
135
  protocol: receipt.protocol,
@@ -125,21 +141,28 @@ export async function persistDeliberationReceipt(receipt, {
125
141
  recordedAt: new Date(recordedAt).toISOString(),
126
142
  };
127
143
  const key = `dual-deliberation-${recordedAt}-${receipt.taskHash.slice(0, 12)}`;
128
- const result = run('ruflo', [
129
- 'memory', 'store',
130
- '-k', key,
131
- '--value', JSON.stringify(value),
132
- '--namespace', 'ruvnet-brain',
133
- '--path', '.swarm/memory.db',
134
- '--no-upsert',
135
- '--scan-content',
136
- ], {
137
- cwd,
138
- encoding: 'utf8',
139
- env: subscriptionOnlyEnv(),
140
- timeout: 15_000,
141
- });
142
- return result?.status === 0;
144
+ return {
145
+ tool: 'memory_store',
146
+ name: 'memory_store',
147
+ arguments: { key, value: JSON.stringify(value), namespace: 'ruvnet-brain' },
148
+ };
149
+ }
150
+
151
+ function persistenceProof(proof, key) {
152
+ return proof === true || (proof?.stored === true && proof?.verified === true && proof?.key === key);
153
+ }
154
+
155
+ export async function persistDeliberationReceipt(receipt, {
156
+ now = Date.now,
157
+ memoryStore,
158
+ } = {}) {
159
+ const request = deliberationMemoryStoreRequest(receipt, { now });
160
+ if (typeof memoryStore !== 'function') return request;
161
+ try {
162
+ return persistenceProof(await memoryStore(request), request.arguments.key);
163
+ } catch {
164
+ return false;
165
+ }
143
166
  }
144
167
 
145
168
  function missingHosts(probes) {
@@ -164,7 +187,6 @@ export async function deliberate(task, options = {}) {
164
187
  runSubscriptionHost(host, stage, payload, { cwd: options.cwd })
165
188
  ));
166
189
  const cwd = options.cwd ?? process.cwd();
167
- const persist = options.persist ?? ((receipt) => persistDeliberationReceipt(receipt, { cwd }));
168
190
  const eligibleHosts = HOSTS.filter((host) => probes[hostKey(host)]?.eligible);
169
191
 
170
192
  if (eligibleHosts.length === 0) {
@@ -247,7 +269,18 @@ export async function deliberate(task, options = {}) {
247
269
  roles,
248
270
  accepted,
249
271
  };
250
- const learningPersisted = accepted ? Boolean(await persist(receipt)) : false;
272
+ const learningPersistenceRequest = accepted
273
+ ? deliberationMemoryStoreRequest(receipt, { now: options.now ?? Date.now })
274
+ : undefined;
275
+ let learningPersisted = false;
276
+ if (accepted && typeof options.persist === 'function') {
277
+ try {
278
+ const persisted = await options.persist(learningPersistenceRequest, receipt);
279
+ learningPersisted = persistenceProof(persisted, learningPersistenceRequest.arguments.key);
280
+ } catch {
281
+ learningPersisted = false;
282
+ }
283
+ }
251
284
  return {
252
285
  status: accepted ? 'accepted' : 'unresolved',
253
286
  dual: true,
@@ -256,6 +289,7 @@ export async function deliberate(task, options = {}) {
256
289
  verification: verification.ok ? verification.value : undefined,
257
290
  verifiedOutcome: accepted,
258
291
  learningPersisted,
292
+ ...(learningPersistenceRequest ? { learningPersistenceRequest } : {}),
259
293
  };
260
294
  }
261
295
 
@@ -37,9 +37,15 @@ export function observeNpxPackage(npmCache, expected) {
37
37
  export function validateNpxObservations(proof) {
38
38
  const expected = proof.packageExecution?.expected;
39
39
  const observations = proof.packageExecution?.observations;
40
- if (proof.packageExecution?.policy !== 'production-latest-exact-cache-v1'
41
- || proof.registration?.packageTarget?.spec !== 'ruvnet-brain@latest'
42
- || proof.registration.packageTarget.sha256 !== null
40
+ const policy = proof.packageExecution?.policy;
41
+ const target = proof.registration?.packageTarget;
42
+ const productionLatest = policy === 'production-latest-exact-cache-v1'
43
+ && target?.spec === 'ruvnet-brain@latest' && target.sha256 === null;
44
+ const sealedCandidate = policy === 'sealed-candidate-exact-cache-v1'
45
+ && typeof target?.spec === 'string' && target.spec.endsWith('.tgz')
46
+ && /^[a-f0-9]{64}$/.test(String(target.sha256 || ''))
47
+ && target.sha256 === proof.candidate?.sha256;
48
+ if ((!productionLatest && !sealedCandidate)
43
49
  || expected?.packageSha256 !== proof.candidate?.sha256
44
50
  || expected?.name !== 'ruvnet-brain' || expected.version !== proof.candidate?.version
45
51
  || !/^[a-f0-9]{64}$/.test(expected.treeSha256 || '') || !Number.isSafeInteger(expected.fileCount)
@@ -180,6 +180,127 @@ export async function triggerNativeRun({ scheduler, registration, platform, env,
180
180
  }
181
181
  }
182
182
 
183
+ /**
184
+ * Fast release proof for the native scheduler boundary. This deliberately checks only the
185
+ * scheduler's real registration, load, trigger dispatch, and cleanup. The expensive two-run
186
+ * updater lifecycle remains a scheduled/post-publication soak and must not block every release.
187
+ */
188
+ export async function runNativeSchedulerSmoke({ packageRoot, packagePath, bundlePath, sourceSha, workflowRunId,
189
+ scheduler, env, brainHome, kbDir, now = Date.now, command = run } = {}) {
190
+ if (!['darwin', 'linux', 'win32'].includes(process.platform)) {
191
+ throw new Error(`unsupported native scheduler platform: ${process.platform}`);
192
+ }
193
+ if (!packageRoot) throw new Error('scheduler smoke package root is missing');
194
+ const packageRootStat = fs.lstatSync(packageRoot);
195
+ if (!packageRootStat.isDirectory() || packageRootStat.isSymbolicLink()) {
196
+ throw new Error('scheduler smoke package root is not a regular directory');
197
+ }
198
+ const packageArchive = path.resolve(packagePath || '');
199
+ const packageStat = fs.lstatSync(packageArchive);
200
+ if (!packageStat.isFile() || packageStat.isSymbolicLink() || !packageArchive.endsWith('.tgz')) {
201
+ throw new Error('scheduler smoke package archive is not a regular file');
202
+ }
203
+ const bundle = path.resolve(bundlePath || '');
204
+ const stat = fs.lstatSync(bundle);
205
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('scheduler smoke bundle is not a regular file');
206
+ if (!/^[a-f0-9]{40}$/.test(String(sourceSha || ''))) throw new Error('scheduler smoke source SHA is invalid');
207
+ if (!/^\d+$/.test(String(workflowRunId || ''))) throw new Error('scheduler smoke workflow run ID is invalid');
208
+ if (!scheduler || typeof scheduler.installNightlyRunner !== 'function'
209
+ || typeof scheduler.installScheduler !== 'function' || typeof scheduler.schedulerStatus !== 'function'
210
+ || typeof scheduler.removeScheduler !== 'function') throw new Error('scheduler smoke adapter is incomplete');
211
+
212
+ const identity = `com.ruvnet.brain-update.proof-smoke-${process.pid}-${crypto.randomBytes(6).toString('hex')}`;
213
+ const packageSha256 = sha256(fs.readFileSync(packageArchive));
214
+ const bundleSha256 = sha256(fs.readFileSync(bundle));
215
+ const registration = scheduler.installNightlyRunner({ brainHome,
216
+ source: path.join(packageRoot, 'bin', 'nightly-refresh.mjs'), nodePath: process.execPath, identity, env,
217
+ packageTarget: { spec: packageArchive, sha256: packageSha256 },
218
+ bundleTarget: { spec: bundle, sha256: bundleSha256 } });
219
+ // A hosted runner can expose a native scheduler binary whose IPC endpoint is unavailable.
220
+ // Every scheduler operation is therefore bounded independently; a stuck launchd/schtasks
221
+ // call must become a typed failure, never consume the entire release job timeout.
222
+ const schedulerRun = (name, args, options = {}) => spawnSync(name, args, {
223
+ encoding: 'utf8', timeout: 30_000, ...options,
224
+ });
225
+ const options = { platform: process.platform, env, brainHome, kbDir, identity, run: schedulerRun,
226
+ proofTick: process.platform === 'linux', proofAt: process.platform === 'linux' ? now() + 60_000 : undefined,
227
+ pathValue: env.PATH };
228
+ const before = scheduler.schedulerStatus(options);
229
+ if (before.state !== 'off') throw new Error(`scheduler smoke identity is not absent: ${before.evidence}`);
230
+ let installed = false;
231
+ let cleaned = false;
232
+ try {
233
+ const result = scheduler.installScheduler(registration, options);
234
+ if (!result.ok) throw new Error(`native scheduler smoke install failed: ${result.why}`);
235
+ installed = true;
236
+ const loaded = scheduler.schedulerStatus(options);
237
+ if (loaded.state !== 'on') throw new Error(`native scheduler smoke was not loaded: ${loaded.evidence}`);
238
+ if (process.platform === 'darwin') {
239
+ const uid = typeof process.getuid === 'function' ? process.getuid() : 0;
240
+ command('launchctl', ['kickstart', '-k', `gui/${uid}/${identity}`], { timeout: 30_000 });
241
+ } else if (process.platform === 'win32') {
242
+ command('schtasks', ['/Run', '/TN', identity], { timeout: 30_000 });
243
+ }
244
+ const removed = scheduler.removeScheduler({ ...options, platform: process.platform, env, identity });
245
+ if (!removed.ok) throw new Error(`native scheduler smoke cleanup failed: ${removed.why}`);
246
+ cleaned = true;
247
+ const absent = scheduler.schedulerStatus(options);
248
+ if (absent.state !== 'off') throw new Error(`native scheduler smoke absence was not verified: ${absent.evidence}`);
249
+ return {
250
+ schemaVersion: 1,
251
+ kind: 'ruvnet-brain-native-scheduler-smoke',
252
+ scope: 'public-release-scheduler-boundary',
253
+ platform: process.platform,
254
+ sourceSha: String(sourceSha),
255
+ workflowRunId: String(workflowRunId),
256
+ identity,
257
+ registration: {
258
+ recordPath: registration.recordPath, nodePath: registration.nodePath,
259
+ runnerPath: registration.runnerPath, runnerSha256: registration.runnerSha256,
260
+ packageTarget: registration.packageTarget, bundleTarget: registration.bundleTarget,
261
+ },
262
+ trigger: { kind: { darwin: 'launchctl-kickstart', linux: 'cron-registration', win32: 'schtasks-run' }[process.platform], identity },
263
+ loaded: true,
264
+ cleaned: true,
265
+ observedAt: new Date().toISOString(),
266
+ };
267
+ } finally {
268
+ if (installed && !cleaned) {
269
+ try { scheduler.removeScheduler({ ...options, platform: process.platform, env, identity }); } catch { /* preserve original failure */ }
270
+ }
271
+ }
272
+ }
273
+
274
+ export function validateNativeSchedulerSmoke(smoke, { platform, sourceSha, workflowRunId,
275
+ bundleSha256, packageSha256 } = {}) {
276
+ if (!smoke || typeof smoke !== 'object') {
277
+ return { ok: false, failures: ['invalid native nightly proof: scheduler smoke receipt is missing'] };
278
+ }
279
+ const failures = [];
280
+ if (smoke?.schemaVersion !== 1 || smoke?.kind !== 'ruvnet-brain-native-scheduler-smoke'
281
+ || smoke.scope !== 'public-release-scheduler-boundary') failures.push('scheduler smoke envelope is invalid');
282
+ if (smoke.platform !== platform || smoke.sourceSha !== sourceSha
283
+ || String(smoke.workflowRunId || '') !== String(workflowRunId || '')) failures.push('scheduler smoke identity differs');
284
+ if (!/^com\.ruvnet\.brain-update\.proof-smoke-[A-Za-z0-9._-]+$/.test(smoke.identity || '')) failures.push('scheduler smoke identity is not isolated');
285
+ if (smoke.loaded !== true || smoke.cleaned !== true || smoke.trigger?.identity !== smoke.identity) failures.push('scheduler smoke did not prove load, trigger, and cleanup');
286
+ const expectedTrigger = { darwin: 'launchctl-kickstart', linux: 'cron-registration', win32: 'schtasks-run' }[platform];
287
+ if (smoke.trigger?.kind !== expectedTrigger) failures.push('scheduler smoke trigger differs');
288
+ const registration = smoke.registration;
289
+ // Receipts from a Windows runner are finalized on Ubuntu. Validate the path syntax emitted by
290
+ // the producing OS instead of applying the verifier host's POSIX rules to drive-letter paths.
291
+ const nativePath = platform === 'win32' ? path.win32 : path;
292
+ if (!registration || !/^[a-f0-9]{64}$/.test(registration.runnerSha256 || '')
293
+ || !/^[a-f0-9]{64}$/.test(registration.bundleTarget?.sha256 || '')
294
+ || registration.bundleTarget.sha256 !== bundleSha256
295
+ || !/^[a-f0-9]{64}$/.test(registration.packageTarget?.sha256 || '')
296
+ || (packageSha256 !== undefined && registration.packageTarget.sha256 !== packageSha256)
297
+ || !nativePath.isAbsolute(registration.packageTarget?.spec || '') || !registration.packageTarget.spec.endsWith('.tgz')
298
+ || !nativePath.isAbsolute(registration.recordPath || '') || !nativePath.isAbsolute(registration.nodePath || '')
299
+ || !nativePath.isAbsolute(registration.runnerPath || '')) failures.push('scheduler smoke registration identity is incomplete');
300
+ if (!Number.isFinite(Date.parse(smoke.observedAt)) || Date.parse(smoke.observedAt) > Date.now() + 60_000) failures.push('scheduler smoke observation time is invalid');
301
+ return { ok: failures.length === 0, failures };
302
+ }
303
+
183
304
  export function validateTwoRunEvidence({ first, second, inventoryBefore, inventoryAfterFirst,
184
305
  inventoryAfterSecond, retention, validateEnvelope, identity }) {
185
306
  const failures = [];
@@ -385,8 +506,15 @@ export async function runNightlyTwoRunProof({ packagePath, bundlePath, out, time
385
506
  const packageRoot = path.join(prefix, 'node_modules', 'ruvnet-brain');
386
507
  const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
387
508
  const expectedPackage = packageTreeObservation(packageRoot);
388
- // Exercise the production target, not npx's ambiguous positional local-tarball path.
389
- const prepare = npmInvocation(['exec', '--yes', '--package=ruvnet-brain@latest', '--', 'ruvnet-brain', '--help']);
509
+ // Release verification already downloaded and hashed the exact npm bytes above. Reusing
510
+ // those bytes keeps the per-release scheduler smoke deterministic and avoids a second
511
+ // registry/latest resolution. The unattended production nightly still uses @latest.
512
+ const packageTarget = {
513
+ spec: candidate,
514
+ sha256: packageSha256,
515
+ policy: 'sealed-candidate-exact-cache-v1',
516
+ };
517
+ const prepare = npmInvocation(['exec', '--yes', `--package=${packageTarget.spec}`, '--', 'ruvnet-brain', '--help']);
390
518
  run(prepare.executable, prepare.args, { env, cwd: home });
391
519
  const packageObservations = [observeNpxPackage(npmCache, expectedPackage)];
392
520
  // Register the exact public package's plugin in this fresh home. GitHub shorthand can
@@ -400,7 +528,7 @@ export async function runNightlyTwoRunProof({ packagePath, bundlePath, out, time
400
528
  scheduler = await import(`${pathToFileURL(path.join(packageRoot, 'plugin', 'scripts', 'nightly-scheduler.mjs')).href}?proof=${Date.now()}`);
401
529
  registration = scheduler.installNightlyRunner({ brainHome,
402
530
  source: path.join(packageRoot, 'bin', 'nightly-refresh.mjs'), nodePath: process.execPath, identity, env,
403
- packageTarget: { spec: 'ruvnet-brain@latest', sha256: null },
531
+ packageTarget: { spec: packageTarget.spec, sha256: packageTarget.sha256 },
404
532
  bundleTarget: { spec: stagedBundle.sourcePath, sha256: stagedBundle.sha256 } });
405
533
  const inventoryBefore = managedStorageInventory(kbDir);
406
534
  const kick = () => triggerNativeRun({ scheduler, registration, platform: process.platform,
@@ -431,7 +559,7 @@ export async function runNightlyTwoRunProof({ packagePath, bundlePath, out, time
431
559
  kind: 'ruvnet-brain-native-two-run-nightly-proof',
432
560
  scope: 'installed-update',
433
561
  installationConfiguration: { npmBinLinks: false, source: 'initial-reader-install-only', removedEmptyScaffolds },
434
- packageExecution: { policy: 'production-latest-exact-cache-v1',
562
+ packageExecution: { policy: packageTarget.policy,
435
563
  expected: { ...expectedPackage, packageSha256 }, observations: packageObservations },
436
564
  upstreamFreshness: 'UNKNOWN',
437
565
  observedAt: new Date().toISOString(),
@@ -6,7 +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
+ import { validateNightlyProofReceipt, validateNativeSchedulerSmoke } from './nightly-two-run-proof.mjs';
10
10
 
11
11
  export const PUBLIC_VERIFICATION_OS = Object.freeze(['linux', 'macos', 'windows']);
12
12
  export const PUBLIC_VERIFICATION_MODES = Object.freeze(['claude', 'codex', 'dual']);
@@ -63,12 +63,21 @@ export function validatePublicVerificationLeaf(leaf, { publicKey } = {}) {
63
63
  }
64
64
  if (leaf.mode === 'dual') {
65
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('; ')}`);
66
+ if (leaf.nativeNightly !== undefined) {
67
+ const native = validateNightlyProofReceipt(leaf.nativeNightly, {
68
+ platform: { linux: 'linux', macos: 'darwin', windows: 'win32' }[leaf.os],
69
+ version: leaf.version, sourceSha: leaf.sourceSha, workflowRunId: leaf.workflowRunId,
70
+ packageSha256: leaf.artifactSha256, bundleSha256: leaf.bundleSha256, publicKey,
71
+ });
72
+ if (!native.ok) throw new Error(`${leaf.os}/dual native nightly proof failed: ${native.failures.join('; ')}`);
73
+ } else {
74
+ const smoke = validateNativeSchedulerSmoke(leaf.nativeSchedulerSmoke, {
75
+ platform: { linux: 'linux', macos: 'darwin', windows: 'win32' }[leaf.os],
76
+ sourceSha: leaf.sourceSha, workflowRunId: leaf.workflowRunId, bundleSha256: leaf.bundleSha256,
77
+ packageSha256: leaf.artifactSha256,
78
+ });
79
+ if (!smoke.ok) throw new Error(`${leaf.os}/dual native scheduler smoke failed: ${smoke.failures.join('; ')}`);
80
+ }
72
81
  } else if (leaf.nativeNightly !== undefined) {
73
82
  throw new Error('native nightly proof belongs only to the dual-host leaf');
74
83
  }
@@ -178,18 +178,26 @@ export async function createPublicVerificationLane({
178
178
  failures.push({ mode, reason: error.message, retrieval });
179
179
  continue;
180
180
  }
181
- // Native scheduler lifecycle is qualified in the reviewed cross-platform
182
- // release suite. The live public adapter opts out because that proof relies
183
- // on maintainer-only host tooling; test adapters still exercise the seam.
181
+ // Native scheduler lifecycle is part of the accepted release contract. Keep
182
+ // the public lane bound to the same proof required by the leaf/aggregate
183
+ // validators; an adapter cannot silently downgrade a dual-host leaf.
184
184
  let nativeNightly;
185
- if (mode === 'dual' && adapter.publicNativeNightly !== false) {
186
- if (typeof adapter.runNativeNightly !== 'function') throw new Error('native nightly proof adapter is required');
187
- nativeNightly = await adapter.runNativeNightly({ identity, workflowRunId: String(workflowRunId) });
185
+ let nativeSchedulerSmoke;
186
+ if (mode === 'dual') {
187
+ const schedulerMode = process.env.RUVNET_PUBLIC_SCHEDULER_MODE || 'full';
188
+ if (schedulerMode === 'smoke') {
189
+ if (typeof adapter.runNativeNightlySmoke !== 'function') throw new Error('native scheduler smoke adapter is required');
190
+ nativeSchedulerSmoke = await adapter.runNativeNightlySmoke({ identity, workflowRunId: String(workflowRunId) });
191
+ } else {
192
+ if (typeof adapter.runNativeNightly !== 'function') throw new Error('native nightly proof adapter is required');
193
+ nativeNightly = await adapter.runNativeNightly({ identity, workflowRunId: String(workflowRunId) });
194
+ }
188
195
  }
189
196
  leaves.push(createPublicVerificationLeaf({
190
197
  ...common,
191
198
  ...(publication.acceptancePolicy ? { acceptancePolicy: publication.acceptancePolicy, searchTiming: { firstSearchMs: installed.searchMs, broadMs: publication.brain.broadMs, deadlineMs: publication.brain.deadlineMs } } : {}),
192
199
  ...(nativeNightly === undefined ? {} : { nativeNightly }),
200
+ ...(nativeSchedulerSmoke === undefined ? {} : { nativeSchedulerSmoke }),
193
201
  os,
194
202
  mode,
195
203
  status: verifiedRetrieval.receiptSha256 === retrieval.receiptSha256 ? 'completed' : 'failed',
@@ -20,7 +20,12 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
20
20
  import { evaluateCandidateReceipt, evaluatePublicationReceipt } from './release-proof.mjs';
21
21
  import { verifyPayload } from './release-payload.mjs';
22
22
  import { resolveInstalledCanaryCitation } from './retrieval-canary.mjs';
23
- import { runNightlyTwoRunProof, validateNightlyProofReceipt } from './nightly-two-run-proof.mjs';
23
+ import {
24
+ runNativeSchedulerSmoke,
25
+ runNightlyTwoRunProof,
26
+ validateNativeSchedulerSmoke,
27
+ validateNightlyProofReceipt,
28
+ } from './nightly-two-run-proof.mjs';
24
29
  import { parseRetrievalResult } from '../kb/retrieval-result.mjs';
25
30
 
26
31
  const REPO = 'stuinfla/ruvnet-brain';
@@ -59,6 +64,39 @@ function command(name, args, options = {}) {
59
64
  }
60
65
  return String(result.stdout || '').trim();
61
66
  }
67
+
68
+ // The public host matrix owns three independent install fixtures. Keep their long-running
69
+ // installer/doctor processes asynchronous: a synchronous child here would block Node's event loop,
70
+ // freezing an already-started MCP deadline while the next host is being installed. That was the
71
+ // concrete reason the macOS recovery lane reported a false 30-second Brain timeout after all three
72
+ // installs had succeeded. Short metadata probes still use command() above; only the host work uses
73
+ // this bounded async boundary.
74
+ function commandAsync(name, args, options = {}) {
75
+ const { platform = process.platform, timeout, ...spawnOptions } = options;
76
+ const invocation = commandInvocation(name, args, { platform, env: spawnOptions.env || process.env });
77
+ return new Promise((resolve, reject) => {
78
+ const child = spawn(invocation.executable, invocation.args, spawnOptions);
79
+ let settled = false;
80
+ let timer;
81
+ const finish = (error, value = '') => {
82
+ if (settled) return;
83
+ settled = true;
84
+ if (timer) clearTimeout(timer);
85
+ error ? reject(error) : resolve(value);
86
+ };
87
+ if (timeout !== undefined) {
88
+ timer = setTimeout(() => {
89
+ child.kill('SIGTERM');
90
+ finish(new Error(`${name} ${args.join(' ')} exceeded ${timeout}ms deadline`));
91
+ }, timeout);
92
+ }
93
+ child.once('error', (error) => finish(error));
94
+ child.once('close', (code, signal) => {
95
+ if (code === 0) finish(null);
96
+ else finish(new Error(`${name} ${args.join(' ')} failed: ${signal || `exit ${code}`}`));
97
+ });
98
+ });
99
+ }
62
100
  export function validateCandidateSource(root, { sha, version }) {
63
101
  const candidateRoot = fs.realpathSync(root);
64
102
  const options = { cwd: candidateRoot };
@@ -302,8 +340,9 @@ export function livePublicationAdapter({ root = process.cwd(), candidateRoot = r
302
340
  const mcpSessions = new Map();
303
341
  const passageFileDigests = new Map();
304
342
  let installTemp = null;
343
+ let installedPackageRoot = null;
344
+ let installedBundlePath = null;
305
345
  return {
306
- publicNativeNightly: false,
307
346
  async downloadNpm({ version, destination }) {
308
347
  const metadata = JSON.parse(command('npm', ['view', `${PACKAGE}@${version}`, '--json']));
309
348
  if (metadata.version !== version || !metadata.dist?.tarball) {
@@ -340,8 +379,12 @@ export function livePublicationAdapter({ root = process.cwd(), candidateRoot = r
340
379
  if (sha256(npm.path) !== identity.packageSha256 || sha256(bundle.path) !== identity.bundleSha256
341
380
  || npm.version !== identity.version || bundle.sha !== identity.candidateSha
342
381
  || signature.sha !== identity.candidateSha) throw new Error('nightly public artifact identity mismatch');
382
+ const timeoutMs = Number(process.env.RUVNET_PUBLIC_NATIVE_TIMEOUT_MS || 10 * 60 * 1000);
383
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 60_000) {
384
+ throw new Error('RUVNET_PUBLIC_NATIVE_TIMEOUT_MS must be an integer of at least 60000ms');
385
+ }
343
386
  const proof = await runNightlyTwoRunProof({ packagePath: npm.path, bundlePath: bundle.path,
344
- signaturePath: signature.path, sourceSha: identity.candidateSha, workflowRunId,
387
+ signaturePath: signature.path, sourceSha: identity.candidateSha, workflowRunId, timeoutMs,
345
388
  out: path.join(temp, 'nightly-proof.json') });
346
389
  const validation = validateNightlyProofReceipt(proof, { platform: process.platform, version: identity.version,
347
390
  packageSha256: identity.packageSha256, bundleSha256: identity.bundleSha256,
@@ -355,6 +398,44 @@ export function livePublicationAdapter({ root = process.cwd(), candidateRoot = r
355
398
  }
356
399
  },
357
400
 
401
+ async runNativeNightlySmoke({ identity, workflowRunId }) {
402
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-public-scheduler-smoke-'));
403
+ try {
404
+ const npm = await this.downloadNpm({ version: identity.version,
405
+ destination: path.join(temp, 'package.tgz') });
406
+ const bundle = await this.downloadGithub({ tag: identity.tag, assetName: 'ruvnet-brain.zip',
407
+ destination: path.join(temp, 'ruvnet-brain.zip') });
408
+ if (sha256(npm.path) !== identity.packageSha256 || sha256(bundle.path) !== identity.bundleSha256
409
+ || npm.version !== identity.version || bundle.sha !== identity.candidateSha) {
410
+ throw new Error('scheduler smoke public artifact identity mismatch');
411
+ }
412
+ const packageRoot = installedPackageRoot || path.join(temp, 'package');
413
+ if (!installedPackageRoot) {
414
+ const extraction = tarExtractionInvocation(npm.path, temp);
415
+ command('tar', extraction.args, { cwd: extraction.cwd });
416
+ if (!fs.existsSync(packageRoot)) throw new Error('scheduler smoke package extraction failed');
417
+ }
418
+ const brainHome = path.join(temp, 'brain');
419
+ const smokeHome = path.join(temp, 'home');
420
+ fs.mkdirSync(smokeHome, { recursive: true });
421
+ const smokeEnv = { ...process.env, HOME: smokeHome, USERPROFILE: smokeHome,
422
+ RUVNET_BRAIN_HOME: brainHome, RUVNET_BRAIN_KB: path.join(brainHome, 'kb'),
423
+ PATH: process.env.PATH || '' };
424
+ const smoke = await runNativeSchedulerSmoke({ packageRoot, packagePath: npm.path,
425
+ bundlePath: installedBundlePath || bundle.path, sourceSha: identity.candidateSha,
426
+ workflowRunId, env: smokeEnv, brainHome, kbDir: path.join(brainHome, 'kb'),
427
+ scheduler: await import(`${pathToFileURL(path.join(packageRoot, 'plugin', 'scripts', 'nightly-scheduler.mjs')).href}?smoke=${Date.now()}`) });
428
+ const validation = validateNativeSchedulerSmoke(smoke, {
429
+ platform: process.platform, sourceSha: identity.candidateSha,
430
+ workflowRunId, bundleSha256: identity.bundleSha256, packageSha256: identity.packageSha256,
431
+ });
432
+ if (!validation.ok) throw new Error(`scheduler smoke failed: ${validation.failures.join('; ')}`);
433
+ return smoke;
434
+ } finally {
435
+ fs.rmSync(temp, { recursive: true, force: true });
436
+ }
437
+ },
438
+
358
439
  async installHosts({ artifactPath, artifactSha256, bundlePath, bundleSha256, version }) {
359
440
  const temp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-public-install-')));
360
441
  installTemp = temp;
@@ -362,14 +443,20 @@ export function livePublicationAdapter({ root = process.cwd(), candidateRoot = r
362
443
  const extraction = tarExtractionInvocation(artifactPath, temp);
363
444
  command('tar', extraction.args, { cwd: extraction.cwd });
364
445
  stageVerifiedBundle({ bundlePath, bundleSha256, packageRoot });
446
+ installedPackageRoot = packageRoot;
447
+ installedBundlePath = path.join(packageRoot, 'dist', 'ruvnet-brain.zip');
365
448
  const sealedPlugin = path.join(packageRoot, 'plugin');
366
449
  const results = {};
367
450
  let bundle = null;
368
451
  const sharedModelCache = path.join(temp, 'models-cache');
369
452
  fs.mkdirSync(sharedModelCache, { recursive: true });
370
- // Derived from HOST_MODES, so a fourth host shape is added in ONE place and this loop
371
- // cannot fall behind the staged-side check the way it did.
372
- for (const mode of HOST_MODES.map((m) => RECEIPT_MODE_NAMES[m])) {
453
+ // Derived from HOST_MODES, so a fourth host shape is added in ONE place and this matrix
454
+ // cannot fall behind the staged-side check the way it did. Each host owns its HOME and
455
+ // mutable state, so installation runs concurrently rather than adding three cold-start
456
+ // durations together on slower runners. Search is deliberately a second phase: one real
457
+ // query warms the shared model cache before the other two hosts start their MCP workers.
458
+ const hostResults = await Promise.all(HOST_MODES.map(async (hostMode) => {
459
+ const mode = RECEIPT_MODE_NAMES[hostMode];
373
460
  console.log(`Public verification: installing ${mode} on ${process.platform}`);
374
461
  const home = path.join(temp, `home-${mode}`);
375
462
  const codexHome = path.join(home, '.codex');
@@ -398,12 +485,12 @@ export function livePublicationAdapter({ root = process.cwd(), candidateRoot = r
398
485
  ? { CODEX_BIN: nativeCodexExecutable() } : {}),
399
486
  };
400
487
  if (mode !== 'claudeOnly') {
401
- command(env.CODEX_BIN || 'codex', ['--version'], { env, timeout: 30_000, stdio: 'inherit' });
402
- command(env.CODEX_BIN || 'codex', ['plugin', 'list', '--json'], { env, timeout: 30_000, stdio: 'inherit' });
488
+ await commandAsync(env.CODEX_BIN || 'codex', ['--version'], { env, timeout: 30_000, stdio: 'inherit' });
489
+ await commandAsync(env.CODEX_BIN || 'codex', ['plugin', 'list', '--json'], { env, timeout: 30_000, stdio: 'inherit' });
403
490
  }
404
- if (process.platform === 'win32') command('git', ['--version'], { env, timeout: 30_000, stdio: 'inherit' });
491
+ if (process.platform === 'win32') await commandAsync('git', ['--version'], { env, timeout: 30_000, stdio: 'inherit' });
405
492
  const installer = path.join(packageRoot, 'bin', 'install.mjs');
406
- command(process.execPath, [
493
+ await commandAsync(process.execPath, [
407
494
  installer, '--yes', '--force', '--version', `v${version}`,
408
495
  '--no-nightly-prompt', '--no-telemetry', '--no-stack', '--no-enhance', '--no-statusline',
409
496
  '--no-selfcheck', '--no-verify',
@@ -425,21 +512,44 @@ export function livePublicationAdapter({ root = process.cwd(), candidateRoot = r
425
512
  throw new Error(`${mode} installed public Brain bundle version mismatch`);
426
513
  }
427
514
  bundle ||= { brainVersion: source.brainVersion, releaseTag: source.releaseTag };
428
- const searched = await rpcSearch(findMcpServer(home), env,
429
- 'How does RuvNet Brain prove a public release artifact?', 5, DEADLINE_MS);
430
- command(process.execPath, [installer, '--doctor', '--hooks'], {
431
- env, cwd: packageRoot, timeout: 300_000, stdio: 'inherit',
515
+ return {
516
+ mode,
517
+ verified,
518
+ context: { temp, home, codexHome, brainHome, kb, env, packageRoot },
519
+ installer,
520
+ bundle: { brainVersion: source.brainVersion, releaseTag: source.releaseTag },
521
+ };
522
+ }));
523
+
524
+ // Warm one installed worker before opening the other two. Concurrent first-run model loads
525
+ // contend for the same cache and can exceed the public 30-second search acceptance window;
526
+ // this keeps the window strict while making the expensive work bounded and parallel.
527
+ const searched = new Map();
528
+ const first = hostResults[0];
529
+ searched.set(first.mode, await rpcSearch(findMcpServer(first.context.home), first.context.env,
530
+ 'How does RuvNet Brain prove a public release artifact?', 5, DEADLINE_MS));
531
+ await Promise.all(hostResults.slice(1).map(async ({ mode, context }) => {
532
+ searched.set(mode, await rpcSearch(findMcpServer(context.home), context.env,
533
+ 'How does RuvNet Brain prove a public release artifact?', 5, DEADLINE_MS));
534
+ }));
535
+ await Promise.all(hostResults.map(async ({ context, installer }) => {
536
+ await commandAsync(process.execPath, [installer, '--doctor', '--hooks'], {
537
+ env: context.env, cwd: packageRoot, timeout: 300_000, stdio: 'inherit',
432
538
  });
539
+ }));
540
+
541
+ for (const { mode, verified, context } of hostResults) {
542
+ const search = searched.get(mode);
433
543
  results[mode] = {
434
544
  status: 'PASS', doctorExit: 0, version, artifactSha256,
435
- functionalSearch: true, searchMs: searched.broadMs, hostsOnPath: mode,
545
+ functionalSearch: true, searchMs: search.broadMs, hostsOnPath: mode,
436
546
  ...verified,
437
547
  };
438
- installContexts.set(MODE_FROM_RECEIPT_NAME[mode], { temp, home, codexHome, brainHome, kb, env, packageRoot });
548
+ installContexts.set(MODE_FROM_RECEIPT_NAME[mode], context);
439
549
  }
440
550
  return {
441
551
  ...results,
442
- bundle,
552
+ bundle: hostResults[0]?.bundle || bundle,
443
553
  };
444
554
  },
445
555
 
@@ -30,6 +30,17 @@ const LANES = Object.freeze({
30
30
  { kind: 'command', command: npm, args: ['run', 'version:check'], timeoutMs: 60_000 },
31
31
  vitest(['tests/qe/gpt56/critical-risk-map.test.mjs'], 60_000),
32
32
  ],
33
+ // Early public-artifact smoke: exercise the exact packed installer and host convergence on
34
+ // every OS before publication. This is deliberately smaller than terminal public verification:
35
+ // it proves the candidate package boundary early, while the protected post-publication matrix
36
+ // still owns public bytes, retrieval canaries, and native scheduled-update proof.
37
+ 'early-public': [
38
+ vitest([
39
+ 'tests/qe/release/packed-clean-install.test.mjs',
40
+ 'tests/qe/release/issue-64-host-convergence.test.mjs',
41
+ 'tests/unit/npm-tarball-codex.test.mjs',
42
+ ], 180_000),
43
+ ],
33
44
  check: [
34
45
  vitest([
35
46
  'tests/qe/gpt56/critical-risk-map.test.mjs',
@@ -202,6 +202,12 @@ export function liveReleaseProvider({ root = process.cwd() } = {}) {
202
202
  ? receiptsFor(hydratedRelease(releaseById(matchingDrafts[0].id))) : [];
203
203
  const latestByTransaction = new Map();
204
204
  for (const release of all) {
205
+ // Draft-release assets are not readable through the public GitHub API (they return 404
206
+ // even though the release metadata is visible). The matching candidate above is read
207
+ // explicitly because the publisher owns it; unrelated drafts are not public channel
208
+ // state and must not make every subsequent release fail while scanning history. Published
209
+ // releases remain fully receipt-gated and continue to block competing transactions.
210
+ if (release.draft && release.tag_name !== identity.tag) continue;
205
211
  for (const receipt of receiptsFor(hydratedRelease(release))) {
206
212
  const prior = latestByTransaction.get(receipt.transactionId);
207
213
  if (!prior || receipt.sequence > prior.receipt.sequence) {