mirror of
https://github.com/ruvnet/ruflo.git
synced 2026-09-28 06:22:58 +08:00
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:
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
Reference in New Issue
Block a user