diff --git a/.github/workflows/v3-ci.yml b/.github/workflows/v3-ci.yml index a8f4a9871..d3b9b4ea6 100644 --- a/.github/workflows/v3-ci.yml +++ b/.github/workflows/v3-ci.yml @@ -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 diff --git a/package.json b/package.json index 40d25b39b..70f9aefdc 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/ruflo/package.json b/ruflo/package.json index 2a8bde2aa..cc0eb3550 100644 --- a/ruflo/package.json +++ b/ruflo/package.json @@ -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", diff --git a/scripts/smoke-ruvllm-wasm-auto-init.mjs b/scripts/smoke-ruvllm-wasm-auto-init.mjs new file mode 100644 index 000000000..ec282b3a2 --- /dev/null +++ b/scripts/smoke-ruvllm-wasm-auto-init.mjs @@ -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'); +} diff --git a/v3/@claude-flow/cli/package.json b/v3/@claude-flow/cli/package.json index 14e36ba39..aa03e7d6f 100644 --- a/v3/@claude-flow/cli/package.json +++ b/v3/@claude-flow/cli/package.json @@ -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", diff --git a/v3/@claude-flow/cli/src/mcp-tools/ruvllm-tools.ts b/v3/@claude-flow/cli/src/mcp-tools/ruvllm-tools.ts index 3dda19e16..899fa49cc 100644 --- a/v3/@claude-flow/cli/src/mcp-tools/ruvllm-tools.ts +++ b/v3/@claude-flow/cli/src/mcp-tools/ruvllm-tools.ts @@ -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)