mirror of
https://github.com/ruvnet/ruflo.git
synced 2026-09-14 14:01:28 +08:00
31f22ff9a9
* fix(#1916): register agent_logs MCP tool + resolve hive-spawned workers `ruflo agent logs <id>` called `callMCPTool('agent_logs', …)` against a tool that was never registered → `MCP tool not found: agent_logs`. Registered it in agent-tools.ts. Today it returns the agent's createdAt + lastResult as synthetic entries (per-agent activity logging lands with hive worker execution wiring — #1916 items 3/4, a tracked follow-up) with the documented shape `{ agentId, entries: [{timestamp,level,message,context?}], total }`. Also: `hive-mind_spawn` writes workers to `.claude-flow/agents.json`, a different file from the canonical `.claude-flow/agents/store.json` that agent_status/agent_list/agent_logs read — so `agent status <hive-worker-id>` returned `not_found`. Added `loadAllAgents()` (canonical store ∪ hive store, canonical wins on collision) and routed those three tools through it. NOT in this PR: the task-execution dispatch (#1916 items 3/4 — `task_assign` transitions state but doesn't run a worker; hive metrics stay 0). That needs a design decision (which executor) and is a separate change. CI guard: scripts/audit-cli-mcp-tools.mjs — scans every `callMCPTool('<name>')` in src/commands/*.ts and fails if `<name>` isn't registered in src/mcp-tools/*.ts (the `MCP tool not found` class). Monotone-decreasing baseline at verification/cli-mcp-tool-baseline.json — surfaces 20 *other* CLI subcommands with the same dangling-tool bug (`memory export`, `task retry`, `workflow validate`, `session export`, …; agent_logs is now fixed and not in the list). Wired into v3-ci.yml `tool-descriptions-audit` job (already in witness-verify needs[]). Behavioral guard: __tests__/agent-logs-hive-resolution.test.ts. Witness markers for #1916 + #1916-guard. Co-Authored-By: RuFlo <ruv@ruv.net> * fix(#1916): register the 16 missing CLI↔MCP tools — cli-mcp-tool-baseline → 0 The #1916-guard (scripts/audit-cli-mcp-tools.mjs) surfaced 20 `ruflo <cmd>` subcommands that callMCPTool() a tool that was never registered (would die with `MCP tool not found`). Cleared all of them: - Renamed the coverage tools `hooks/coverage-{route,suggest,gaps}` → `hooks_coverage-{route,suggest,gaps}` (the handlers already existed; the slash names just didn't match the CLI's underscore calls). - Pointed `ruflo swarm stop` / `ruflo stop` at the existing `swarm_shutdown` tool instead of a non-existent `swarm_stop`. - Registered 16 new MCP tools (each with ADR-112 "use when … is wrong" guidance): task_retry, workflow_stop, workflow_validate, session_current, session_export, session_import, memory_detailed-stats, memory_cleanup, memory_compress, memory_export, memory_import, hive-mind_optimize-memory, hooks_teammate-idle, hooks_task-completed, mcp_start, mcp_stop Real where the operation maps cleanly to an existing store/backend (task_retry clones into a fresh pending task; memory_export/import dump & re-store entries; memory_cleanup prunes expired TTLs dry-run-by-default; workflow_validate structurally checks a JSON workflow; session_export/import round-trip the session record; memory_detailed-stats reshapes memory_stats). Honest no-ops with the correct return shape + a `note` where the underlying capability genuinely isn't wired yet (memory_compress — sql.js has no on-disk compression; hive-mind_optimize-memory — structural compaction only, pattern-quality consolidation is the intelligence pipeline's job; hooks_* team hooks — auto-assignment/pattern-learning delegated; mcp_start/stop — the MCP toolset runs in-process, there's no separate server to start/stop). cli-mcp-tool-baseline.json is now 0. tool-descriptions-audit still passes (all 16 new tools carry guidance, ≥80 chars, unique). Full cli test suite green (48 files / 1977 tests). Witness #1916-cli-tools marker added → ruflo verify 107 verified, 0 regressed. mcp-tools-deep.test.ts exception list updated for mcp_start/mcp_stop. Co-Authored-By: RuFlo <ruv@ruv.net> * fix(#1916): register coverageRouterTools + harden the CLI↔MCP audit; bump 3.7.0-alpha.26 Validating the alpha.25 publish caught a gap: `ruflo hooks coverage-route` (and -suggest / -gaps) still failed with `Tool not found: hooks_coverage-route`. Root cause: the coverage tools were defined in ruvector/coverage-tools.ts but their array was never imported into mcp-client.ts's TOOL_REGISTRY — renaming the slash→underscore name didn't help because the file wasn't registered at all. - mcp-client.ts: import + `...coverageRouterTools` in registerTools([…]). - coverage-tools.ts: category 'coverage' → 'hooks' (matches the hooks_* names). - scripts/audit-cli-mcp-tools.mjs: now derives the "registered" set from what mcp-client.ts ACTUALLY spreads into registerTools([…]) (resolving each `...xTools` to its `import { xTools } from '<path>'` and scanning that file), instead of "any src file with a name: definition". So an unregistered tool-source file now correctly produces a dangling-reference flag — this is exactly the gap that let hooks_coverage-* slip through. Version bumped to 3.7.0-alpha.26 (alpha.25 shipped the still-broken coverage tools). Witness manifest regenerated. cli-mcp-tool-baseline.json still 0; audit + mcp-tools-deep + mcp-client tests green. Co-Authored-By: RuFlo <ruv@ruv.net> --------- Co-authored-by: Reuven <cohen@ruv-mac-mini.local>
157 lines
7.2 KiB
JavaScript
157 lines
7.2 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* CLI ↔ MCP tool coverage audit — regression guard for #1916.
|
|
*
|
|
* The `ruflo agent logs <id>` CLI subcommand referenced an `agent_logs` MCP
|
|
* tool that was never registered, so the command died with
|
|
* `MCP tool not found: agent_logs`. There turned out to be ~20 more CLI
|
|
* subcommands with the same shape (`callMCPTool('<name>', …)` where `<name>`
|
|
* isn't in the registry — `memory export`, `task retry`, `workflow validate`,
|
|
* `session export`, …). Fixing them all is a backlog; this guard prevents
|
|
* the count from *growing* (monotone-decreasing baseline, same pattern as
|
|
* scripts/audit-tool-descriptions.mjs) and fails immediately on any NEW
|
|
* dangling reference.
|
|
*
|
|
* It scans every `callMCPTool('<name>', …)` reference in
|
|
* `v3/@claude-flow/cli/src/commands/*.ts` and checks the name is registered
|
|
* by some MCPTool definition in `v3/@claude-flow/cli/src/mcp-tools/*.ts`
|
|
* (the files `mcp-client.ts` assembles into TOOL_REGISTRY).
|
|
*
|
|
* Usage:
|
|
* node scripts/audit-cli-mcp-tools.mjs # exit 1 if regressed
|
|
* node scripts/audit-cli-mcp-tools.mjs --json # machine-readable report
|
|
* node scripts/audit-cli-mcp-tools.mjs --update-baseline # after fixing some
|
|
*/
|
|
|
|
import { readFileSync, readdirSync, existsSync, writeFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
|
|
const REPO_ROOT = process.cwd();
|
|
const CLI_SRC = join(REPO_ROOT, 'v3', '@claude-flow', 'cli', 'src');
|
|
const COMMANDS_DIR = join(CLI_SRC, 'commands');
|
|
const MCP_CLIENT = join(CLI_SRC, 'mcp-client.ts'); // single source of truth for what is registered
|
|
const BASELINE_FILE = join(REPO_ROOT, 'verification', 'cli-mcp-tool-baseline.json');
|
|
const JSON_OUT = process.argv.includes('--json');
|
|
const UPDATE_BASELINE = process.argv.includes('--update-baseline');
|
|
|
|
const TOOL_DEF_RE = /name:\s*'([^']+)',\s*\n\s*description:\s*'/g;
|
|
const CALL_RE = /callMCPTool\s*(?:<[\s\S]*?>)?\s*\(\s*['"]([^'"]+)['"]/g;
|
|
|
|
function listTs(dir) {
|
|
if (!existsSync(dir)) return [];
|
|
return readdirSync(dir).filter(f => f.endsWith('.ts') && !f.endsWith('.test.ts')).map(f => join(dir, f));
|
|
}
|
|
|
|
// --- which tool-source files are ACTUALLY registered ---
|
|
// Mirror mcp-client.ts: find every `...<ident>(?())` spread inside its
|
|
// `registerTools([ … ])` call, then resolve each <ident> to its
|
|
// `import { … } from '<path>'` and scan that file. A file that defines tools
|
|
// but is never imported into registerTools is NOT registered — so a CLI
|
|
// callMCPTool() against it correctly counts as dangling (this caught the
|
|
// `hooks_coverage-*` tools, which were defined in ruvector/coverage-tools.ts
|
|
// but never wired in — #1916).
|
|
function resolveImportPath(spec) {
|
|
// spec like './mcp-tools/agent-tools.js' or '../ruvector/coverage-tools.js'
|
|
let p = spec.replace(/^\.\//, '').replace(/^\.\.\//, '../');
|
|
// .js → .ts (source files)
|
|
p = p.replace(/\.js$/, '.ts');
|
|
return p.startsWith('../') ? join(CLI_SRC, p) : join(CLI_SRC, p);
|
|
}
|
|
function registeredToolSourceFiles() {
|
|
const src = readFileSync(MCP_CLIENT, 'utf-8');
|
|
const regBlock = (src.match(/registerTools\(\[([\s\S]*?)\]\);/) || [, ''])[1];
|
|
// collect spread identifiers (strip trailing () if it's `...getX()`)
|
|
const idents = new Set();
|
|
for (const m of regBlock.matchAll(/\.\.\.\s*([A-Za-z_$][\w$]*)\s*\(?\s*\)?/g)) idents.add(m[1]);
|
|
// map each ident to its import path
|
|
const files = new Set();
|
|
for (const id of idents) {
|
|
// `import { id } from '<path>'` OR `import { x, id, y } from '<path>'`
|
|
const re = new RegExp(`import\\s*\\{[^}]*\\b${id.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}\\b[^}]*\\}\\s*from\\s*['"]([^'"]+)['"]`);
|
|
const im = src.match(re);
|
|
if (im) {
|
|
const f = resolveImportPath(im[1]);
|
|
if (existsSync(f)) files.add(f);
|
|
} else {
|
|
// `getX()` factory — the function may live in <x>-tools.ts; try a name guess
|
|
const guess = id.replace(/^get/, '').replace(/^([A-Z])/, c => c.toLowerCase()).replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
const cand = join(CLI_SRC, 'mcp-tools', `${guess}.ts`);
|
|
if (existsSync(cand)) files.add(cand);
|
|
}
|
|
}
|
|
return [...files];
|
|
}
|
|
|
|
// --- registered tool names ---
|
|
const registered = new Set();
|
|
for (const file of registeredToolSourceFiles()) {
|
|
const fsrc = readFileSync(file, 'utf-8');
|
|
let m; TOOL_DEF_RE.lastIndex = 0;
|
|
while ((m = TOOL_DEF_RE.exec(fsrc))) registered.add(m[1]);
|
|
}
|
|
|
|
// --- callMCPTool references in command files ---
|
|
const references = []; // { name, file, line }
|
|
for (const file of listTs(COMMANDS_DIR)) {
|
|
const src = readFileSync(file, 'utf-8');
|
|
let m; CALL_RE.lastIndex = 0;
|
|
while ((m = CALL_RE.exec(src))) {
|
|
const line = src.slice(0, m.index).split('\n').length;
|
|
references.push({ name: m[1], file: file.replace(REPO_ROOT + '/', ''), line });
|
|
}
|
|
}
|
|
|
|
const danglingRefs = references.filter(r => !registered.has(r.name));
|
|
const danglingNames = [...new Set(danglingRefs.map(r => r.name))].sort();
|
|
|
|
const baseline = existsSync(BASELINE_FILE)
|
|
? JSON.parse(readFileSync(BASELINE_FILE, 'utf-8'))
|
|
: { danglingCount: Number.POSITIVE_INFINITY, knownDangling: [] };
|
|
const baselineKnown = new Set(baseline.knownDangling || []);
|
|
const newOffenders = danglingNames.filter(n => !baselineKnown.has(n));
|
|
|
|
if (UPDATE_BASELINE) {
|
|
const next = {
|
|
_comment: 'Monotone-decreasing baseline of CLI subcommands that callMCPTool() a tool not registered in src/mcp-tools/*.ts (would fail with `MCP tool not found`). Fix one → re-run with --update-baseline to lock the new floor. NEVER increase.',
|
|
danglingCount: danglingNames.length,
|
|
knownDangling: danglingNames,
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
writeFileSync(BASELINE_FILE, JSON.stringify(next, null, 2) + '\n', 'utf-8');
|
|
console.log(`baseline updated → ${danglingNames.length} dangling tool name(s)`);
|
|
process.exit(0);
|
|
}
|
|
|
|
const regressed = newOffenders.length > 0 || danglingNames.length > (baseline.danglingCount ?? Infinity);
|
|
|
|
const report = {
|
|
registeredToolCount: registered.size,
|
|
commandFilesScanned: listTs(COMMANDS_DIR).length,
|
|
danglingCount: danglingNames.length,
|
|
baselineCount: baseline.danglingCount,
|
|
newOffenders,
|
|
dangling: danglingRefs.map(r => ({ name: r.name, where: `${r.file}:${r.line}` })),
|
|
};
|
|
|
|
if (JSON_OUT) {
|
|
console.log(JSON.stringify(report, null, 2));
|
|
} else {
|
|
console.log(`CLI ↔ MCP tool coverage — ${registered.size} registered tools; ${danglingNames.length} dangling callMCPTool() name(s) (baseline ${baseline.danglingCount})`);
|
|
if (!regressed) {
|
|
console.log(` ✓ no NEW dangling references (${danglingNames.length} known offenders carried in verification/cli-mcp-tool-baseline.json)`);
|
|
} else {
|
|
if (newOffenders.length) {
|
|
console.log(` ✗ NEW dangling callMCPTool() reference(s) — register the tool in src/mcp-tools/*.ts:`);
|
|
for (const name of newOffenders) {
|
|
for (const r of danglingRefs.filter(x => x.name === name)) console.log(` '${name}' ${r.file}:${r.line}`);
|
|
}
|
|
}
|
|
if (danglingNames.length > (baseline.danglingCount ?? Infinity)) {
|
|
console.log(` ✗ dangling count ${danglingNames.length} exceeds baseline ${baseline.danglingCount}`);
|
|
}
|
|
console.log(`\nFix the new reference(s), or (only after fixing some existing ones) run: node scripts/audit-cli-mcp-tools.mjs --update-baseline`);
|
|
}
|
|
}
|
|
|
|
process.exit(regressed ? 1 : 0);
|