fix(mcp): #2086 — auto-init ruvllm WASM in loadRuvllmWasm + CI regression guard

Reported by @seo-yas. The `loadRuvllmWasm()` helper in
`v3/@claude-flow/cli/src/mcp-tools/ruvllm-tools.ts` did a bare
`import('../ruvector/ruvllm-wasm.js')` and never called
`initRuvllmWasm()`. Result: `_wasmReady` stayed false even after
`ruvllm_sona_create` / `ruvllm_microlora_create` / `ruvllm_hnsw_create`,
which left every downstream `sona_*` / `microlora_*` / `hnsw_*` call
either erroring inside the WASM module or returning empty.

`ruvllm_status` reported `wasm.available=true, wasm.initialized=false`
forever because nothing triggered `initSync({ module: wasmBytes })`.

Fix: fold `await mod.initRuvllmWasm()` into `loadRuvllmWasm()`. The
init function is idempotent (early-returns on `_wasmReady`), so the
cost after the first call is one boolean check. `ruvllm_status`
deliberately uses a separate `loadRuvllmWasmModule()` helper so the
diagnostic path stays callable when init has not been attempted (a
diagnostic that triggers the very thing it diagnoses is worse than
useless).

CI guard: `scripts/smoke-ruvllm-wasm-auto-init.mjs` statically asserts
all 12 invariants the fix relies on. Wired into v3-ci.yml as
`ruvllm-wasm-auto-init-smoke` with path filters on the two source files
and the smoke itself, so future refactors that drop the `await
mod.initRuvllmWasm()`, reroute `ruvllm_status` through the auto-init
loader, or add a new `ruvllm_*` tool that bypasses `loadRuvllmWasm()`
without an instance lookup will fail the check before merge.

Versions bumped to 3.7.0-alpha.73 (cli, claude-flow umbrella, ruflo).

