ruvnet-brain 4.5.11 โ†’ 4.5.13

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.5.11 โ€” updated 2026-07-30 03:24 EDT](https://img.shields.io/badge/version_4.5.11-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.5.13 โ€” updated 2026-07-30 03:24 EDT](https://img.shields.io/badge/version_4.5.13-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
 
@@ -273,7 +273,7 @@ The honest limit: this is **3.5, not 4.0**. The advocacy surface is live and the
273
273
 
274
274
  The 2.4 router was **hand-rolled**: our own heuristic with a placeholder policy, presented as "the MetaHarness router." rUv had already shipped the real thing. 2.5 replaces it with **[`@metaharness/router`](https://www.npmjs.com/package/@metaharness/router)** โ€” his actual learned cost-optimal router (k-NN over labelled embeddings, the productized DRACO Phase-2 finding, ADR-040/043).
275
275
 
276
- Our code now does **one honest job**: a price transform. A model your subscription already covers becomes `costPerMTok = 0`, and rUv's router does the rest natively โ€” a $0 model that clears the quality bar simply *is* the cheapest sufficient candidate. Cost-optimal routing and "already paid for" compose; they never competed.
276
+ Subscription membership is an eligibility constraint, not a measurement of allowance consumption. The managed terminal workflow uses the current per-user model and effort policy, supervised native workers, executable checks, independent review, and execution receipts. MetaHarness supplies routing and evaluation capabilities; registration or a price transform alone does not execute a prompt or prove a handoff. The supported entry paths and remaining boundaries are described in `docs/model-routing-operation.md`.
277
277
 
278
278
  > **`npm run substitution:check`** โ€” a new CI gate that fails the build if any code implements a capability rUv already ships *and* wears his name without either using the real package or openly disclosing the hand-roll. **Verified to fire on the exact commit where we got this wrong.** You may hand-roll. You may never hand-roll *silently*.
279
279
 
package/bin/install.mjs CHANGED
@@ -14,6 +14,7 @@
14
14
  import { downloadFileWithRetry, httpsJsonWithRetry } from '../kb/download-retry.mjs';
15
15
  import fs from 'node:fs';
16
16
  import os from 'node:os';
17
+ import { installCodexConsoleAlias } from '../plugin/scripts/codex-console-alias.mjs';
17
18
  import path from 'node:path';
18
19
  import { spawn, spawnSync } from 'node:child_process';
19
20
  import { readAll as readAllReadiness, aggregate as aggregateReadiness } from '../plugin/scripts/mcp-readiness.mjs';
@@ -82,7 +83,7 @@ import { shellDiff as pluginShellDiff } from '../plugin/scripts/host-shell-bound
82
83
  import { codexTrustChanges, CODEX_TRUST_ACTION } from '../scripts/codex-hook-trust.mjs';
83
84
  import { probeFreshCodexDeclarations } from '../scripts/codex-fresh-host-proof.mjs';
84
85
  import { repairSecurityGuidance } from '../scripts/security-guidance-codex-compat.mjs';
85
- import { installNativeLaunchers, installRoutingRuntime } from '../scripts/model-routing-launchers.mjs';
86
+ import { installNativeLaunchers, installRoutingRuntime, runtimeSnapshot } from '../scripts/model-routing-launchers.mjs';
86
87
  import { installTerminalLaunchers } from '../scripts/model-terminal-launchers.mjs';
87
88
  import { readConsoleReceipts, replaceStaleConsoles } from '../scripts/console-instances.mjs';
88
89
  import { moveBrain, MoveRefused } from '../scripts/move-brain.mjs';
@@ -2000,6 +2001,7 @@ export function wireCodexPlugin({
2000
2001
  cwd = process.cwd(),
2001
2002
  announce = true,
2002
2003
  runJson = runCodexJson,
2004
+ aliasHome,
2003
2005
  } = {}) {
2004
2006
  if (!fs.existsSync(codexDir)) return { host: false, action: 'no-host' };
2005
2007
  const options = { codexBin, codexHome, cwd };
@@ -2024,6 +2026,11 @@ export function wireCodexPlugin({
2024
2026
  if (announce) warn(`Codex Brain plugin is installed but disabled by user or policy โ€” left disabled (${CODEX_PLUGIN_ID}).`);
2025
2027
  return { host: true, action: 'disabled', ...before };
2026
2028
  }
2029
+ // Explicit alternate homes support isolated host qualification; metadata-only injected probes
2030
+ // must not write the real user's skill directory.
2031
+ if (aliasHome !== undefined || (runJson === runCodexJson && codexHome === codexHomeDir())) {
2032
+ installCodexConsoleAlias({ home: aliasHome ?? os.homedir(), codexHome });
2033
+ }
2027
2034
  // An existing Codex session can keep the plugin generation it loaded at boot. Compare the
2028
2035
  // installed bytes with the source candidate before mutating the marketplace so body-only updates
2029
2036
  // remain live while a changed/unknown boot surface gets one explicit restart request.
@@ -4931,7 +4938,7 @@ export function syncManagedRouterTools({ routerDir = path.join(os.homedir(), '.c
4931
4938
  const destination = path.join(routerDir, 'bin');
4932
4939
  fs.mkdirSync(destination, { recursive: true });
4933
4940
  let copied = 0;
4934
- for (const name of ['model-router-engine.mjs', 'model-router-setup.mjs', 'model-router-status.mjs', 'model-router-outcome.mjs', 'subscription-hosts.mjs', 'review-model-defaults.mjs', 'dual-host-deliberation.mjs', 'dual-host-suggest.mjs', 'route-cheap.mjs', 'dispatch-receipt.mjs', 'metaharness-receipts.mjs', 'metaharness-router.mjs', 'model-router-dispatch.mjs', 'model-currency.mjs', 'model-currency-evidence.mjs', 'user-model-prompt-hook.mjs', 'model-router-agent-hook.mjs', 'model-weekly-assessment.mjs', 'model-weekly-analyst.mjs', 'model-weekly-cycle.mjs', 'model-weekly-qualification.mjs', 'model-native-qualification.mjs', 'model-routing-launchers.mjs', 'model-terminal-gateway.mjs', 'model-terminal-launchers.mjs', 'claude-terminal-mod.mjs', 'claude-controlled-terminal.mjs', 'model-native-catalog.mjs', 'model-analyst-sandbox.mjs', 'model-routing-policy-promotion.mjs', 'model-routing-gateway.mjs', 'native-subscription-usage.mjs', 'codex-routed.sh', 'goldie-weekly.sh']) {
4941
+ for (const name of ['model-router-engine.mjs', 'model-router-setup.mjs', 'model-router-status.mjs', 'model-router-outcome.mjs', 'subscription-hosts.mjs', 'review-model-defaults.mjs', 'dual-host-deliberation.mjs', 'dual-host-suggest.mjs', 'route-cheap.mjs', 'dispatch-receipt.mjs', 'metaharness-receipts.mjs', 'metaharness-router.mjs', 'model-router-dispatch.mjs', 'model-currency.mjs', 'model-currency-evidence.mjs', 'user-model-prompt-hook.mjs', 'model-router-agent-hook.mjs', 'model-weekly-assessment.mjs', 'model-weekly-analyst.mjs', 'model-weekly-cycle.mjs', 'model-weekly-qualification.mjs', 'model-native-qualification.mjs', 'model-routing-launchers.mjs', 'model-terminal-gateway.mjs', 'model-terminal-launchers.mjs', 'claude-terminal-mod.mjs', 'claude-controlled-terminal.mjs', 'model-managed-prompt.mjs', 'model-managed-workflow-service.mjs', 'model-routing-controller.mjs', 'model-routing-execution-adapters.mjs', 'codex-managed-terminal.mjs', 'grok-subscription-host.mjs', 'model-native-catalog.mjs', 'model-analyst-sandbox.mjs', 'model-routing-policy-promotion.mjs', 'model-routing-gateway.mjs', 'native-subscription-usage.mjs', 'codex-routed.sh', 'goldie-weekly.sh']) {
4935
4942
  const source = path.join(packageRoot, 'scripts', name);
4936
4943
  if (!fs.existsSync(source)) throw new Error(`Packaged router tool missing: ${name}`);
4937
4944
  const target = path.join(destination, name);
@@ -4942,20 +4949,18 @@ export function syncManagedRouterTools({ routerDir = path.join(os.homedir(), '.c
4942
4949
  finally { fs.rmSync(temporary, { force: true }); }
4943
4950
  copied++;
4944
4951
  }
4945
- // Preserve package-relative imports without replacing user policy.mjs overrides.
4946
- for (const runtimeRelative of [path.join('plugin', 'scripts', 'runtime-preferences.mjs'),
4947
- path.join('plugin', 'scripts', 'project-identity.mjs'),
4948
- path.join('config', 'model-router', 'policy.default.mjs')]) {
4949
- const runtimeTarget = path.join(routerDir, runtimeRelative);
4950
- const runtimeBytes = fs.readFileSync(path.join(packageRoot, runtimeRelative));
4951
- if (!fs.existsSync(runtimeTarget) || !fs.readFileSync(runtimeTarget).equals(runtimeBytes)) {
4952
+ // Copy the same transitive dependencies used by immutable terminal runtimes.
4953
+ // Repository scripts live in bin here, preserving their relative sibling imports.
4954
+ for (const [relative, runtimeBytes] of runtimeSnapshot(packageRoot).files) {
4955
+ const mapped = relative.startsWith('scripts/') ? `bin/${relative.slice(8)}` : relative;
4956
+ const runtimeTarget = path.join(routerDir, mapped);
4957
+ if (fs.existsSync(runtimeTarget) && fs.readFileSync(runtimeTarget).equals(runtimeBytes)) continue;
4952
4958
  fs.mkdirSync(path.dirname(runtimeTarget), { recursive: true });
4953
4959
  const temporary = `${runtimeTarget}.${crypto.randomUUID()}.tmp`;
4954
4960
  try { fs.writeFileSync(temporary, runtimeBytes, { mode: 0o600 }); fs.renameSync(temporary, runtimeTarget); }
4955
4961
  finally { fs.rmSync(temporary, { force: true }); }
4956
4962
  copied++;
4957
4963
  }
4958
- }
4959
4964
  const qualificationSource = path.join(packageRoot, 'config', 'model-router', 'qualification-contract.json');
4960
4965
  const qualificationTarget = path.join(routerDir, 'qualification-contract.json');
4961
4966
  const qualificationBytes = fs.readFileSync(qualificationSource);
@@ -39,12 +39,12 @@ function assessmentText(text) {
39
39
  export function validateTaskFacts(facts) {
40
40
  if (facts === undefined) return undefined;
41
41
  if (!facts || typeof facts !== 'object' || Array.isArray(facts)) throw new Error('taskFacts must be an object');
42
- const keys = new Set(['taskType','scope','uncertainty','consequentialPlanning','finalSubstantiveReview','exceptionalReason']);
42
+ const keys = new Set(['taskType','scope','uncertainty','consequentialPlanning','finalSubstantiveReview','exceptionalReason','verifiedTaskQualityFailure']);
43
43
  if (Object.keys(facts).some((key) => !keys.has(key))) throw new Error('Unknown taskFacts field');
44
44
  if (facts.taskType !== undefined && !['mechanical','coding','research','planning','review'].includes(facts.taskType)) throw new Error('Invalid taskFacts taskType');
45
45
  if (facts.scope !== undefined && !['routine','substantial'].includes(facts.scope)) throw new Error('Invalid taskFacts scope');
46
46
  if (facts.uncertainty !== undefined && !['none','architecture','coupled-implementation','missing-information','environment'].includes(facts.uncertainty)) throw new Error('Invalid taskFacts uncertainty');
47
- for (const key of ['consequentialPlanning','finalSubstantiveReview']) {
47
+ for (const key of ['consequentialPlanning','finalSubstantiveReview','verifiedTaskQualityFailure']) {
48
48
  if (facts[key] !== undefined && typeof facts[key] !== 'boolean') throw new Error(`taskFacts ${key} must be boolean`);
49
49
  }
50
50
  if (facts.exceptionalReason !== undefined && !/^[a-z][a-z0-9-]{2,79}$/.test(facts.exceptionalReason)) {
@@ -68,7 +68,7 @@ export function classify(features, harness = features.harness || 'codex') {
68
68
  const facts = validateTaskFacts(features.taskFacts);
69
69
  // Partial caller metadata supplements the assessment; it cannot lower explicit high-consequence text.
70
70
  if (facts?.exceptionalReason) return harness === 'claude-code' ? 'hard' : 'exceptional';
71
- if (hardText) return 'hard';
71
+ if (hardText || facts?.verifiedTaskQualityFailure) return 'hard';
72
72
  if (facts && (['architecture','coupled-implementation'].includes(facts.uncertainty) ||
73
73
  facts.consequentialPlanning || facts.finalSubstantiveReview ||
74
74
  ((facts.scope === 'substantial' || substantialText) && ['planning','review'].includes(facts.taskType)))) return 'hard';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
- "version": "4.5.11",
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.",
3
+ "version": "4.5.13",
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": {
7
7
  "ruvnet-brain": "bin/install.mjs"
@@ -149,8 +149,10 @@
149
149
  "vitest": "^4.1.10"
150
150
  },
151
151
  "dependencies": {
152
+ "@claude-flow/aidefence": "^3.0.3",
152
153
  "@metaharness/flywheel": "^0.1.7",
153
154
  "@metaharness/router": "^0.4.0",
155
+ "@pacphi/agentic-kit": "next",
154
156
  "ws": "^8.22.0"
155
157
  }
156
158
  }
@@ -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.5.11",
4
+ "version": "4.5.13",
5
5
  "author": {
6
6
  "name": "Stuart Kerr"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
- "version": "4.5.11",
3
+ "version": "4.5.13",
4
4
  "description": "Source-grounded RuvNet knowledge, lifecycle enforcement, and learning for Codex.",
5
5
  "author": {
6
6
  "name": "Stuart Kerr"
@@ -0,0 +1,88 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { spawn } from 'node:child_process';
5
+
6
+ export const ALIAS_MARKER = '<!-- managed-by: ruvnet-brain; short-console-alias-v1 -->';
7
+ export const ALIAS_NOTICE = 'RuvNet Brain is loaded. Configure it with $rvbc.';
8
+ const aliasPath = (home) => path.join(home, '.agents', 'skills', 'rvbc', 'SKILL.md');
9
+ const aliasBody = `---\nname: rvbc\ndescription: Open the RuvNet Brain configuration console when the user invokes $rvbc or says Configure RuvNet Brain.\n---\n\n${ALIAS_MARKER}\nOpen the installed RuvNet Brain Console. Resolve \`\${RUVNET_BRAIN_KB:-$HOME/.cache/ruvnet-brain/kb}/.console-runtime/scripts/onboarding-console.mjs\`; do not guess a developer checkout. Run \`node <resolved-script> --serve --open\` in the background. Preserve the user's configuration. Report startup failures honestly. Verify the rendered console and client accessibility before claiming it opened or presenting its URL.\n`;
10
+
11
+ export function ownedCodexConsoleAlias(home = os.homedir()) {
12
+ try {
13
+ const file = aliasPath(home);
14
+ if (fs.lstatSync(path.dirname(file)).isSymbolicLink() || !fs.lstatSync(file).isFile()
15
+ || fs.lstatSync(file).isSymbolicLink()) return false;
16
+ const text = fs.readFileSync(file, 'utf8');
17
+ return text.includes(ALIAS_MARKER) && /^name: rvbc$/m.test(text);
18
+ } catch { return false; }
19
+ }
20
+
21
+ export function installCodexConsoleAlias({ home = os.homedir(), codexHome = path.join(home, '.codex'), enabled = true } = {}) {
22
+ if (!enabled) return { action: 'disabled' };
23
+ const file = aliasPath(home);
24
+ try {
25
+ const config = fs.existsSync(path.join(codexHome, 'config.toml')) ? fs.readFileSync(path.join(codexHome, 'config.toml'), 'utf8') : '';
26
+ for (const section of config.split(/(?=^\[)/m)) {
27
+ if (!/^\[\[skills\.config\]\]/.test(section) || !/^\s*enabled\s*=\s*false\s*(?:#.*)?$/m.test(section)) continue;
28
+ const disabled = section.match(/^\s*path\s*=\s*(['"])(.*?)\1\s*(?:#.*)?$/m)?.[2];
29
+ if (disabled && [file, path.dirname(file)].includes(path.resolve(disabled))) return { action: 'disabled' };
30
+ }
31
+ if (fs.existsSync(path.dirname(file)) && !ownedCodexConsoleAlias(home)) return { action: 'user-owned' };
32
+ fs.mkdirSync(path.dirname(file), { recursive: true });
33
+ const existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null;
34
+ if (existing === aliasBody) return { action: 'unchanged', path: file };
35
+ fs.writeFileSync(file, aliasBody);
36
+ return { action: existing === null ? 'installed' : 'updated', path: file };
37
+ } catch (error) { return { action: 'unavailable', reason: error.message }; }
38
+ }
39
+
40
+ // Fresh native metadata only: no inference or plugin mutation requested.
41
+ export function probeCodexConsoleAlias({ home = os.homedir(), codexHome = path.join(home, '.codex'),
42
+ cwd = process.cwd(), binary = 'codex', timeoutMs = 150, spawnChild = spawn } = {}) {
43
+ if (!ownedCodexConsoleAlias(home)) return Promise.resolve(false);
44
+ return new Promise((resolve) => {
45
+ let child; let timer; let buffer = ''; let settled = false;
46
+ const finish = (ready) => { if (settled) return; settled = true; clearTimeout(timer); try { child?.kill(); } catch {} resolve(ready); };
47
+ try { child = spawnChild(binary, ['app-server'], { cwd, env: { ...process.env, HOME: home, CODEX_HOME: codexHome }, stdio: ['pipe', 'pipe', 'ignore'] }); }
48
+ catch { finish(false); return; }
49
+ timer = setTimeout(() => finish(false), timeoutMs);
50
+ child.on('error', () => finish(false)); child.on('exit', () => finish(false));
51
+ child.stdin.on('error', () => finish(false));
52
+ child.stdout.on('data', (chunk) => {
53
+ buffer += String(chunk);
54
+ if (buffer.length > 8 * 1024 * 1024) { finish(false); return; }
55
+ let newline;
56
+ while ((newline = buffer.indexOf('\n')) >= 0) {
57
+ let message; try { message = JSON.parse(buffer.slice(0, newline)); } catch {}
58
+ buffer = buffer.slice(newline + 1);
59
+ if (message?.id === 1 && !message.error) {
60
+ child.stdin.write(`${JSON.stringify({ method: 'initialized' })}\n`);
61
+ child.stdin.write(`${JSON.stringify({ id: 2, method: 'skills/list', params: { cwds: [cwd], forceReload: true } })}\n`);
62
+ } else if (message?.id === 2) {
63
+ const groups = message.result?.data;
64
+ const ready = !message.error && Array.isArray(groups) && groups.length === 1 && groups[0].cwd === cwd
65
+ && Array.isArray(groups[0].skills) && !(groups[0].errors?.length)
66
+ && groups[0].skills.some((row) => row.name === 'rvbc' && row.path === aliasPath(home) && row.enabled === true);
67
+ finish(Boolean(ready && ownedCodexConsoleAlias(home)));
68
+ }
69
+ }
70
+ });
71
+ child.stdin.write(`${JSON.stringify({ id: 1, method: 'initialize', params: { clientInfo: { name: 'rnb_short_console_alias', version: '1' } } })}\n`);
72
+ });
73
+ }
74
+
75
+ export function readSessionSource(input = process.stdin) {
76
+ if (input.isTTY) return Promise.resolve(null);
77
+ return new Promise((resolve) => {
78
+ let raw = ''; let settled = false;
79
+ const finish = () => {
80
+ if (settled) return; settled = true; clearTimeout(timer); input.pause();
81
+ input.removeListener('data', onData); input.removeListener('end', finish); input.removeListener('error', finish);
82
+ try { resolve(JSON.parse(raw).source ?? null); } catch { resolve(null); }
83
+ };
84
+ const onData = (chunk) => { raw += String(chunk); if (raw.length > 32768) { raw = ''; finish(); } };
85
+ const timer = setTimeout(finish, 100);
86
+ input.on('data', onData); input.once('end', finish); input.once('error', finish); input.resume();
87
+ });
88
+ }
@@ -47,7 +47,7 @@ export const STAGE_BUDGETS_MS = {
47
47
  'stable-spine': 300, // seed-dispatch decision + a single detach launch
48
48
  heartbeat: 300, // update-check dispatch launch
49
49
  'ascii-drift': 300, // optional ascii->svg drift advisory, already spawnSync-timeout bounded
50
- banner: 200, // version/readiness/health banner assembly โ€” pure fs reads, no subprocess
50
+ banner: 200, // banner assembly, including a bounded native skill metadata probe
51
51
  };
52
52
 
53
53
  export function sumBudgetsMs(budgets = STAGE_BUDGETS_MS) {
@@ -39,6 +39,7 @@ import { FOOTPRINT_LINE_PREFIX } from './brain-confirmation.mjs';
39
39
  import { describeLifecycleHooks, readHookContracts } from './session-start-hook-description.mjs';
40
40
  import { createStageTracer } from './session-start-trace.mjs';
41
41
  import { sessionStartProofRecorder } from './session-start-proof.mjs';
42
+ import { ALIAS_NOTICE, probeCodexConsoleAlias, readSessionSource } from './codex-console-alias.mjs';
42
43
 
43
44
  // Re-exported for callers/tests that import the entitlement check directly from this file's own
44
45
  // long-standing public surface (tests/unit/session-start-core-parity.test.mjs).
@@ -90,6 +91,8 @@ export async function runSessionStart({
90
91
  platform = process.platform,
91
92
  restoreContinuity = restoreWithBrief,
92
93
  runHeartbeat = true,
94
+ sessionSource = 'startup',
95
+ probeConsoleAlias = probeCodexConsoleAlias,
93
96
  } = {}) {
94
97
  const lines = [];
95
98
  // SessionStart is context plumbing, not an instruction channel. Keep factual, actionable
@@ -99,6 +102,7 @@ export async function runSessionStart({
99
102
  const isSafeStatus = (line) => {
100
103
  const s = String(line);
101
104
  return s.startsWith('๐Ÿšจ')
105
+ || s === ALIAS_NOTICE
102
106
  || s.startsWith('[RuvNet Brain โ€” HEALTH ALARM')
103
107
  || s.startsWith('[RuvNet Brain โ€” INSTALL ALARM')
104
108
  || s.startsWith('[RuvNet Brain โ€” NIGHTLY FAILED')
@@ -332,7 +336,7 @@ export async function runSessionStart({
332
336
  }
333
337
  });
334
338
 
335
- tracer.stage('banner', () => {
339
+ await tracer.stageAsync('banner', async () => {
336
340
  const bannerVersion = effectiveVersion || 'unknown';
337
341
  if (pluginRoot.startsWith(path.join(home, '.claude', 'plugins') + path.sep)) {
338
342
  if (running) write(path.join(stateDir, '.running-version'), `${running}\n`);
@@ -368,6 +372,9 @@ export async function runSessionStart({
368
372
  // correction it is delivered to everyone, worded for the user, through the same always-shown
369
373
  // alarm mechanism as HEALTH ALARM above (never gated by maintainerIssueEntitlement).
370
374
  emit(`[RuvNet Brain v${bannerVersion} โ€” active this session${updated ? ` ยท updated ${updated}` : ''}]`);
375
+ if (env.RUVNET_HOOK_HOST === 'codex' && sessionSource === 'startup'
376
+ && await probeConsoleAlias({ home, codexHome: env.CODEX_HOME || path.join(home, '.codex'),
377
+ cwd, binary: env.CODEX_BIN || 'codex' })) emit(ALIAS_NOTICE);
371
378
  bannerEmitted = true;
372
379
  // S2 (ONE CURRENCY VERDICT): this no longer compares the KB's own SOURCE.json releaseTag to the
373
380
  // plugin version โ€” that heuristic fires a FALSE POSITIVE for the entire (normal, expected)
@@ -430,7 +437,8 @@ function isDirectInvocation() {
430
437
 
431
438
  const direct = isDirectInvocation();
432
439
  if (direct) {
433
- runSessionStart().catch((error) => {
440
+ const sessionSource = await readSessionSource();
441
+ runSessionStart({ sessionSource }).catch((error) => {
434
442
  if (process.env.RUVNET_SESSION_TRACE === '1') {
435
443
  process.stderr.write(`SESSION_TRACE native-fail-open ${error?.message || error}\n`);
436
444
  }
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ // A release adapter over doc-currency, not a second review engine.
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { REPO_ROOT, evaluateDoc, blockingFindings } from './doc-currency.mjs';
6
+
7
+ export const GOVERNING_ADR = 'docs/adr/0103-routing-architecture-review-lock.md';
8
+
9
+ export function evaluateArchitectureReview(root = REPO_ROOT) {
10
+ try {
11
+ const doc = evaluateDoc(root, GOVERNING_ADR);
12
+ const findings = blockingFindings([doc], { strict: true });
13
+ const block = (code, message) => findings.push({ file: GOVERNING_ADR, level: 'block', code, message });
14
+ if (doc.id !== 'ADR-103' || doc.status !== 'Accepted') {
15
+ block('architecture-decision-not-accepted', 'The finite governing ADR-103 must be Accepted.');
16
+ }
17
+ if (!doc.governsDeclared.length || doc.governsDeclared.some((entry) => /[*?\[\]{}]/.test(entry))
18
+ || !doc.governed?.length || doc.governed.some((entry) => !entry.resolved)) {
19
+ block('architecture-scope-incomplete', 'The governing set must name existing tracked files explicitly, without directories or globs.');
20
+ }
21
+ if (doc.review?.current !== true) {
22
+ block('architecture-review-not-current', 'Update the architecture/test mapping and record a source-bound review of the final governed bytes before qualification.');
23
+ }
24
+ return { schemaVersion: 1, pass: findings.length === 0, file: GOVERNING_ADR,
25
+ review: doc.review ?? null, findings };
26
+ } catch (error) {
27
+ return { schemaVersion: 1, pass: false, file: GOVERNING_ADR, review: null,
28
+ findings: [{ file: GOVERNING_ADR, level: 'block', code: 'architecture-review-unreadable', message: error.message }] };
29
+ }
30
+ }
31
+
32
+ export function main(argv = process.argv.slice(2)) {
33
+ if (argv.length && (argv.length !== 2 || argv[0] !== '--root')) {
34
+ console.error('Usage: node scripts/architecture-review-lock.mjs [--root PATH]');
35
+ return 2;
36
+ }
37
+ const result = evaluateArchitectureReview(argv.length ? path.resolve(argv[1]) : REPO_ROOT);
38
+ console.log(JSON.stringify(result, null, 2));
39
+ return result.pass ? 0 : 1;
40
+ }
41
+
42
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
43
+ process.exitCode = main();
44
+ }
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import crypto from 'node:crypto';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { verifyPayloadMembers } from './release-payload.mjs';
7
+ import { validateQualificationReceipt } from './release-qualification.mjs';
8
+
9
+ export const REQUIRED_CI_JOBS = ['candidate-preflight', 'release-acceptance-linux',
10
+ 'release-acceptance-windows', 'release-acceptance-macos', 'release-qe'];
11
+ const sha256 = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
12
+
13
+ export function createCandidateCiReceipt({ sha, runId, runAttempt, results, ciResult, manifestFile,
14
+ candidateFile, acceptanceDir }) {
15
+ if (!/^[a-f0-9]{40}$/.test(sha || '') || !Number.isSafeInteger(runId) || runId <= 0
16
+ || !Number.isSafeInteger(runAttempt) || runAttempt <= 0) throw new Error('candidate CI run identity is invalid');
17
+ if (ciResult !== 'success') throw new Error('candidate CI reusable workflow did not succeed');
18
+ if (!results || Object.keys(results).sort().join(',') !== [...REQUIRED_CI_JOBS].sort().join(',')
19
+ || REQUIRED_CI_JOBS.some(name => results[name] !== 'success')) {
20
+ throw new Error('candidate CI contains a missing or non-success dependency');
21
+ }
22
+ const manifestBytes = fs.readFileSync(manifestFile);
23
+ const manifest = JSON.parse(manifestBytes);
24
+ const candidate = JSON.parse(fs.readFileSync(candidateFile));
25
+ if (manifest.candidateSha !== sha || candidate.sha !== sha || manifest.version !== candidate.version
26
+ || !/^\d+\.\d+\.\d+$/.test(manifest.version || '') || manifest.tag !== `v${manifest.version}`) {
27
+ throw new Error('candidate CI receipt identity differs from checked-out source');
28
+ }
29
+ const members = manifest.members;
30
+ if (!Array.isArray(members) || !members.length
31
+ || new Set(members.map(row => row.name)).size !== members.length
32
+ || members.filter(row => row.role === 'npm').length !== 1) throw new Error('candidate CI payload members are invalid');
33
+ const payload = verifyPayloadMembers({ manifest, root: path.dirname(manifestFile) });
34
+ const npm = members.find(row => row.role === 'npm');
35
+ if (String(candidate.artifact?.sha256 || '').replace(/^sha256:/, '') !== npm.sha256) {
36
+ throw new Error('candidate package digest differs from payload');
37
+ }
38
+ const acceptanceReceipts = ['linux', 'macos', 'windows'].map(platform => {
39
+ const bytes = fs.readFileSync(path.join(acceptanceDir, `release-acceptance-${platform}.json`));
40
+ const report = JSON.parse(bytes);
41
+ const tests = validateQualificationReceipt(report, { suite: 'source', platform, sourceSha: sha });
42
+ return { platform, sourceSha: report.source.sha, passed: tests.passed, receiptSha256: sha256(bytes) };
43
+ });
44
+ return { acceptanceReceipts, schemaVersion: 1, kind: 'ruvnet-brain-candidate-ci-evidence',
45
+ sourceSha: sha, version: manifest.version, payloadId: payload.payloadId,
46
+ payloadManifestSha256: sha256(manifestBytes), producerWorkflow: 'release-candidate-preflight',
47
+ producerJob: 'aggregate', summarizedWorkflow: 'ci', runId, runAttempt,
48
+ jobs: REQUIRED_CI_JOBS.map(name => ({ name, conclusion: results[name],
49
+ workflow: name === 'candidate-preflight' ? 'release-candidate-preflight' : 'ci' })),
50
+ verdict: 'PASS', skipped: 0, unknown: 0 };
51
+ }
52
+
53
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
54
+ const receipt = createCandidateCiReceipt({ sha: process.env.CANDIDATE_SHA,
55
+ runId: Number(process.env.GITHUB_RUN_ID), runAttempt: Number(process.env.GITHUB_RUN_ATTEMPT),
56
+ ciResult: process.env.CI_RESULT,
57
+ results: { ...JSON.parse(process.env.CI_RESULTS_JSON), 'candidate-preflight': process.env.PREFLIGHT_RESULT }, manifestFile: 'release-evidence/payload-manifest.json',
58
+ candidateFile: 'release-evidence/candidate-receipt.json', acceptanceDir: 'acceptance-reports' });
59
+ fs.writeFileSync('lane-evidence/ci/candidate-ci-evidence.json', JSON.stringify(receipt, null, 2) + '\n', { flag: 'wx' });
60
+ }