Bump the harness and the umbrella (which pins it), point README and the
user guide at 0.9.1, sync the stale lockfile header (0.5.1, old bin name)
with package.json, and add a plain-language changelog entry for the
live sensing views in Claude Code (ADR-377, ADR-378).
Co-Authored-By: claude-flow <ruv@ruv.net>
- Stale frames shown as live: spectrumWith now keeps only nodes in the
latest capture, so a node that stops streaming (or a failed capture)
leaves the waterfall and the "no CSI frames" card returns.
- Multi-antenna ESP32 nodes never reached the waterfall: with --spectrum
the parser adds chain-0 amplitudes (ADR-018 antenna-major) in a separate
field the SpectrumCollector reads; the kernel path is unchanged.
- Frames from different CSI layouts could be joined: spectrum results
carry their shape, and frames merge only when shape and bins match.
- Animation ran on assumed time: frames read the real clock (the one the
full render uses), so pulses keep the reported rates and the replay
keeps up when a frame fires late; replay lag is pruned with the nodes.
Tests: regression tests for each; npm test 193 pass, test:security 91
pass, engine tests 4 pass, validate passes.
Co-Authored-By: claude-flow <ruv@ruv.net>
Hotkeys reach a pane only while it holds the keyboard, and `focus` alone
was not taking it. The engine's documented dialog form does: open with
focus + closeOnEscape + holdToasts (Escape closes, as originally). Each
open also requests `rows` for its view (overview 16, waterfall 30,
radar 26) so the inline pane opens tall enough for the bordered design
instead of a third of the screen; switching views re-requests the size,
and picture heights leave room for the chrome (rows - 15).
Co-Authored-By: claude-flow <ruv@ruv.net>
The compact inline layout read as broken next to the bordered cards the
user prefers; restore the previous design (78a147405). This reverts
commit 629e1b9c6.
Co-Authored-By: claude-flow <ruv@ruv.net>
Outside fullscreen the pane sits inline above the prompt with a few rows,
so the bordered cards, rule and footer were cut off (Refresh/Close never
visible). Inline (or under 18 rows) the pane is now compact: views and
actions share the top row, no borders/rule/gaps, pictures sized to the
rows left, radar readings beside a small fan. The top row always says
whether the pane holds the keyboard ("keys active · Esc returns them" or
"click the pane or press ctrl+x tab to use the keys").
Co-Authored-By: claude-flow <ruv@ruv.net>
Button hotkeys reach a Pane only while it holds the keyboard; the pane
opened without it, so 1/2/3/r/c went to the prompt. Open (and
/ruview waterfall|radar) now request `focus: true`; closeOnEscape is
dropped so Escape hands the keys back without closing; an unfocused pane
shows "click the pane or press ctrl+x tab to use the keys".
Co-Authored-By: claude-flow <ruv@ruv.net>
The /ruview pane gains three views (keys 1/2/3) drawn in terminal Rasters
and animated in place with $.ui.blit (~12 fps, no redraws):
- CSI waterfall: per-subcarrier amplitude over time from the new
`esp32 --spectrum` capture, two frames per row via the half block,
5-95th percentile colour limits, replayed at the frames' arrival rate,
labelled MEASURED or SYNTHETIC from the packets' own flag; n cycles nodes.
- Radar: a 120-degree range fan with rings, a fading distance trail and a
sonar ping to the measured range; braille heart/breathing/distance charts
scaled to the data; a heart and breathing gauge that pulse at the
device-reported rates ("a metronome, not a waveform").
- A shimmer rule under the title; surfaces without Raster get a note.
Harness: captureEsp32 / ruview_esp32_capture take spectrum,
spectrum_bins (8-128) and spectrum_frames (8-256): a bounded ring of
binned amplitude frames per node, dominant shape, rate and synthetic flag.
The mod is split into model/raster/anim/views/register (own-file imports
only). Tests: 11 showcase + 11 mod Node tests, a --spectrum loopback test,
4 engine tests (waterfall tab sends --spectrum, draws the Raster, blits);
npm test 189 pass, test:security 90 pass, manifest verified, validate passes.
Evidence: MEASURED on an ESP32-C6 (COM11, release c6-adr110 v0.7.0, node 6)
streaming 7-18 Hz CSI to this host; a 1x256 capture renders the occupied
band against guard/null tones. Unpacked budget 384 -> 448 KiB (+~37 KB).
Co-Authored-By: claude-flow <ruv@ruv.net>
- Header: RuView + LIVE/STALE badge (fresh within 2 refresh periods),
"updated <time> (Ns ago)", a 5 s redraw tick keeps the age current.
- Bordered cards for the radar and the CSI nodes, side by side from 100
columns, stacked below.
- Radar: PRESENCE DETECTED (+ target count) in green; distance, heart and
breathing with per-refresh sparklines; device-reported vitals outside
resting ranges (heart 40-180, breathing 4-40 bpm) are flagged in yellow
instead of shown as fact (the live kit reported breathing 1.0 bpm).
- Nodes: per-node rate sparkline; "no packets" is a short hint in the card
rather than a long top alert (status no longer counts it as an alert);
an unconfigured radar says how to configure it.
- Buttons show their hotkeys.
Tests: 11 Node unit tests, 3 engine tests (mounted pane validates on the
terminal surface), validate passes.
Co-Authored-By: claude-flow <ruv@ruv.net>
A hot reload (or a resumed session) re-runs register with fresh variables
while the engine keeps the pane open, so no timer ran and the pane stayed
on "waiting for the first capture". Drawing the pane now resumes polling
when no timer is running. Engine test mounts the pane with no /ruview in
the module instance: fails without the fix, passes with it.
Co-Authored-By: claude-flow <ruv@ruv.net>
$.clock.now() resolves a promise of epoch milliseconds; refresh passed the
promise into the model. Await it, and have the pane print "—" for any
non-finite timestamp. Seen live in Claude Code 2.1.287 (/ruview pane).
Regression tests fail without the fix (2/2) and pass with it.
Co-Authored-By: claude-flow <ruv@ruv.net>
- mod/: a Claude Code mod (function hooks). /ruview opens a pane beside
the transcript with CSI nodes (rate, loss, RSSI, shape, SYNTHETIC) and an
optional ESPHome radar (presence, distance, device-reported vitals),
alerts (heartbeat-only boards + buffer-starvation hint), Refresh/Close
buttons and a one-line status; refreshed on $.clock.every.
- Reads only through the harness CLI beside it (fixed read-only argv,
--json) located via $.plugin.root (mods may import nothing else).
- Packaged in @ruvnet/ruview (mod/.claude-plugin, mod/hooks), listed in the
repo marketplace as ruview-live@ruview; `ruview mod` prints how to load it.
- @ruvnet/ruview 0.9.0; umbrella ruview 0.9.0 (still unpublished).
Verified: claude plugin validate (mod + marketplace); claude plugin test
2/2 x3 runs; harness 172 tests + 89 security; real Claude Code headless
`/ruview refresh` from an installed tarball and with the live radar kit.
Co-Authored-By: claude-flow <ruv@ruv.net>
- harness/ruview-umbrella -> npm `ruview` 0.8.0: one CLI and one MCP server
for the operator harness, Homecore and the vitals kernel. `ruview homecore
...`, `ruview kernel <doctor|info|selftest|parity|synth|analyze|bench>`,
`ruview capabilities`; everything else delegates to @ruvnet/ruview. MCP
serves ruview_* + homecore_* tools and the ui:// console (stdio + HTTP).
- @ruvnet/ruview 0.8.0: bin renamed `ruview-harness` (the umbrella owns
`ruview`; a clean tarball install showed the bin collision). New exports
(cli, cli-ui, mcp, mcp-http, ui, esphome, kernel); injectable MCP handler;
kernel as optional peer + setKernelImporter so the kernel resolves from
the umbrella's tree under linked/pnpm installs.
- mmwave --source esphome: dependency-free read-only ESPHome native API
client (private hosts only; encrypted/password APIs reported). Live Seeed
MR60BHA2 kit: presence, distance, device-reported HR/BR (MEASURED).
- monitor detects Ameba CSI buffer starvation; radar views in terminal UI
and console widget.
- homecore 0.1.0 and @ruvnet/ruview-kernel 0.1.0 gain ./cli exports.
Gates: harness 166 + 89 security, homecore 27 + 23, kernel 21 + 12,
umbrella 4; clean-dir tarball install smoke incl. live radar over HTTP MCP.
Local publish is a one-off exception to ADR-265 D2, recorded in ADR-376.
Co-Authored-By: claude-flow <ruv@ruv.net>
Live RTL8721Dx capture (node 3, COM10) reported 97% loss: one stray
sequence value (+52,835, then straight back) was counted as 52,834 lost
frames, and every late UDP packet re-counted its gap. The tracker now
treats a single out-of-window value as a stray, needs two agreeing frames
to accept a counter reset, and lets a late packet fill its own gap
(seqReordered / seqStrays / seqResyncs in the summary). Against ground
truth on the live board (unique sequence numbers over the span): 4,783 of
4,783 present, loss 0, 9 strays; the tracker matched both.
ADR-373 Amendment 2 records the live Realtek run: devices -> realtek,
7,404/7,404 packets decoded, monitor at 1500000 baud without a reset, and
12,549 live frames analyzed by the WASM kernel.
Co-Authored-By: claude-flow <ruv@ruv.net>
runProcess() ends the child's stdin with no error handler. A child that
never reads stdin (git check-ignore) can exit and close the pipe before the
write flushes; the unhandled EPIPE then crashes `agent.mjs validate`. This
failed harness/ruview (node 22) on PR #2062 and reproduces 12/12 locally
under CPU load (0/12 after this change). The command's real outcome is
still reported by the 'close' handler; this mirrors the existing handler in
harness/ruview/src/process-runner.js.
Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_015H55jmprfz4oHWXSFbX5rf
The flash flow's boot-log capture decoded serial lines and wrote text to a
stdout that the scrubbed child env leaves on cp1252. The ESP32-C6 release
firmware logs U+2192, so the capture died mid-boot (bootLog.captured=false);
an earlier abort would have hidden the CSI evidence and reported a good
flash as unvalidated. Write raw bytes instead. Regression test forces
PYTHONIOENCODING=cp1252 through a stand-in serial module (fails without
the fix). ADR-373 Amendment 2 records the real C6 flash evidence.
Co-Authored-By: claude-flow <ruv@ruv.net>
Findings from a hardware run of this branch against an RTL8721Dx board
(PL2303GC) and an ESP32-C6 (CP210x) on Windows 11:
- esp32 capture decodes Realtek RAC1 (ADR-323, CRC-verified, 8/16-bit
tones) and RHB1 heartbeats, ADR-110 sync and ADR-081 mesh packets, and
reports every CSI shape a node sends.
- Capture fails (no_decodable_packets / heartbeat_only) instead of
returning ok:true + MEASURED when nothing decodes; names
heartbeat-only senders.
- --analyze runs live single-antenna CSI through @ruvnet/ruview-kernel at
the measured frame rate with a bounded buffer.
- monitor opens the port with DTR/RTS deasserted (no node reboot), takes
--baud, and explains the C6/S3 USB-Serial/JTAG console.
- devices classifies Prolific 067B:23A3 as a Realtek board.
- Windows: owner-only ACL on the hosts file (fails closed); symlink test
skips only where Windows refuses symlinks.
- kernel build names uninitialized submodules when cargo fails.
- Parser ~3.2x faster (MEASURED, 0.42-0.52M -> 1.33-1.67M packets/s).
Co-Authored-By: claude-flow <ruv@ruv.net>
CI fixes (both failures on PR #2062 were in this branch):
- harness/ruview: remove `2.11.0` literals from training remedies; the
ADR-265 D3 version-literal gate rejects them.
- ruview-kernel workflow: install wasm32 for the toolchain pinned by
v2/rust-toolchain (the action only added it to `stable`); build.mjs now
fails fast with the exact `rustup target add` remedy.
Kernel optimization (ADR-368 amendment):
- New `analyze_flat` / `session_push_flat` ops via `call_f64`: WASM
`rvk_call_f64` (alignment-agnostic bytes) and napi `callF64(Float64Array)`.
`analyze()`/`session.push()` pack uniform frames automatically; irregular
input keeps the JSON path and its per-frame errors. `analyzeFlat()` is the
zero-copy entry. Shared `Session::step` makes both paths byte-identical on
identical f64 inputs (Rust test), and the binary path avoids serde_json's
non-correctly-rounded float parsing.
- MEASURED (`ruview-kernel bench --seconds 120`, one Linux x64 host):
2.82x (wasm) and 2.45x (napi) faster than the JSON transport.
- Profiled remaining cost (examples/profile_stages.rs): heart-rate ACF in
wifi-densepose-vitals dominates; left for a separate, output-reviewed
change. Rejected WASM +simd128 and opt-level=s after three measured runs
(bench/wasm-variants.mjs).
Device e2e (ADR-373 amendment):
- test/e2e/devices.e2e.mjs + `npm run test:e2e:devices` + new workflow
ruview-device-e2e.yml: pty-emulated MR60BHA2 and RPLIDAR through real
pyserial, a UDP ESP32 node, and the real iPhone relay, via the CLI.
- mmWave auto-detect keeps probe frames and reads only the remainder;
iPhone LiDAR gains max_frames and returns immediately when refused.
Suite wall time 19.6 s -> 5.6 s (MEASURED, dev container).
Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_015H55jmprfz4oHWXSFbX5rf
@ruvnet/ruview 0.7.0 gives CLI, MCP, and SDK access to RuView sensing
hardware on the machine it is attached to, and to other machines over SSH.
Devices (ADR-373, src/devices/):
- ruview_devices_scan: pyserial port list classified by USB VID:PID
(Espressif native USB, CP210x, CH34x, FTDI) into ESP32 / mmWave / RPLIDAR
roles with the command that confirms each.
- ruview_esp32_capture: receive-only UDP listener parsing ADR-018 CSI
frames, vitals, and the other 0xC511000x packets per node: rate,
sequence loss, RSSI, subcarriers, frequency, device vitals.
- ruview_mmwave_read: MR60BHA2 (60 GHz) and LD2410 (24 GHz) parsers ported
byte-for-byte from firmware mmwave_sensor.c; baud/frame auto-detect.
- ruview_lidar_read: RPLIDAR SCAN over serial (start/stop, DTR best-effort)
and the iPhone LiDAR relay as a WebSocket client (token only via
RUVIEW_LIDAR_TOKEN; statistics only, never raw depth).
- Dependency-free serial via a fixed pyserial pump (argv-only, bounded,
hex output immune to secret redaction). Schema-level port pattern
validation added to the policy validator.
- All device tools require the new `device-access` MCP grant.
Remote hosts (ADR-374, src/remote.js):
- ~/.config/ruview/hosts.json (0600), CLI-only `hosts add`; strict
name/target validation against ssh option and shell injection.
- `ssh -o BatchMode=yes -o StrictHostKeyChecking=yes -- <target>` running a
version-pinned `ruview call <tool> --read-only`, every token single-quoted;
read-only enforced locally and remotely; no mutations, credentialed reads,
or host hopping. MCP needs `remote-host` plus the remote tool's grant.
Also: device-access skill, doctor `devices`/`remote` groups, SDK
`devices.*`/`hosts.*` with types, generic `ruview call` verb, guidance
capability, 320 KiB package budget, CI smoke expects 22 tools.
Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_015H55jmprfz4oHWXSFbX5rf
@ruvnet/ruview 0.6.0 turns the harness into a complete npm operator
surface: one policy-checked tool registry behind the CLI, the MCP server,
and a new typed SDK.
- SDK (ADR-369): `@ruvnet/ruview/sdk` createRuView() with doctor,
firmware.{ports,plan,flash,monitor}, training.{plan,run,gate,calibrate},
kernel.{selfTest,load}, guidance/claimCheck/verify/spaces; strict mode
throws RuViewError; TypeScript declarations in src/sdk.d.ts.
- Firmware (ADR-370): cross-platform `python -m esptool` flashing replaces
the Windows-only stub. SHA256SUMS verification (mismatch always fatal),
bundle containment, variant/partition checks, argv-only command, chip_id
preflight that refuses a mismatched chip, boot-log capture;
hardwareValidated only when CSI callbacks appear without a panic.
Read-only ruview_firmware_plan / ruview_firmware_ports.
- Training (ADR-371): ruview_train / ruview_train_plan run pose-smoke,
pose (MM-Fi), or room training from the trusted checkout with in-repo
paths only, logs/checkpoints under v2/target/ruview-train, and libtorch
remedies. ruview_train_gate enforces mean-pose baseline, leakage-free
splits, subject/time disjointness, and evidence tagging.
- Doctor (ADR-372): structured pass/warn/fail/skip checks with remedies
across runtime, integrity, hosts, repo, Rust, Python/esptool, firmware
bundles, serial, sensing server, and the compute kernel; --json/--group;
hardware probe and network checks are CLI/SDK-only.
- Plan tools cannot take `confirm`; acting tools keep grant+confirm over
MCP. Policy files and TOOL_POLICY are drift-tested.
- Unpacked budget 160 KiB → 224 KiB in both npm workflows; CI tarball smoke
now checks the SDK and the training gate.
Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_015H55jmprfz4oHWXSFbX5rf
Adds a portable RuView compute kernel for JavaScript, built from the Rust
source of truth and wired into the @ruvnet/ruview metaharness.
Rust
- v2/crates/ruview-kernel: bounded single-function JSON ABI
(call(op, json) -> envelope) over the ADR-021 wifi-densepose-vitals
pipeline: streaming sessions, batch analyze, deterministic SplitMix64
synthetic CSI (SYNTHETIC), bounded session registry, strict input
validation (deny_unknown_fields, width/finite/magnitude checks, size and
count limits). wasm32-only zero-import C ABI (rvk_alloc/free/call).
- v2/crates/ruview-kernel-napi: napi-rs transport exposing only
abiVersion()/call(); standalone workspace (excluded from v2) so napi never
enters `cargo test --workspace`; panic=unwind + catch_unwind.
npm (harness/ruview-kernel, zero runtime deps)
- loadKernel({backend: wasm|napi|auto}); WASM default (no host authority),
napi never silently falls back, auto reports fallbackReason.
- SHA256SUMS integrity verification; mismatch is always fatal; loader
rejects any WASM module that imports host functions.
- Browser-safe ./wasm entry; CLI doctor/info/selftest/parity/synth/
analyze/bench; build.mjs builds artifacts from source (no shell).
- Unit, security, and e2e tests (CLI round trip, cross-backend parity).
Metaharness
- New read-only tool ruview_kernel_selftest / `ruview kernel`: imports the
fixed optional package, fails closed with kernel_not_installed; policy,
claims, MCP policy, guidance capability, and manifest updated.
CI: .github/workflows/ruview-kernel-npm.yml builds both transports on
Linux/macOS/Windows, requires napi in tests, runs parity, verifies
artifacts, packs, and runs the cross-package e2e. No publish step.
Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_015H55jmprfz4oHWXSFbX5rf
Verified on a physical ESP32-C6 (4 MB, esp32c6 defaults, ESP-IDF v5.4):
- App image: 1,085,424 B on main, 1,042,368 B with this change (43,056 B smaller).
- The 'Failed to find FATFS partition storage' error seen on every 4 MB C6 boot is gone.
- Wi-Fi join, CSI init and CSI UDP delivery to the host were confirmed.
CI: 35/35 green.
The new schema constant was defined and asserted as a raw string literal
directly in main.rs, which the semconv registry test scans and requires
every "ruview.*" literal there to be a registered attribute/event. This
isn't a tracing event, though -- it's a wire-schema tag structurally
identical to CALIBRATION_MODEL_RECEIPT_SCHEMA, which already lives in
field_bridge.rs specifically to stay out of that scan. Move the new
constant there and reference it by identifier from main.rs, matching the
existing precedent instead of hand-editing the generated semconv registry.
Co-Authored-By: claude-flow <ruv@ruv.net>
calibration_status unconditionally overwrote status with "none" whenever
active was false, which also erased the legitimate "expired" status for
a calibration that ran long ago and has since expired -- losing the
distinction between "never calibrated" and "was calibrated, now expired".
Fixes the pre-existing failure in
calibration_expiry_tests::long_running_expiry_is_not_reported_active_for_runtime_or_bootstrap,
called out as a known, unrelated gap in PR #1936.
Co-Authored-By: claude-flow <ruv@ruv.net>
`accept_grid` locks each node onto the densest grid it has seen and rejects
sparser frames from the feature path -- on an ESP32-C6 the ~16% HT 64-bin
minority that arrives alongside HE-SU 256-bin. `select_calibration_grid`
ordered candidates by `max_gap_s` first, so it preferred whichever grid
looked temporally smoothest. A sparse trickle at a metronomic cadence has a
smaller worst-case gap than a busy stream with one scheduling hiccup, so
selection kept choosing the grid admission is designed to discard.
Measured on four ESP32-C6 nodes: a capture bound 64sc on node 11, every node
then locked onto 256sc, the bound grid went stale with no frame for five
minutes, frame_count froze at 10,997 and `calibration_grid_is_fresh` failed.
The capture could never finalize -- `collecting` for 858 s with both minimums
long since met, no error surfaced, and a flickering UI as the client retried.
The same rig bound 256sc on a different capture and finalized with five
baseline eigenvalues.
Order by subcarrier count first so selection agrees with admission, then fall
back to the previous gap/rate/ppdu ordering to choose among equally dense
candidates.
Co-Authored-By: Ruflo & AQE
Claude-Session: https://claude.ai/code/session_01Qh7eir6C2U5CSwD7GR43Zv
A room binding may name several radios, and every one of them was admitted
into the field model. That model is single-link: one baseline, one set of
amplitude offsets. Averaging two radios' offsets into it flattens the
eigenstructure and leaves only a scalar energy threshold behind.
Measured on three ESP32-C6 nodes:
4-node binding baseline_eigenvalue_count 0, residual threshold 18.90,
empty room reported an occupant in 1162/1162 frames
1-node binding baseline_eigenvalue_count 5, residual threshold 3.65,
empty room absent 604/604, occupied present 616/616
`person_count_at` already refused to score a non-bound radio, calling it
"deterministic false occupancy from hardware-specific amplitude offsets" --
but only when exactly one node was bound. With more it fell through to the
shared mixed history, so the guard never fired in the case it described.
Restrict the baseline write, not the admission. Every bound radio is still
admitted and recorded as a contributor: `calibration_source_nodes_missing`
refuses to finalize until each one proves it is live on the frozen grid, so
gating them out of the feed path entirely deadlocks the capture -- measured
on hardware as 43,120 frames over 51 minutes stuck in `collecting` with
missing_source_node_ids=[12,13,14]. Only `maybe_feed_calibration` is
restricted to the grid-bound radio, and scoring follows that same radio
whatever the size of the room's node set.
This is the narrowing `bootstrap_baseline::store` already applies when it
persists `vec![binding.source_node_id]` as the frozen model source.
Co-Authored-By: Ruflo & AQE
Claude-Session: https://claude.ai/code/session_01Qh7eir6C2U5CSwD7GR43Zv
Assert the exact wire schema and every identity field the Mac app matches
against its model receipt, plus that evidence agrees with the server's own
person count. Pair the refusals (no receipt, bootstrap-only authority,
expired model) with the positive case so an absent value can never be read as
proof when the path is simply broken.
Co-Authored-By: Ruflo & AQE
Claude-Session: https://claude.ai/code/session_01Qh7eir6C2U5CSwD7GR43Zv
The Mac app gates its held-out empty-room check on
`calibrated_presence_evidence` carrying
`ruview.calibration.calibrated-presence-evidence.v2`. No shipped server has
ever emitted that key, so the check fails on its first iteration with "No
fresh calibrated presence result matched this room's current model receipt",
`bootstrap/promote` is never called, and `bootstrap_baseline::store` -- the
only path that persists a calibration -- never runs. A completed 62-minute
field model is therefore lost on the next restart.
Emit the evidence bound to the active model receipt. `None` unless an
explicit calibration is fresh and the field model scores the observation
without a heuristic fallback, so a present value is always calibrated.
Extract the field-model scoring history into `scoring_history()` and reuse it
for both `person_count_at` and the evidence, so the published evidence cannot
disagree with the count derived from the same model.
Co-Authored-By: Ruflo & AQE
Claude-Session: https://claude.ai/code/session_01Qh7eir6C2U5CSwD7GR43Zv