Commit Graph
1438 Commits
Author SHA1 Message Date
ruv 9baaf8434f Merge PR #2155 (aepod) into release integration 2026-10-05 11:44:21 -04:00
ruv 27eefb6a7f Merge PR #2140 (aepod) into release integration 2026-10-05 11:44:20 -04:00
ruv c6f7e8ca0d Merge PR #2120 (aepod) into release integration 2026-10-05 11:44:20 -04:00
ruv 474f1a904e Merge PR #2118 (aepod) into release integration 2026-10-05 11:44:19 -04:00
ruv ddaa54eba0 Merge PR #2110 (aepod) into release integration 2026-10-05 11:44:19 -04:00
ruv b3d31510ee Merge PR #2130 (aepod) into release integration 2026-10-05 11:44:18 -04:00
ruv bce512cb8d Merge PR #2131 (aepod) into release integration 2026-10-05 11:44:18 -04:00
ruv 9950ecc340 fix(provision): refuse to write credentials through a symlink with a clear error
Co-Authored-By: claude-flow <ruv@ruv.net>
2026-10-05 11:44:17 -04:00
ruv e0c4207bd5 Merge PR #2152 (aepod) into release integration
# Conflicts:
#	firmware/esp32-csi-node/provision.py
2026-10-05 11:43:47 -04:00
ruv 044a41d2de Merge PR #2153 (aepod) into release integration 2026-10-05 11:43:14 -04:00
ruv 8d243a67f4 Merge PR #2111 (aepod) into release integration 2026-10-05 11:43:13 -04:00
Mathew Beaneandclaude-flow f5a36666fc fix(hardware): accept ESP32 frames up to the firmware buffer size
Esp32CsiParser capped n_subcarriers at 256. Real ESP32-S3 nodes on
firmware 0.8.12 send 1-antenna frames with 306 subcarriers (612 I/Q
bytes), so the parser dropped about a third of live frames. Pointcloud
started hitting that cap once it switched to this parser.

The bound now comes from the firmware: frames are serialized into
CSI_MAX_FRAME_SIZE = 20 + 4 * 256 * 2 bytes, so a frame carries at most
1024 I/Q pairs across all antennas. The check is n_antennas *
n_subcarriers <= 1024.

HT/legacy bandwidth no longer maps subcarrier counts above 128 to 80 or
160 MHz. ESP32 radios use 20/40 MHz channels, and the larger counts
(192, 306) come from merged LLTF/HT-LTF buffers, so anything above 64
is reported as 40 MHz.

The pointcloud UDP receive buffer grows from 2048 to 4096 bytes so the
largest legal frame (2068 bytes) is not truncated.