Co-Authored-By: RuFlo <ruflo-bot@users.noreply.github.com>
This commit is contained in:
ruv
2026-05-21 12:36:22 -04:00
co-authored by RuFlo
parent a01d75299e
commit bc1f587c0f
6 changed files with 196 additions and 4 deletions
+49
View File
@@ -74,6 +74,12 @@ on:
- 'v3/@claude-flow/cli/src/transfer/ipfs/client.ts'
- 'v3/@claude-flow/cli/scripts/publish-registry.ts'
- 'scripts/smoke-plugin-registry-signature.mjs'
# ruvllm WASM auto-init regression smoke (#2086) — the
# `loadRuvllmWasm()` helper in `ruvllm-tools.ts` and the
# `ruvllm_status` un-init diagnostic path must stay in lockstep.
- 'v3/@claude-flow/cli/src/mcp-tools/ruvllm-tools.ts'
- 'v3/@claude-flow/cli/src/ruvector/ruvllm-wasm.ts'
- 'scripts/smoke-ruvllm-wasm-auto-init.mjs'
pull_request:
branches: [main, develop]
paths:
@@ -127,6 +133,10 @@ on:
- 'v3/@claude-flow/cli/src/transfer/ipfs/client.ts'
- 'v3/@claude-flow/cli/scripts/publish-registry.ts'
- 'scripts/smoke-plugin-registry-signature.mjs'
# ruvllm WASM auto-init regression (#2086)
- 'v3/@claude-flow/cli/src/mcp-tools/ruvllm-tools.ts'
- 'v3/@claude-flow/cli/src/ruvector/ruvllm-wasm.ts'
- 'scripts/smoke-ruvllm-wasm-auto-init.mjs'
# witness manifests / fix list — so witness-verify runs on PRs that
# touch them (otherwise a stale per-OS manifest only fails post-merge).
- 'verification/**'
@@ -924,6 +934,45 @@ jobs:
- name: Run plugin-registry signature smoke
run: node scripts/smoke-plugin-registry-signature.mjs
ruvllm-wasm-auto-init-smoke:
# Regression guard for ruvnet/ruflo#2086 — ruvllm WASM bootstrap not
# exposed via MCP. Reporter: @seo-yas. Every `ruvllm_*` MCP tool that
# touches the WASM runtime calls `loadRuvllmWasm()` in
# `v3/@claude-flow/cli/src/mcp-tools/ruvllm-tools.ts`. That helper used
# to just `import(...)` the module and never call `initRuvllmWasm()`,
# leaving `_wasmReady=false`. Result: `ruvllm_status` reported
# `wasm.initialized=false` even after `ruvllm_sona_create` and
# downstream sona/microlora/hnsw operations silently failed or
# returned empty results.
#
# The fix wires `initRuvllmWasm()` into `loadRuvllmWasm()` (it's
# idempotent — `_wasmReady` short-circuits subsequent calls).
# `ruvllm_status` keeps a separate un-init `loadRuvllmWasmModule()`
# path so diagnostics still report uninitialized state without
# eagerly bootstrapping.
#
# This smoke statically asserts:
# 1. `loadRuvllmWasm()` awaits `mod.initRuvllmWasm()`.
# 2. `loadRuvllmWasmModule()` exists and does NOT init.
# 3. `ruvllm_status` handler uses the un-init loader.
# 4. Every WASM-touching ruvllm_* tool either routes through
# `loadRuvllmWasm()` or looks up a pre-initialized instance.
# 5. No new ruvllm_* tools have been added that bypass the gate.
name: ruvllm WASM auto-init smoke (#2086)
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Run ruvllm WASM auto-init smoke
run: node scripts/smoke-ruvllm-wasm-auto-init.mjs
pre-bash-hook-smoke:
# Regression guard for ruvnet/ruflo#2017 — the `pre-bash` PreToolUse hook
# in `.claude/helpers/hook-handler.cjs` is a security gate that refuses to
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claude-flow",
"version": "3.7.0-alpha.72",
"version": "3.7.0-alpha.73",
"description": "Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
"main": "dist/index.js",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ruflo",
"version": "3.7.0-alpha.72",
"version": "3.7.0-alpha.73",
"description": "Ruflo - Enterprise AI agent orchestration platform. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
"main": "bin/ruflo.js",
"type": "module",
+126
View File
@@ -0,0 +1,126 @@
#!/usr/bin/env node
/**
* Regression guard for #2086 — ruvllm WASM auto-init via MCP tools.
*
* Reported by @seo-yas: every `ruvllm_*` MCP tool that touches the WASM
* runtime requires `initRuvllmWasm()` to have run first, but no MCP tool
* exposed that bootstrap call and `loadRuvllmWasm()` didn't trigger it.
* Result: `ruvllm_status` reported `wasm.initialized=false` even after
* calling `ruvllm_sona_create` / `ruvllm_microlora_create` / `ruvllm_hnsw_create`.
*
* Fix: `loadRuvllmWasm()` now calls `mod.initRuvllmWasm()` after import.
* `ruvllm_status` deliberately keeps using the un-init loader so it can
* report a non-initialized state for diagnostics.
*
* This smoke verifies:
* 1. The `loadRuvllmWasm` helper exists AND calls `initRuvllmWasm`
* (regression catch — easy to delete the await in a refactor).
* 2. The `ruvllm_status` handler does NOT call `initRuvllmWasm`
* (it must remain a pure diagnostic).
* 3. The set of WASM-touching tools is exactly the expected list —
* adding a new ruvllm_* tool that talks to WASM without going
* through `loadRuvllmWasm()` is a regression of #2086.
*
* Run: `node scripts/smoke-ruvllm-wasm-auto-init.mjs`
*/
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));
const SOURCE = resolve(__dirname, '../v3/@claude-flow/cli/src/mcp-tools/ruvllm-tools.ts');
function fail(msg) {
console.error(`✗ ${msg}`);
process.exitCode = 1;
}
function pass(msg) {
console.log(`✓ ${msg}`);
}
const src = readFileSync(SOURCE, 'utf8');
// Check 1: loadRuvllmWasm awaits mod.initRuvllmWasm()
const loaderBlock = src.match(/async function loadRuvllmWasm\(\)[\s\S]*?\n\}/);
if (!loaderBlock) {
fail('loadRuvllmWasm() helper not found in ruvllm-tools.ts');
} else if (!/await\s+mod\.initRuvllmWasm\(\)/.test(loaderBlock[0])) {
fail('loadRuvllmWasm() does NOT call `await mod.initRuvllmWasm()` — #2086 regression');
} else {
pass('loadRuvllmWasm() invokes mod.initRuvllmWasm()');
}
// Check 2: loadRuvllmWasmModule helper exists (the un-init variant for status)
const moduleBlock = src.match(/async function loadRuvllmWasmModule\(\)[\s\S]*?\n\}/);
if (!moduleBlock) {
fail('loadRuvllmWasmModule() helper missing — #2086 fix removed the diagnostic path');
} else if (/initRuvllmWasm/.test(moduleBlock[0])) {
fail('loadRuvllmWasmModule() should NOT init — its purpose is to report uninitialized state');
} else {
pass('loadRuvllmWasmModule() preserves un-initialized diagnostic path');
}
// Check 3: ruvllm_status handler uses the un-init loader
const statusHandler = src.match(/name:\s*'ruvllm_status'[\s\S]*?handler:\s*async[\s\S]*?\n\s{4,6}\},?\n/);
if (!statusHandler) {
fail('Could not locate ruvllm_status handler in ruvllm-tools.ts');
} else if (/await\s+loadRuvllmWasm\(\)/.test(statusHandler[0])) {
fail('ruvllm_status handler uses loadRuvllmWasm() — would auto-init, losing diagnostic value');
} else if (!/await\s+loadRuvllmWasmModule\(\)/.test(statusHandler[0])) {
fail('ruvllm_status handler does not use loadRuvllmWasmModule()');
} else {
pass('ruvllm_status handler uses loadRuvllmWasmModule() (no auto-init)');
}
// Check 4: every other WASM-touching tool routes through loadRuvllmWasm()
const wasmTouchingTools = [
'ruvllm_hnsw_create',
'ruvllm_hnsw_add',
'ruvllm_hnsw_route',
'ruvllm_sona_create',
'ruvllm_sona_adapt',
'ruvllm_microlora_create',
'ruvllm_microlora_adapt',
'ruvllm_chat_format',
];
for (const name of wasmTouchingTools) {
const re = new RegExp(`name:\\s*'${name}'[\\s\\S]*?handler:\\s*async[\\s\\S]*?\\n\\s{4,6}\\},?\\n`);
const block = src.match(re);
if (!block) {
fail(`Could not locate ${name} handler`);
continue;
}
// Either it routes through loadRuvllmWasm (auto-init path) OR it uses
// a previously created instance (sonaInstances / hnswRouters) where the
// create handler already did the init.
const usesAutoInit = /await\s+loadRuvllmWasm\(\)/.test(block[0]);
const usesInstanceLookup = /(?:sonaInstances|hnswRouters|loraInstances)\.get/.test(block[0]);
if (!usesAutoInit && !usesInstanceLookup) {
fail(`${name} bypasses loadRuvllmWasm() AND has no instance lookup — #2086 regression`);
} else {
pass(`${name} ${usesAutoInit ? 'auto-inits via loadRuvllmWasm()' : 'uses prior instance from create handler'}`);
}
}
// Check 5: ruvllm_generate_config is the only tool that legitimately
// doesn't touch the runtime (it just composes a config object). Verify
// we haven't added a new tool that bypasses loadRuvllmWasm by accident.
const allToolNames = [...src.matchAll(/name:\s*'(ruvllm_[a-z_]+)'/g)].map((m) => m[1]);
const expectedUnique = new Set([...wasmTouchingTools, 'ruvllm_status', 'ruvllm_generate_config']);
const unexpected = allToolNames.filter((n) => !expectedUnique.has(n));
if (unexpected.length > 0) {
fail(
`New ruvllm_* tools found that this smoke does not classify: ${unexpected.join(', ')}. ` +
`If they touch WASM, ensure they call loadRuvllmWasm(); then add them to this smoke.`,
);
} else {
pass(`Tool surface = expected ${allToolNames.length} (${[...allToolNames].sort().join(', ')})`);
}
if (process.exitCode) {
console.error('\n#2086 regression smoke FAILED');
} else {
console.log('\n#2086 regression smoke PASS');
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@claude-flow/cli",
"version": "3.7.0-alpha.72",
"version": "3.7.0-alpha.73",
"type": "module",
"description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
"main": "dist/src/index.js",
@@ -9,7 +9,24 @@ import type { MCPTool } from './types.js';
import { validateIdentifier, validateText } from './validate-input.js';
import type { ChatMessage } from '../ruvector/ruvllm-wasm.js';
// #2086 — every ruvllm_* MCP handler that touches the WASM runtime calls
// this. The downstream `createSonaInstant`/`createMicroLora`/`createHnswRouter`
// helpers all need `initSync({ module: wasmBytes })` to have run, otherwise
// the WASM exports throw. Doing it here makes the bootstrap invisible to
// MCP callers — they don't need a separate `ruvllm_init` tool. `_wasmReady`
// inside `initRuvllmWasm` short-circuits on the second+ call, so the cost
// after the first invocation is one boolean check.
//
// `ruvllm_status` deliberately uses `loadRuvllmWasmModule()` (no init) so a
// caller diagnosing why nothing works gets `initialized=false` instead of
// an error from a failed init.
async function loadRuvllmWasm() {
const mod = await loadRuvllmWasmModule();
await mod.initRuvllmWasm();
return mod;
}
async function loadRuvllmWasmModule() {
return import('../ruvector/ruvllm-wasm.js');
}
@@ -20,7 +37,7 @@ export const ruvllmWasmTools: MCPTool[] = [
inputSchema: { type: 'object' as const, properties: {} },
handler: async () => {
try {
const mod = await loadRuvllmWasm();
const mod = await loadRuvllmWasmModule();
const wasmStatus = await mod.getRuvllmStatus();
// Also include native ruvllm CJS backend status (ADR-086)