mirror of
https://github.com/ruvnet/RuView
synced 2026-07-05 14:33:19 +00:00
e6f26e9ac9
* docs(adr): deep review of the RuView npm surface — ADR-263/264/265 optimization strategies
ADR-263 — @ruvnet/ruview@0.1.0 harness review (O1–O9):
- HIGH: claim-check CLI fails open on empty input (no --text/--file -> PASS exit 0)
- HIGH: MCP stdio server head-of-line blocking (spawnSync verify/calibrate up to 600s)
- MEASURED: optionalDependencies triple the cold npx install (4 pkgs/620kB/71 files
vs 1 pkg/172kB/22 files with --omit=optional) for a path that never imports them
- maxBuffer truncation, python -c port interpolation, version drift, duplicate skills,
guardrail METRIC_TERMS substring false positives ('map'/'F1' — found by dogfooding
claim-check on these very ADRs), zero CI
ADR-264 — @ruvnet/rvagent@0.1.0 + @ruv/ruview-cli review (O1–O9), verified against
the published registry tarball:
- HIGH: exports.require -> dist/index.cjs which is never built nor published
- MEASURED: 44 dead source-map files = 62,698B of the 188kB unpacked payload
- stdio-only server described as dual-transport; mixed dot/underscore tool names;
double Zod validation + hand-duplicated advertised schemas; 2-fd leak per training
job; unbounded body in the unwired HTTP scaffold; dead detectCogBinary candidates;
ruview bin-name collision
ADR-265 — cross-cutting npm distribution strategy: npm-packages.yml CI matrix
(test + pack-content/size gate + tarball-install smoke test), publish-from-CI-only
with npm provenance, version single-sourcing from package.json, bin/namespace
ownership (ruview bin belongs to @ruvnet/ruview), claim-check on package READMEs.
Docs only — no runtime code changed. Index/CHANGELOG/CLAUDE.md/README counts updated.
Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01WrGfTGKv1oWZ6iwXZACULz
* fix(npm): implement ADR-263/264/265 — harness fail-closed + async MCP, rvagent packaging/transport/naming, npm CI+provenance gate
ADR-263 (@ruvnet/ruview 0.2.0), O1-O9:
- claim-check fails closed on empty input (CLI exit 2, empty_text tool error)
- MCP stdio server dispatches tools/call asynchronously (promise-based spawn);
ping answers while a 3s fake verify runs — pinned by new e2e test
- optionalDependencies dropped: cold npx installs exactly 1 package
(MEASURED: was 4 pkgs/620kB/71 files via npm i in a clean prefix)
- bounded rolling output tails replace spawnSync 1MiB maxBuffer
- node_monitor port passed via sys.argv, never spliced into python -c source
- serverInfo.version read from package.json; resources/prompts stubs
- skills single-sourced: prepack sync script generates .claude/skills/ copies
- which() = memoized dep-free PATH scan
- tools underscore-canonical (ruview_claim_check, ...) + dotted aliases
- guardrail precision: word-boundary map/f1/auc/iou, code-span + F1/O2 label
scrubbing, quantitative-claims-only; packaging reproducer hints
- 30/30 tests (was 17), incl. concurrency e2e + fail-open regression pins
ADR-264 (@ruvnet/rvagent 0.2.0), O1-O9:
- exports fixed: types-first, phantom dist/index.cjs require target removed
- tarball map-free: 127,704B unpacked / 46 files / 0 maps (MEASURED,
npm pack --dry-run; was 188kB incl. 44 maps referencing unshipped src)
- Streamable HTTP actually wired behind RVAGENT_HTTP_PORT: one transport +
one MCP server per session (mcp-session-id routing), 1MiB body cap (413),
port-aware localhost origin gate; dual-transport description now true
- tools renamed underscore-canonical with dotted router-only aliases
- single Zod validation gate; advertised inputSchema generated from the same
Zod source (zod-to-json-schema)
- train_count: parent log fds closed (was leaking 2/job); job records
persisted to <jobsDir>/<id>.json (job_status survives restarts); bounded
log-tail reads
- detectCogBinary probes its candidates instead of dead-coding them
- version from package.json; @types/express dropped; @types/jest -> 29
- README rewritten to match reality (no phantom subcommands/policy layer)
- 99/99 jest tests (incl. new session/body-cap suite + previously-broken
manifest suite); stdio handshake + HTTP session flow smoke-tested live
ADR-265 D1-D4:
- .github/workflows/npm-packages.yml: 3-package x Node 20/22 gate — tests,
version-literal grep (D3), pack-content/size gate, tarball-install smoke
test (catches the ADR-264 F1 class), README claim-check (D4)
- .github/workflows/ruview-npm-release.yml: publish from CI only with
npm publish --provenance
- @ruv/ruview-cli bin renamed ruview-cli (ruview bin belongs to
@ruvnet/ruview); version single-sourced
- ci.yml NODE_VERSION 18 -> 20
ADR statuses updated to Accepted/implemented; harness manifest re-pinned;
ADR-263/264/265 + both package READMEs pass claim-check.
Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01WrGfTGKv1oWZ6iwXZACULz
* perf(rvagent): lazy-load HTTP transport + memoize generated tool schemas
stdio time-to-first-response ~242ms -> ~189ms (-22%; MEASURED, median of
repeated initialize round-trips against dist/index.js in this container).
- ./http-transport.js now imported lazily inside the RVAGENT_HTTP_PORT
branch: it chain-loads the MCP SDK streamableHttp module (~48ms MEASURED
via per-module import() timing) which the default stdio path never uses
- toolInputJsonSchema memoized per tool: schemas are static for the process
lifetime; under the session-per-server HTTP model every session calls
tools/list, so stop re-walking the Zod tree each time
No behavior change: 99/99 jest tests; HTTP session flow re-smoke-tested
through the lazy import path (initialize -> 200 + mcp-session-id).
Profiled @ruvnet/ruview too and left it alone: 50ms CLI startup vs ~29ms
bare 'node -e ""' floor on the same box (MEASURED) — already near the
interpreter floor with zero dependencies.
Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01WrGfTGKv1oWZ6iwXZACULz
* ci(ruview-cli): pass jest --passWithNoTests so the private no-test package doesn't fail the npm-packages matrix
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(npm): address 10 verified review findings in harness + rvagent before 0.2.0 publish
harness/ruview (@ruvnet/ruview):
- guardrails: digit gate now sees numbers inside code spans; F1-style
metric tokens followed by ':' or a nearby number are no longer scrubbed
(fail-open regressions in the honesty gate)
- mcp-server: tools/call requests serialize through a FIFO promise chain
(hardware/mutating tools never overlap) while ping/tools/list stay
immediate; stdin close drains in-flight responses before exit
- tools: which() no longer memoizes negative lookups
tools/ruview-mcp (@ruvnet/rvagent):
- index: realpath invoked-directly guard — library import no longer
connects a stdio transport to the consumer's process
- http-transport: explicit allowedOrigins is exact-match only (localhost
any-port convenience applies only with no configured allowlist);
session map gains maxSessions=64 + 5min idle TTL sweep
- train-count: job records persist the child pid and reconcile stale
'running' status after a server restart (exit-code marker or dead pid)
- config: cog binary candidates ordered by process.arch
.github/workflows/ruview-npm-release.yml: port the full ADR-265 D1 gate
(version-literal check, unpacked-size budget, tarball-install smoke test)
from npm-packages.yml so the publish path enforces what the header claims.
Tests: harness 30→36, rvagent 99→112, all passing.
Co-Authored-By: claude-flow <ruv@ruv.net>
---------
Co-authored-by: Claude <noreply@anthropic.com>
187 lines
7.7 KiB
JavaScript
187 lines
7.7 KiB
JavaScript
#!/usr/bin/env node
|
|
// SPDX-License-Identifier: MIT
|
|
// `npx ruview` — the RuView WiFi-sensing operator harness (minted via metaharness,
|
|
// hardened per ADR-182). Plain ESM, no build step: ships and runs as-is.
|
|
//
|
|
// The `ruview.*` tools (onboard/verify/claim-check/…) are PURE Node and run with
|
|
// zero deps. The kernel + host adapter are only touched by `doctor`/`install`
|
|
// (the harness-into-a-repo story), so the operator tools never block on a wasm load.
|
|
|
|
import { fileURLToPath } from 'node:url';
|
|
import { realpathSync, existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
import { join, dirname } from 'node:path';
|
|
import { argv } from 'node:process';
|
|
import { TOOLS, runTool, listTools } from '../src/tools.js';
|
|
import { claimCheck, summarize } from '../src/guardrails.js';
|
|
|
|
const NAME = 'ruview';
|
|
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
const SKILLS_DIR = join(ROOT, 'skills');
|
|
|
|
// Map friendly CLI verbs → registry tool names (underscore-canonical, ADR-263).
|
|
const VERB_TO_TOOL = {
|
|
onboard: 'ruview_onboard',
|
|
verify: 'ruview_verify',
|
|
'claim-check': 'ruview_claim_check',
|
|
calibrate: 'ruview_calibrate',
|
|
monitor: 'ruview_node_monitor',
|
|
flash: 'ruview_node_flash',
|
|
};
|
|
|
|
function pjson(o) { console.log(JSON.stringify(o, null, 2)); }
|
|
|
|
function listSkills() {
|
|
if (!existsSync(SKILLS_DIR)) return [];
|
|
return readdirSync(SKILLS_DIR).filter((f) => f.endsWith('.md')).map((f) => f.replace(/\.md$/, ''));
|
|
}
|
|
|
|
async function doctor() {
|
|
const checks = [];
|
|
// Tools layer (always available, no deps).
|
|
checks.push(['tool registry loads', Object.keys(TOOLS).length > 0]);
|
|
checks.push(['claim_check flags a 100% claim',
|
|
!claimCheck('We hit 100% accuracy on poses.').ok]);
|
|
checks.push(['claim_check passes a tagged MEASURED claim',
|
|
claimCheck('Held-out PCK@20 59.5% (MEASURED vs mean-pose baseline, verify.py).').ok]);
|
|
checks.push(['skills present', listSkills().length > 0]);
|
|
// Kernel + host adapter (optional — only needed to install into a repo).
|
|
let kernelLine = 'kernel/host: not installed (ok — operator tools run without them)';
|
|
try {
|
|
const { loadKernel } = await import('@metaharness/kernel');
|
|
const adapter = (await import('@metaharness/host-claude-code')).default;
|
|
const k = await loadKernel();
|
|
const info = k.kernelInfo();
|
|
checks.push(['kernel loads + reports version', typeof info.version === 'string' && info.version.length > 0]);
|
|
checks.push(['kernel backend is native|wasm|js', ['native', 'wasm', 'js'].includes(k.backend)]);
|
|
checks.push(['host adapter resolves', typeof adapter?.name === 'string']);
|
|
kernelLine = `kernel ${info.version} (${k.backend}) · host ${adapter.name}`;
|
|
} catch {
|
|
/* kernel not installed — fine for the tools-only path */
|
|
}
|
|
let ok = true;
|
|
for (const [label, pass] of checks) { console.log(`${pass ? 'PASS' : 'FAIL'} ${label}`); if (!pass) ok = false; }
|
|
console.log(`\n${NAME}: ${ok ? 'all checks passed' : 'doctor found problems'} — ${kernelLine}`);
|
|
return ok ? 0 : 1;
|
|
}
|
|
|
|
function help() {
|
|
console.log(`Usage: ${NAME} <command> [options]
|
|
|
|
Operator tools:
|
|
onboard [--path docker-demo|repo-build|live-esp32] pick a setup path
|
|
verify [--repo <dir>] run the deterministic proof (VERDICT: PASS)
|
|
claim-check --text "..." | --file <path> lint accuracy claims (the honesty guardrail)
|
|
calibrate --step baseline|enroll|train-room|room-watch
|
|
monitor --port COM8 [--seconds 12] assert CSI is flowing on a node
|
|
flash --port COM8 --variant s3-8mb [--confirm] build+flash firmware (Windows/ESP-IDF)
|
|
|
|
Harness:
|
|
doctor verify the install (tools + optional kernel/host)
|
|
skills list bundled skills
|
|
skill <name> print a skill playbook
|
|
mcp start run the ruview.* MCP server (stdio)
|
|
install --host <h> project the harness config into the current repo
|
|
--version | --help
|
|
|
|
Hosts: claude-code, codex, opencode, copilot, pi-dev, hermes, rvm, github-actions`);
|
|
return 0;
|
|
}
|
|
|
|
/** tiny flag parser: --k v / --k=v / --flag (boolean) */
|
|
function parseFlags(rest) {
|
|
const f = {};
|
|
for (let i = 0; i < rest.length; i++) {
|
|
const a = rest[i];
|
|
if (a.startsWith('--')) {
|
|
const eq = a.indexOf('=');
|
|
if (eq !== -1) { f[a.slice(2, eq)] = a.slice(eq + 1); }
|
|
else if (i + 1 < rest.length && !rest[i + 1].startsWith('--')) { f[a.slice(2)] = rest[++i]; }
|
|
else { f[a.slice(2)] = true; }
|
|
}
|
|
}
|
|
return f;
|
|
}
|
|
|
|
export async function run(args) {
|
|
const cmd = args[0] ?? 'onboard';
|
|
const rest = args.slice(1);
|
|
const flags = parseFlags(rest);
|
|
|
|
// Direct tool verbs.
|
|
if (VERB_TO_TOOL[cmd]) {
|
|
const toolArgs = { ...flags };
|
|
if (cmd === 'claim-check') {
|
|
if (flags.file) toolArgs.text = readFileSync(flags.file, 'utf8');
|
|
// Fail closed (ADR-263 O1): an honesty gate must never PASS on no input.
|
|
if (typeof toolArgs.text !== 'string' || toolArgs.text.trim().length === 0) {
|
|
console.error('claim-check: no input — pass --text "..." or --file <path> (empty input is an error, not a PASS).');
|
|
return 2;
|
|
}
|
|
const res = await runTool('ruview_claim_check', toolArgs);
|
|
pjson(res);
|
|
return res.ok ? 0 : 1;
|
|
}
|
|
if (cmd === 'monitor' && flags.seconds) toolArgs.seconds = Number(flags.seconds);
|
|
if (cmd === 'calibrate' && typeof flags.args === 'string') toolArgs.args = flags.args.split(',');
|
|
const res = await runTool(VERB_TO_TOOL[cmd], toolArgs);
|
|
pjson(res);
|
|
return res.ok ? 0 : 1;
|
|
}
|
|
|
|
switch (cmd) {
|
|
case 'doctor': return doctor();
|
|
case 'skills': console.log(listSkills().join('\n') || '(none)'); return 0;
|
|
case 'skill': {
|
|
const n = rest[0];
|
|
const p = n && join(SKILLS_DIR, `${n}.md`);
|
|
if (!p || !existsSync(p)) { console.error(`No skill "${n}". Try: ${listSkills().join(', ')}`); return 2; }
|
|
console.log(readFileSync(p, 'utf8'));
|
|
return 0;
|
|
}
|
|
case 'mcp': {
|
|
if (rest[0] === 'start' || rest[0] === undefined) {
|
|
const { startMcpServer } = await import('../src/mcp-server.js');
|
|
startMcpServer();
|
|
return new Promise(() => {}); // run until stdin closes
|
|
}
|
|
console.error('Usage: ruview mcp start'); return 2;
|
|
}
|
|
case 'install': {
|
|
const host = flags.host || 'claude-code';
|
|
try {
|
|
const adapter = (await import('@metaharness/host-claude-code')).default;
|
|
console.log(`Projecting RuView harness for host "${host}" via ${adapter.name}.`);
|
|
console.log('Add to your host config — MCP server command: npx -y ruview mcp start');
|
|
console.log('Skills:', listSkills().join(', '));
|
|
return 0;
|
|
} catch {
|
|
console.error('Host adapter not installed. `npm i @metaharness/host-claude-code` or use the bundled .claude/ config.');
|
|
return 1;
|
|
}
|
|
}
|
|
case 'tools': pjson(listTools()); return 0;
|
|
case '--version': case '-v': {
|
|
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'));
|
|
console.log(pkg.version); return 0;
|
|
}
|
|
case '--help': case '-h': return help();
|
|
default:
|
|
console.error(`Unknown command: ${cmd}. Try \`${NAME} --help\`.`);
|
|
return 2;
|
|
}
|
|
}
|
|
|
|
// CLI guard: run only when invoked directly (realpath both sides — npm/npx shims
|
|
// pass a non-normalized, possibly case-skewed argv[1] on Windows).
|
|
const invokedDirectly = (() => {
|
|
if (!argv[1]) return false;
|
|
try {
|
|
const a = realpathSync(argv[1]);
|
|
const b = realpathSync(fileURLToPath(import.meta.url));
|
|
return process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
} catch { return false; }
|
|
})();
|
|
if (invokedDirectly) {
|
|
run(argv.slice(2)).then((code) => process.exit(code)).catch((err) => { console.error(err); process.exit(1); });
|
|
}
|