Refs #1348

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-10-04 16:52:10 -04:00
Mathew Beaneandclaude-flow d78a221607 feat(provision): read the WiFi password from a file or a prompt
--password puts the WiFi password in argv, where ps and shell history can
see it (#1754). This adds two ways to give it without that:

- --password-file PATH reads it from a file, dropping one trailing newline
  (\n or \r\n). The file must exist and not be empty. If group or others can
  read it, the script refuses and suggests chmod 600.
  --allow-insecure-password-file accepts such a file with a warning, for
  read-only mounts such as 0444 secrets.
- When --ssid is given with no password flag and stdin is a terminal, the
  script asks with getpass. It reuses a saved password only if it was saved
  for the same SSID. Without a terminal (scripts, CI) nothing is asked and
  the existing missing-credentials check applies.

--password still works, but now prints a one-line warning on stderr that
points at --password-file. The two flags are mutually exclusive. The
firmware README examples now use --password-file.

Tests: 10 new tests in tests/test_provision_secrets.py, with placeholder
credentials and getpass and the flasher stubbed. 8 fail on the previous
commit; the other 2 check that nothing is asked without a terminal or when a
saved password matches the SSID. The suite goes from 25 to 35 passing.

Refs #1754

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-10-04 16:50:43 -04:00
Mathew Beaneandclaude-flow 3a8ec8139e fix(pointcloud): parse ESP32 CSI frames with the reference ADR-018 parser
The pointcloud parser read channel from byte 8, RSSI from byte 9, noise
floor from byte 10 and a "timestamp" from bytes 16..19. The firmware
(csi_serialize_frame) writes freq_mhz as u32 at 8..11, sequence at
12..15, RSSI at 16, noise floor at 17 and PPDU type/flags at 18..19. So
RSSI came out as the high byte of the frequency (+9 for 2437 MHz), and
the tomography attenuation was built from it.

parse_adr018 now delegates to wifi_densepose_hardware::Esp32CsiParser,
the same parser the rest of the workspace uses, and only reshapes its
output. CsiFrame.channel/timestamp_us become freq_mhz/sequence, which
are what the header actually carries. ADR-081 feature-state packets
(0xC5110006) are no longer decoded as CSI frames.

build_test_frame (used by csi-test) now emits the real layout. Golden
byte-by-byte tests cover single and two-antenna frames, truncation and
the feature-state packet; a cross-check test compares against the
reference parser.

Refs #1348

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-10-04 16:30:45 -04:00
Mathew Beaneandclaude-flow 98c1d4a1e9 fix(provision): key saved state by board MAC, not serial port
provision.py saved each board's settings under the serial port path. Ports
get reused when boards are swapped (a second board on the same hub slot often
comes up as the same /dev/cu.usbmodem*), so the next board inherited the
previous board's node_id, target_port and channel without any sign (#1755).

- On a real flash the script reads the base MAC with `esptool read_mac` and
  keeps state in mac-<aabbccddeeff>.json. The record also stores the MAC and
  the port it was last written from.
- EUI-64 chips (C5, C6, H2) print an 8-byte "MAC:" line before "BASE MAC:";
  the parser takes the base MAC on those and the single MAC line elsewhere.
- Migration: if the board has no record yet and a port-keyed file from an
  earlier version exists, it is used once, with a notice that points at
  --reset, then removed after the save so no other board can pick it up.
- --state and --dry-run still never open the port. --state shows the board
  last provisioned from that port, and --mac selects a board by hand.
  --dry-run without --mac keeps the port-keyed file, which the QEMU swarm test
  relies on (--port /dev/null --dry-run).
- If the MAC can't be read, state stays keyed by port and the script says so.

Tests: tests/test_provision_mac_state.py adds 18 tests, using placeholder
MACs, with esptool, the NVS generator and the flasher stubbed. 15 of them
fail on the base; the other 3 guard behaviour the base already has (no serial
access on --dry-run, --reset, rejecting a bad --mac). The suite goes from 14
to 32 passing.

Not verified on hardware: the real esptool read_mac call. The output shapes
in the tests come from esptool 4.12 and 5.4.0 cmds.py.

Refs #1755

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-10-04 16:28:47 -04:00
Mathew Beaneandclaude-flow c00deadaf7 fix(provision): keep the WiFi password owner-only and out of --state output
The per-port state file holds the WiFi password and seed token in clear, but
save_state() created the directory and file with the default umask, so they
landed 0755/0644 and any local user could read them (#1754). --state also
printed both values verbatim, and that output gets pasted into issues.

- The state dir is created 0700 and state files 0600. Staging uses mkstemp
  (unique name, O_EXCL, 0600) plus fchmod, so a stale temp file or a planted
  symlink can't receive the secret.
- harden_state_dir() tightens the dir and every .json/.tmp file in it,
  including ones left by older versions. main() runs it before any read or
  early exit, so --state repairs old files too. It skips symlinks and files
  owned by other users, and only warns if chmod fails.
- The --dry-run NVS binary and the generator-failure CSV fallback are written
  0600 even when the file already exists. The generator's CSV and binary now
  live in a private mkdtemp() dir instead of the shared temp dir.
- --state shows password and seed_token as (set)/(empty). --show-secrets
  prints them.

Windows is unchanged: chmod can't set owner-only ACLs there.

Tests: tests/test_provision_secrets.py adds 11 tests; all 11 fail on the
base and the suite goes from 14 to 25 passing.

Refs #1754

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-10-04 16:25:43 -04:00
Dragan Spiridonov 7c8aeacea2 Merge pull request #2147 from ruvnet/codex/calibrated-vitals-empty-20261004
fix(sensing): gate vital outputs on fresh calibrated occupancy
v3037
2026-10-04 15:07:58 +02:00
Dragan Spiridonov c3764a429f fix(sensing): require calibrated occupancy before publishing vitals 2026-10-04 11:50:13 +02:00
Mathew Beaneandclaude-flow cbd84acf6c fix(unified): stop GaussianMap::decay compounding across calls
decay(now) multiplied confidence by exp(-(now - timestamp_ns)/tau_eff)
on every call, but timestamp_ns is the last-seen time and does not move
when decay runs. Calling decay(t1) then decay(t2) applied
exp(-(t1-ts)/tau) * exp(-(t2-ts)/tau) instead of exp(-(t2-ts)/tau), so
confidence fell faster the more often the caller ticked.

Track how far each Gaussian has already been decayed (decayed_to_ns)
and only apply the interval since max(decayed_to_ns, timestamp_ns).
A fresh observation still restarts the clock, and the lifetime-stretched
tau_eff is unchanged. Fusion and the merge pass keep the later of the
two decayed_to_ns values. The new field is #[serde(default)], so maps
serialized before this change load with 0 and decay from timestamp_ns
as before.

Adds two tests: one-shot vs. per-second decay must agree to 1e-12, and
the wall-vs-transient persistence case must hold when decay runs every
10 s.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-10-03 22:58:13 -04:00
Mathew Beaneandclaude-flow 7af5477f04 fix(scripts): parse the ADR-018 header layout in the UDP capture scripts
Both capture scripts decoded a header layout the firmware does not send.
csi_collector.c and esp32_parser.rs use: magic u32 @0, node_id @4,
n_antennas @5, n_subcarriers u16 @6, freq_mhz u32 @8, sequence u32 @12,
rssi i8 @16, noise_floor i8 @17, ppdu_type @18, flags @19, then
n_antennas * n_subcarriers int8 I/Q pairs from @20.

- collect-training-data.py read a 16-byte header and int16 I/Q, so its
  length check rejected every real single-antenna frame. It now reads the
  20-byte header and int8 pairs, records freq/channel/seq/ppdu/flags, and
  lets the recorder stamp receive time (the header has no timestamp).
- record-csi-udp.py read rssi from byte 6 and channel from byte 7 (the
  n_subcarriers bytes) and treated bytes 8-19 as I/Q, so a 64-subcarrier
  frame came out as 70 subcarriers with header bytes in the first six
  amplitudes. It now checks the magic, reads the real fields, and writes
  only the I/Q block to iq_hex.
- Add scripts/tests/test_capture_adr018.py, which builds frames with the
  esp32_parser.rs test layout and checks the decoded fields.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-10-02 22:01:22 -04:00
Mathew Beaneandclaude-flow 84f5e19e9d fix(scripts): run the proof with python3 and report INCOMPLETE when a gate is skipped
prove.sh looked only for `python`, so on macOS (python3 only) it skipped
the deterministic proof and still printed RESULT: PASS with exit 0.

- Pick $PYTHON, then python3, then python.
- Check numpy/scipy import before running verify.py and print the
  lockfile install command when they are missing; warn when installed
  versions differ from archive/v1/requirements-lock.txt.
- Run verify.py single-threaded, as CI does.
- Hard gates that cannot run (no cargo, no Python, missing deps) are
  reported as [NOT RUN] and the run ends INCOMPLETE with exit 2.
  Data/hardware-gated claims are unchanged.
- Tell people to stop any local server on UDP 5005 first (#2086).

Fixes #2095

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-10-02 21:57:49 -04:00
Mathew Beaneandclaude-flow 0eb952a9c5 fix(sensing-server): evict stale node_states on a timer, not only on frame arrival
Stale node eviction (60 s) lived inside the ESP32 receive path and ran
every 100 ticks, so it only happened while ESP32 frames kept arriving.
After a burst of bogus node ids, or once every node went quiet, nothing
ever removed the entries, and they stayed listed in /api/v1/nodes for the
life of the process.

Move the existing rule unchanged into evict_stale_node_states (60 s,
nodes that never reported included). Call it from the receive path as
before, and from a new node_states_eviction_task that sweeps every 30 s
alongside the UDP receiver.

Developed in whitsentry, a RuView-based project.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-10-02 19:22:26 -04:00
Mathew Beaneandclaude-flow 08f0f8859b fix(sensing-server): reject ESP32 CSI frames with zero antennas or subcarriers
parse_esp32_frame accepted any datagram that started with the 0xC511_0001
magic and had a 20-byte header. With n_antennas or n_subcarriers at zero,
the expected length collapses to the header itself, so the length check
passed and the frame minted a node carrying empty amplitude/phase arrays.
A non-ESP32 sender on the shared UDP port produced exactly that: bogus
nodes in /api/v1/nodes.

Return None when either count is zero. Well-formed frames parse as before.
The new tests cover zero antennas, zero subcarriers, and a bare header,
and check that a well-formed frame still parses.

Developed in whitsentry, a RuView-based project.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-10-02 19:22:26 -04:00
Mathew Beaneandclaude-flow abeca0f0ed fix(sensing-server): send multi-node test frames to an ephemeral port
The multi_node_test UDP smoke tests sent synthetic ADR-018 CSI and
vitals frames to 127.0.0.1:5005, the sensing server's default port.
Because the tests are not ignored, a routine `cargo test` (and
scripts/prove.sh) could inject up to 255 fake node IDs into a locally
running server, and the hardcoded port could collide with other
processes.

Each test now binds its own receiver on 127.0.0.1:0 and sends to the
OS-assigned address, keeping the receiver alive for the test's
duration. The tests still only exercise the sender and frame-builder
path; no server is involved.

Ported from whitsentry, a RuView-based project, which carries the same fix.

Closes #2086

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-10-02 16:04:54 -04:00
Mathew Beaneandclaude-flow 5a36be17d2 fix(provision): make --dry-run and failed --reset leave state intact
--dry-run persisted the merged state record, so the next real run merged
on top of values that never reached the device. It now skips save_state.
The NVS binary is still written to nvs_provision.bin because ADR-061 and
scripts/qemu-mesh-test.sh consume it.

--reset deleted the per-port state file before argument validation, so a
run that then failed (e.g. missing WiFi credentials) still lost the record.
It now only ignores the prior state; a successful flash overwrites the file
as before.

Also fix the module docstring's package name: the PyPI package is
esp-idf-nvs-partition-gen, matching the runtime hint.

Fixes #2096

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-10-02 16:03:30 -04:00
rUv 03a897938f Merge pull request #2062 from ruvnet/claude/ruview-npm-package-9uc5x3
feat(npm): RuView npm toolkit @ruvnet/ruview 0.9.1 — kernel, SDK, flashing, device access, MCP Apps, live Claude Code views (ADR-368..378)
v2975 ruview-harness-v0.9.1
2026-10-01 20:53:26 -04:00
ruv 15d48d4481 style(homecore-hap): rustfmt the new verifier test
Co-Authored-By: claude-flow <ruv@ruv.net>
2026-10-01 19:55:28 -04:00
ruv c764c8916b fix(homecore-hap): pad the SRP verifier to 384 bytes (flaky provisioning)
compute_verifier returns the minimal big-endian encoding, so about one
provisioning in 256 produced a verifier with a zero top byte, encoded to
383 bytes, and failed the store's own 384-byte check
("SRP verifier must contain exactly 384 bytes"). Seen in CI on
first_provisioning_returns_code_but_restart_does_not. Left-pad to the
3072-bit group width at creation: the same number, the fixed length.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-10-01 19:55:05 -04:00
rUv 5efc7e2c2c Merge pull request #2073 from ruvnet/feat/ruview-mcp-apps-ui
feat(npm): live sensor views in Claude Code, MCP Apps console, ESPHome radar, ruview umbrella — @ruvnet/ruview 0.9.1 (ADR-375..378)
2026-10-01 19:23:17 -04:00
ruv 39b4ae9ae1 release: @ruvnet/ruview 0.9.1 and ruview umbrella 0.9.1
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>
2026-10-01 18:57:49 -04:00
ruv 4a472926e1 fix(ruview-live): code-review findings on the waterfall (ADR-378)
- 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>
2026-10-01 18:47:18 -04:00
ruv c4f9d95e11 fix(ruview-live): open as a dialog that takes the keys, sized per view
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>
2026-10-01 18:35:19 -04:00
ruv bf21c8c7a9 Revert "fix(ruview-live): fit the inline pane; actions on top; show focus state"
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>
2026-10-01 18:33:28 -04:00
ruv 629e1b9c6c fix(ruview-live): fit the inline pane; actions on top; show focus state
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>
2026-10-01 18:32:12 -04:00
ruv 78a1474057 fix(ruview-live): pane hotkeys need the keyboard — ask for focus, say how
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>
2026-10-01 18:27:52 -04:00
ruv 751b7ae17b feat(ruview-live): showcase views — CSI waterfall, radar fan, animated cells (ADR-378)
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>
2026-10-01 18:22:40 -04:00
ruv c4d7ef8086 feat(ruview-live): redesigned pane — LIVE badge, cards, trends, plausibility flags
- 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>
2026-10-01 17:50:09 -04:00
ruv bd7c506076 fix(ruview-live): a reloaded pane waited forever for its first capture
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>
2026-10-01 17:44:00 -04:00
ruv 8e12606c6a fix(ruview-live): pane showed "updated Invalid Date"
$.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>
2026-10-01 17:40:59 -04:00
ruv 8b857e41cd docs: README and user guide for @ruvnet/ruview 0.9.0 (devices, radar, MCP/ChatGPT, Claude Code mod)
- README MetaHarness section pinned to 0.9.0 with the hardware, MCP-over-HTTP,
  mod, Homecore and kernel commands; plugin section lists the ruview-live mod.
- User guide: new "RuView npm toolkit" section (hardware CLI, alert meanings,
  ESPHome kits, MCP/ChatGPT connection, the /ruview mod, Homecore + kernel),
  TOC entry, 0.5.0 pins updated.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-10-01 17:26:40 -04:00
ruv 093e3bbeae feat(ruview): ruview-live Claude Code mod shipped in the npm package (ADR-377)
- 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>
2026-10-01 17:12:53 -04:00
ruv 4128661789 docs(adr-376): record release outcome — components published, unscoped ruview refused by npm (403, similar to iview)
Co-Authored-By: claude-flow <ruv@ruv.net>
2026-10-01 16:45:20 -04:00
ruv b6f3abd35e feat(npm): ruview umbrella package + ESPHome radar source (ADR-376, ADR-373 A3)
- 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>
2026-10-01 16:30:23 -04:00
ruv 45796e4e3e feat(ruview): MCP Apps console widget, HTTP transport, terminal UI (ADR-375)
- ui://ruview/console-v1.html: one self-contained widget for MCP Apps hosts
  and ChatGPT (ui.resourceUri + openai/outputTemplate). Renders node
  streams, device scans and doctor results; Refresh re-runs the tool via
  window.openai.callTool or an MCP Apps tools/call. Empty CSP allowlist, no
  network, textContent only, parent-frame messages only.
- MCP: protocol negotiation (2025-06-18/2025-03-26/2024-11-05),
  structuredContent + compact JSON text (17-33% smaller, MEASURED),
  resources/list|read, tool titles, canonical tool name in result _meta.
  Transport-neutral handleRpc + shared FIFO dispatcher.
- `mcp start --http`: Streamable HTTP for ChatGPT/remote clients. Bearer or
  secret-path token (constant-time), loopback default, Origin allowlist,
  256 KiB cap, no batches, write grants never honoured over HTTP.
- CLI: formatted TTY output (tables, rate bars, alerts first, NO_COLOR),
  JSON for pipes/--json, capture countdown, `esp32 --watch` with per-node
  sparklines, lazy module loading (--help ~20% faster, MEASURED).
- monitor detects Ameba CSI report-buffer starvation ("lack of csi buf"),
  the root cause of the Realtek heartbeat-only stall seen live.
- Size budget 320 -> 384 KiB (package ~339 kB).

Verified: 160 tests + 83 security tests; headless-Chrome e2e of the MCP
Apps flow against live boards (refresh round trip 108 -> 105 packets);
HTTP security probes; clean tarball smoke (stdio + HTTP).

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-10-01 15:52:26 -04:00
ruv 3cd360f9e2 fix(ruview): CSI loss survives stray and reordered sequence numbers
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>
2026-10-01 15:20:21 -04:00
Claudeandclaude-flow d31d9daba2 fix(nightly-sota): ignore stdin EPIPE from children that never read input
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
2026-10-01 19:07:40 +00:00
ruv b25729acd6 fix(ruview): boot-log capture aborted on non-ASCII firmware logs on Windows
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>
2026-10-01 15:02:09 -04:00
ruv 8d2be98ed7 feat(ruview): Realtek RAC1 nodes, honest capture, live CSI analysis (ADR-373 Amendment 2)
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>
2026-10-01 14:40:32 -04:00
Claudeandclaude-flow 43f16e5638 perf(npm): binary kernel transport, faster device e2e, and CI fixes (ADR-368/373 amendments)
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
2026-10-01 14:45:55 +00:00
Claudeandclaude-flow 6112a185df feat(npm): host device + SSH remote access — ESP32, 60/24 GHz mmWave, LiDAR (ADR-373/374)
@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
2026-09-30 17:01:15 +00:00