Compare commits

..

5 Commits

Author SHA1 Message Date
rUv 4bf88e1283 feat(firmware): gate LED gamma viz behind CONFIG_LED_GAMMA_VIZ (ADR-183 follow-up) (#1129)
The 40 Hz gamma flicker is now Kconfig-gated (default y, unchanged
behaviour). Set CONFIG_LED_GAMMA_VIZ=n for a dark, lower-power boot (the
LED is simply cleared) — important for photosensitive deployments, no
source edit needed. The colormap saturation point is now operator-tunable
via CONFIG_LED_MOTION_FULLSCALE_MILLI (default 250 = 0.25).

Build + flash confirmed on ESP32-S3 N16R8 (COM8): default y still arms the
gamma timer, CSI flows. ADR-183 updated (gate moved from follow-up to done).
2026-06-17 22:22:20 -04:00
rUv a4c2935a2f feat(firmware): onboard LED 40 Hz gamma stimulus + CSI-motion colour (ADR-183) (#1127)
* chore(deps): bump ruv-neural submodule — ColorMap no_std for ESP32

Points to ruvnet/ruv-neural#3 (c9638fa): ruv-neural-viz::ColorMap now
builds no_std, so it can run on the ESP32. Unblocks driving the onboard
WS2812 from the viridis/cool-warm colormap.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(firmware): onboard LED as 40 Hz gamma stimulus, colour from live CSI motion (ADR-183)

The S3 onboard WS2812 (GPIO 48, #962) now runs a GENUS-style 40 Hz gamma
square wave (12.5 ms on/off, 50% duty). The ON-phase colour is live CSI
motion (edge motion_energy) mapped through a 60-step viridis LUT generated
from ruv-neural-viz::ColorMap::viridis() — still=purple, moving=yellow.

Uses the now-no_std ColorMap (ruvnet/ruv-neural#3 / #1126). Hardware-
confirmed on ESP32-S3 N16R8 (COM8): boot log shows the timer armed, CSI
keeps flowing (27-38 pps). Honesty + photosensitivity notes + a Kconfig-gate
follow-up are in ADR-183.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-17 21:04:02 -04:00
rUv 315d7df09e chore(deps): bump ruv-neural submodule — ColorMap no_std for ESP32 (#1126)
Points to ruvnet/ruv-neural#3 (c9638fa): ruv-neural-viz::ColorMap now
builds no_std, so it can run on the ESP32. Unblocks driving the onboard
WS2812 from the viridis/cool-warm colormap.
2026-06-17 20:18:35 -04:00
rUv bdd1eaf927 chore: untrack ruvector.db runtime artifacts + gitignore at any depth (#1124)
These are hook/runtime-generated databases (ruvector/intelligence store)
that kept showing as dirty and don't belong in version control. Removed
from the index (files kept on disk) and ignored globally.
2026-06-17 17:49:47 -04:00
rUv 4001e9e178 feat(harness): npx @ruvnet/ruview operator harness + ADR-182 (#1123)
A host-portable RuView agent harness minted via MetaHarness and hardened
per ADR-182. Published as @ruvnet/ruview@0.1.0 (bare `ruview` blocked by
npm's typosquat filter → scoped fallback).

What it does:
- 6 fail-closed `ruview.*` tools (onboard, claim_check, verify,
  node_monitor, calibrate, node_flash) exposed as CLI verbs + a
  dependency-free MCP stdio server.
- The "prove everything" rule made executable: `ruview.claim_check`
  flags untagged accuracy claims and the retracted "100%" framing.
- 5 host-neutral skills (onboard/provision-node/calibrate-room/
  train-pose/verify) + bundled .claude/ config + provenance manifest.

Validated: 17/17 unit tests, live MCP handshake, `ruview.verify` ran the
real verify.py to VERDICT: PASS, clean `npx @ruvnet/ruview` from registry.
Packs to 16.7 kB / 21 files; kernel+host are optionalDependencies so the
operator tools install lightweight.

README: documented as the portable, multi-host companion to the in-repo
plugins/ruview/ Claude Code plugin (not a replacement).
2026-06-17 17:46:31 -04:00
34 changed files with 1779 additions and 6 deletions
+11
View File
@@ -280,3 +280,14 @@ assets/MM-Fi/*.zip
# through-wall demo: regenerable trained model artifact
examples/through-wall/model/
# RuView harness (npx ruview) build artifacts — ADR-182
harness/**/node_modules/
harness/**/*.tgz
harness/**/package-lock.json
harness/**/.claude-flow/
harness/**/ruvector.db
# ruvector runtime/hook DB — never tracked (any depth)
ruvector.db
**/ruvector.db
+3
View File
@@ -601,6 +601,8 @@ claude --plugin-dir ./plugins/ruview
Verify the plugin structure: `bash plugins/ruview/scripts/smoke.sh`. Full details: [`plugins/ruview/README.md`](plugins/ruview/README.md).
**Portable harness — `npx @ruvnet/ruview`:** a lighter, host-portable companion to the in-repo plugin, minted via [MetaHarness](https://www.npmjs.com/package/metaharness) and hardened per [ADR-182](docs/adr/ADR-182-npx-ruview-harness-via-metaharness.md). It runs **without cloning this repo** and on more hosts (Claude Code, Codex, Copilot, opencode, …), exposing the RuView operator tools (`onboard`, `verify`, `node_monitor`, `calibrate`, `node_flash`) over an MCP server — plus the project's **MEASURED-vs-CLAIMED honesty guardrail enforced in code** (`ruview.claim_check` flags untagged or retracted-"100%" accuracy claims). v0.1: the onboarding/verify/claim-check paths are tested (17/17, `verify.py` → PASS); the hardware tools are fail-closed wrappers. Try `npx @ruvnet/ruview` to onboard, or `npx @ruvnet/ruview claim-check --text "…"`. Source: [`harness/ruview/`](harness/ruview/README.md).
---
## 📖 Documentation
@@ -614,6 +616,7 @@ Verify the plugin structure: `bash plugins/ruview/scripts/smoke.sh`. Full detail
| [**SENSE-BRIDGE — rvagent MCP server**](tools/ruview-mcp/README.md) | Dual-transport MCP server (`@ruvnet/rvagent`) bridging the RuView sensing stack to AI agents (Claude Code, Cursor, ruflo swarms). 6 tools wired: `ruview.presence.now`, `ruview.vitals.get_{breathing,heart_rate,all}`, `ruview.bfld.last_scan`, `ruview.bfld.subscribe`. stdio + Streamable HTTP (`POST /mcp`, Origin-validated, bearer-token auth, `127.0.0.1` bind). Full 20-tool Zod schema barrel + 5 RUVIEW-POLICY governance tools. 93 tests. [ADR-124](docs/adr/ADR-124-rvagent-mcp-ruvector-npm-integration.md). Try: `npx @ruvnet/rvagent stdio`. |
| [Semantic Primitives — Precision/Recall](docs/integrations/semantic-primitives-metrics.md) | Per-primitive F1 on the held-out paired-capture set: someone-sleeping, possible-distress, room-active, elderly-inactivity-anomaly, meeting, bathroom, fall-risk, bed-exit, no-movement, multi-room. |
| [Claude Code / Codex Plugin](plugins/ruview/README.md) | The `ruview` plugin + marketplace — skills, `/ruview-*` commands, agents, and the Codex prompt mirror |
| [Portable harness — `npx @ruvnet/ruview`](harness/ruview/README.md) | MetaHarness-minted, host-portable RuView operator harness — `ruview.*` MCP tools + the MEASURED-vs-CLAIMED honesty guardrail enforced in code ([ADR-182](docs/adr/ADR-182-npx-ruview-harness-via-metaharness.md)). A lighter, multi-host companion to the in-repo plugin. |
| [Architecture Decisions](docs/adr/README.md) | 96 ADRs — why each technical choice was made, organized by domain (hardware, signal processing, ML, platform, infrastructure) |
| [Domain Models](docs/ddd/README.md) | 8 DDD models (RuvSense, Signal Processing, Training Pipeline, Hardware Platform, Sensing Server, WiFi-Mat, CHCI, rvCSI) — bounded contexts, aggregates, domain events, and ubiquitous language |
| [rvCSI — edge RF sensing runtime](https://github.com/ruvnet/rvcsi) | Rust-first / TypeScript-accessible / hardware-abstracted CSI runtime: multi-source ingestion (incl. real nexmon_csi `.pcap` from a **Raspberry Pi 5** / Pi 4 / Pi 3B+ — CYW43455 / BCM43455c0) → validation → DSP → typed events → RuVector RF memory ([ADR-095](docs/adr/ADR-095-rvcsi-edge-rf-sensing-platform.md), [ADR-096](docs/adr/ADR-096-rvcsi-ffi-crate-layout.md), [domain model](docs/ddd/rvcsi-domain-model.md)). Now its own repo — [`ruvnet/rvcsi`](https://github.com/ruvnet/rvcsi) — vendored here under `vendor/rvcsi`; 9 `rvcsi-*` crates on crates.io, `@ruv/rvcsi` on npm, plus a Claude Code plugin. |
Binary file not shown.
@@ -0,0 +1,279 @@
# ADR-182: `npx ruview` — A RuView Agent Harness Minted via MetaHarness
| Field | Value |
|-------|-------|
| **Status** | Accepted — **P1+P2 implemented & validated** (`harness/ruview/`, 17/17 tests, MCP handshake + `ruview.verify` PASS against the real repo, packs to 16.7 kB / 21 files) · P3 publish-ready (name decision pending) · P4 (router + provenance) designed |
| **Date** | 2026-06-17 |
| **Deciders** | ruv |
| **Codename** | **RUVIEW-HARNESS** |
| **Builds on** | MetaHarness (`metaharness@0.1.15`, `@metaharness/kernel`, `@metaharness/host-*`, `@metaharness/router`), the `ruview-*` Claude Code subagents (`ruview-onboarding-guide`, `ruview-config-engineer`, `ruview-training-engineer`), the `wifi-densepose` CLI (`calibrate`/`enroll`/`train-room`/`room-watch`), the sensing-server, ADR-028 (witness verification), ADR-095/096 (rvCSI runtime), ADR-260/262 (RuField bridge) |
| **Supersedes** | none |
## Context
RuView (WiFi-DensePose) is a deep stack — 15 Rust crates, an ESP32 firmware line,
a sensing-server, a CLI, ~180 ADRs, a calibration pipeline, training recipes, and a
hard cultural rule that **every claim must be independently reproducible** (the
"prove everything" ethos, after the project was accused of AI-slop). The barrier to
entry is correspondingly steep: a newcomer who wants to "set up WiFi sensing" must
discover the right firmware variant, provision an ESP32 over a Windows-only Python
subprocess, point it at the sensing-server, run `calibrate``enroll`
`train-room`, and know which numbers are MEASURED vs CLAIMED. We already encode this
knowledge as **Claude Code subagents** (`ruview-onboarding-guide`,
`ruview-config-engineer`, `ruview-training-engineer`) — but those only exist inside
*this* repo's `.claude/agents/`, only on Claude Code, and only for someone who has
already cloned the monorepo.
Separately, this session shipped **MetaHarness** (`metaharness@0.1.15`): a tool that
*"mints a custom AI agent harness from any repo"*, runnable on **9 hosts**
(claude-code, codex, pi-dev, hermes, openclaw, rvm, copilot, opencode,
github-actions) over a wasm-primary / NAPI-RS-fallback **kernel**, with a
**cost-optimal model router** (`@metaharness/router`, the productized DRACO Phase-2
k-NN finding) and ed25519/SLSA/SBOM provenance baked in. Crucially, MetaHarness
**already ships a `vertical:ruview` template** in its template list. That template
is generic scaffolding; it is not wired to RuView's actual tools, agents, or the
"prove everything" guardrails.
The gap: **there is no single, host-portable, provenance-signed entry point that
gives any user an AI agent that actually knows how to operate RuView.** A user
should be able to run one command —
```bash
npx ruview
```
— in an empty directory (or alongside an ESP32) and get an agent harness that can
onboard them, configure firmware, drive a live capture, train a room model, and
**refuse to overstate accuracy** — on whichever coding host they already use.
## Decision
**Mint a first-class RuView agent harness from this repo using MetaHarness, harden
its `vertical:ruview` template into a RuView-specific harness with a real MCP tool
surface and the project's honesty guardrails, and publish it as `npx ruview`.**
`npx ruview` is *not* a new runtime. It is a **thin, versioned distribution** of a
MetaHarness harness: the kernel + host adapters + a RuView "genome" (skills, agents,
MCP tools, guardrails) generated from and pinned against this monorepo. The harness
is the product; `npx ruview` is the front door.
### Why mint-from-repo instead of hand-writing a harness
MetaHarness's value here is exactly the work we would otherwise hand-roll across 9
hosts: host-specific config (`.claude/settings.json` MCP + hooks for claude-code,
the codex/copilot/opencode equivalents), the kernel that abstracts wasm-vs-native,
the cost router, and the provenance chain. We write the **RuView knowledge once** as
host-neutral genome assets; MetaHarness projects them onto each host adapter. This
also keeps the harness regenerable: when the CLI or an ADR changes, re-mint and
re-pin rather than maintaining 9 divergent copies.
### What the harness contains (the RuView genome)
1. **Skills / playbooks** (host-neutral markdown, projected to each host's skill
format):
- `onboard` — zero-to-sensing path picker (Docker demo / repo build / live
ESP32), the physics caveats, the hardware table. Port of
`ruview-onboarding-guide`.
- `provision-node` — ESP-IDF v5.4 Windows-subprocess build/flash/provision flow
(the exact MSYSTEM-stripped invocation from `CLAUDE.local.md`), firmware
variant selection (8MB display / 4MB no-display / C6), NVS + WiFi + channel /
MAC-filter overrides (ADR-060).
- `calibrate-room``baseline → enroll → extract → train` via the
`wifi-densepose` CLI (`calibrate`/`calibrate-serve`/`enroll`/`train-room`/
`room-watch`, ADR-151).
- `train-pose` — camera-supervised + camera-free training, the MEASURED-vs-CLAIMED
discipline, the mean-pose baseline check (ADR-079, ADR-152, ADR-181).
- `verify` — run the witness bundle + Python proof (`verify.py` → VERDICT: PASS),
ADR-028.
- Ports of `ruview-config-engineer` and `ruview-training-engineer`.
2. **MCP tool surface** (`@metaharness/kernel`-hosted MCP server, one schema per
capability — see "MCP tools" below). This is what makes the harness *operate*
RuView, not just talk about it.
3. **Guardrails** (the differentiator): the harness's system prompt and a
pre-output hook enforce the "prove everything" rule — accuracy numbers must be
tagged MEASURED (with a reproducer) or CLAIMED; the agent must run the mean-pose
baseline before quoting PCK; firmware fixes are never presented as
hardware-validated without a real boot log (the exact discipline this session
followed for `v0.8.1-esp32`).
4. **Host adapters** — claude-code first (P1), then codex / opencode / copilot /
pi-dev / hermes / rvm / github-actions (P3+), each via the published
`@metaharness/host-*` package.
5. **Router**`@metaharness/router` routes each step to the cheapest adequate
model (e.g. a var-rename or a log-grep → Haiku; calibration-math reasoning or a
security review → Sonnet/Opus), mirroring the repo's 3-tier routing (ADR-026).
### MCP tools (the operational surface)
| Tool | Wraps | Purpose |
|------|-------|---------|
| `ruview.onboard` | docs + agent | Pick a setup path, print the next concrete command |
| `ruview.node.flash` | ESP-IDF subprocess (ADR `CLAUDE.local.md`) | Build + flash a firmware variant to a COM port |
| `ruview.node.provision` | `provision.py` | Set SSID/password/target-ip/channel/MAC-filter over serial |
| `ruview.node.monitor` | pyserial | Stream boot log; assert CSI is flowing (MGMT+DATA) |
| `ruview.server.up` | sensing-server | Start the Axum sensing-server (`:3000`/`:5005`/`:8765`) |
| `ruview.calibrate` | `wifi-densepose calibrate`/`enroll`/`train-room` | Run the ADR-151 room pipeline |
| `ruview.room.watch` | `wifi-densepose room-watch` | Live presence/vitals from a trained room |
| `ruview.verify` | `scripts/generate-witness-bundle.sh` + `verify.py` | Produce/verify the witness bundle (must be N/N PASS) |
| `ruview.claim.check` | static lint | Scan output for untagged accuracy claims; flag MEASURED-vs-CLAIMED |
Each tool returns structured JSON and is fail-closed: a tool that cannot prove its
result (e.g. `ruview.node.monitor` sees no CSI callbacks) returns an honest negative,
never a fabricated success — consistent with the RuField `map_privacy` fail-closed
posture (ADR-262 §3.3).
### The mint + pin flow (how the harness is produced)
```bash
# P1 — mint from this repo, claude-code host, RuView vertical
npx metaharness ruview --template vertical:ruview --host claude-code \
--from-existing . --description "RuView WiFi-sensing operator agent" \
--target ./harness/ruview
# readiness + fit/cost/safety scorecards (ADR-041) — gate before publish
npx metaharness genome . # 7-section repo readiness
npx metaharness score . --json # fit / cost / safety
npx metaharness analyze . # recommended harness plan (no-exec)
```
The minted harness is committed under `harness/ruview/` and **pinned** (kernel +
host-adapter + router versions locked) so `npx ruview` is reproducible. Re-minting on
a CLI/ADR change is a reviewed PR, not an implicit regeneration.
### Distribution: `npx ruview`
A small published package whose `bin` boots the pinned harness via the kernel:
- **Preferred name:** `ruview` (currently **free** on npm — verified 2026-06-17).
- **Risk:** npm's typosquat filter may reject `ruview` as too close to `review` /
`preview` (this session hit exactly that on `ruvn``levn`/`raven` and
`worldgraph``world-graph`). **Fallback:** publish scoped `@ruvnet/ruview` (also
free) and/or `npx ruvnet/ruview` straight from GitHub. Decide at publish time;
do not unpublish to rename (the 24-h name-lock lesson from `worldgraphs`).
- `bin: { "ruview": "bin/cli.js" }` — note **`bin/cli.js`, not `./bin/cli.js`** (npm
strips the `./` form; this broke `ruvn@0.1.0` this session).
- `npx ruview` with no args → `onboard` skill (interactive path picker).
`npx ruview <skill> [...]` → run a specific skill. `npx ruview --host codex`
install the harness into an existing repo for that host.
## Architecture
```
npx ruview (thin bin — boots the pinned harness)
@metaharness/kernel (wasm primary · NAPI-RS native fallback)
├── host adapter ── claude-code | codex | opencode | copilot | pi-dev | hermes | rvm | github-actions
├── @metaharness/router (k-NN cost-optimal model routing — DRACO P2 / ADR-026)
└── RuView genome (pinned)
├── skills onboard · provision-node · calibrate-room · train-pose · verify
├── mcp tools ruview.node.* · ruview.calibrate · ruview.room.watch · ruview.verify · ruview.claim.check
└── guardrails MEASURED-vs-CLAIMED · mean-pose baseline · no-unvalidated-firmware-claims
RuView assets (the real system the agent drives)
├── wifi-densepose CLI calibrate / enroll / train-room / room-watch
├── sensing-server :3000 / :5005 / :8765
├── ESP-IDF subprocess build / flash / provision / monitor (COM8/COM9/COM12)
└── witness bundle + verify.py
```
Provenance: the harness ships an **ed25519 witness + SBOM (SPDX) + SLSA** chain
(MetaHarness already does this for minted harnesses), so a recipient can verify the
RuView harness was built from a specific monorepo commit — the agentic analogue of
the firmware witness bundle (ADR-028).
## Phases
- **P1 — Mint & pin (claude-code).** `npx metaharness ruview --template
vertical:ruview --from-existing . --host claude-code`. Port the three `ruview-*`
subagents into host-neutral genome skills. Commit under `harness/ruview/`, pin
versions. Acceptance: `npx metaharness score .` ≥ threshold; the harness can run
`onboard` and `verify` end-to-end locally.
- **P2 — MCP tool surface.** Implement the `ruview.*` MCP tools over the kernel
(start with `onboard`, `verify`, `claim.check`, `node.monitor` — the read-only /
proving tools), then the mutating ones (`node.flash`, `provision`, `calibrate`).
Acceptance: `ruview.verify` returns the witness bundle PASS as structured JSON;
`ruview.claim.check` flags a seeded untagged "100% accuracy" string.
- **P3 — Publish `npx ruview` + multi-host.** Publish the bin package (name decision
per Distribution). Add codex / opencode / copilot / pi-dev / hermes / rvm /
github-actions adapters. Acceptance: `npx ruview` cold-starts on ≥3 hosts and runs
`onboard`; provenance verifies.
- **P4 — Router + guardrail hardening.** Wire `@metaharness/router`; calibrate the
3-tier routing on a RuView task set. Make the MEASURED-vs-CLAIMED guardrail a hard
pre-output gate. Acceptance: a benchmark of RuView tasks shows cost reduction vs
all-Opus with no quality regression; the guardrail blocks an untagged accuracy
claim in a red-team prompt.
## Consequences
**Positive**
- One reproducible, signed entry point (`npx ruview`) that operates RuView on the
host the user already has — onboarding goes from "clone a 15-crate monorepo" to a
single `npx`.
- The "prove everything" ethos becomes **executable**, not just documentation: the
harness *enforces* MEASURED-vs-CLAIMED and the mean-pose baseline.
- Knowledge written once (host-neutral genome) instead of 9× per host; regenerable
from the repo as the system evolves.
- Dogfoods MetaHarness on a hard real vertical, surfacing bugs back to
`agent-harness-generator` (this session already filed #9#13 there).
**Negative / risks**
- **Drift:** a pinned harness goes stale as the CLI/ADRs move; mitigated by a
re-mint-on-change PR ritual and a CI check that the genome's referenced
CLI flags still exist.
- **Surface area:** mutating MCP tools (`node.flash`, `provision`) touch hardware and
the network — must be permission-gated and fail-closed; the firmware-flash tool
must never claim hardware validation without a captured boot log.
- **Name/typosquat:** `ruview` may be rejected at publish; scoped fallback decided in
P3. Do not unpublish-to-rename.
- **Host parity:** not all 9 hosts support MCP + hooks equally; the guardrail gate
may degrade to advisory on weaker hosts — must be disclosed in the badge, not
hidden (same honesty principle as ADR-181's backend badge).
- **Windows-coupled tooling:** the ESP-IDF flow is Windows-subprocess-specific
today; the `node.*` tools are gated to that environment until a cross-platform
path exists.
## Alternatives considered
1. **Keep the `ruview-*` subagents repo-local (status quo).** Zero new surface, but
stays Claude-Code-only and clone-gated; no portable front door. Rejected — it's
the gap this ADR exists to close.
2. **Hand-write a bespoke `npx ruview` harness (no MetaHarness).** Full control, but
re-implements the kernel, 9 host adapters, the router, and the provenance chain
we already ship — months of duplicated work and 9 divergent configs to maintain.
Rejected.
3. **Use the generic `vertical:ruview` template as-is.** It's scaffolding with no
real tools or guardrails — it would *talk about* RuView without being able to
*operate* it or enforce honesty. Rejected as insufficient; P2 is precisely the
hardening that makes it real.
4. **Ship only an MCP server (no harness/host adapters).** Covers tools but not the
skills, routing, guardrails, or multi-host projection — a strictly smaller subset
of this design. Folded in as the P2 layer rather than the whole.
## Open questions
- Final published name: bare `ruview` vs scoped `@ruvnet/ruview` vs GitHub-only
`npx ruvnet/ruview` — resolve against the typosquat filter at P3.
- Does the harness bundle the `wifi-densepose` binary, shell out to a user-installed
one, or offer both? (Leaning: shell out; print install guidance if absent.)
- Where do the `node.*` hardware tools live for non-Windows users — defer, or wrap
the rvCSI runtime (ADR-095/096) which is cross-platform Rust?
- Should `ruview.verify` gate `npx ruview` self-tests in CI (harness can't publish if
the witness bundle regresses)?
- Relationship to the RuField MFS harness surface (ADR-260/262) — one harness with a
RuField skill, or a sibling `npx rufield`?
## References
- MetaHarness: `metaharness@0.1.15` (`npx metaharness`, templates incl.
`vertical:ruview`; hosts: claude-code/codex/pi-dev/hermes/openclaw/rvm/copilot/
opencode/github-actions), `@metaharness/kernel`, `@metaharness/router`,
`@metaharness/host-*`, repo `github.com/ruvnet/agent-harness-generator`.
- RuView subagents: `ruview-onboarding-guide`, `ruview-config-engineer`,
`ruview-training-engineer` (`.claude/agents/`).
- ADR-026 (3-tier model routing), ADR-028 (witness verification), ADR-041
(MetaHarness scorecards), ADR-060 (channel / MAC-filter overrides), ADR-079
(camera ground-truth training), ADR-095/096 (rvCSI runtime), ADR-151 (per-room
calibration), ADR-152/181 (WiFlow / browser pose), ADR-260/262 (RuField bridge).
@@ -0,0 +1,98 @@
# ADR-183: Onboard LED as a 40 Hz Gamma Stimulus, Colour-Mapped from Live CSI via `ruv-neural-viz`
| Field | Value |
|-------|-------|
| **Status** | Accepted — implemented & hardware-confirmed on ESP32-S3 N16R8 (COM8) |
| **Date** | 2026-06-17 |
| **Deciders** | ruv |
| **Codename** | **GAMMA-VIZ** |
| **Builds on** | `ruv-neural-viz::ColorMap` (now `no_std` — ruvnet/ruv-neural#3 / RuView#1126), the ESP32 edge `motion_energy` metric (`edge_processing.c`), PR #962 (WS2812 on GPIO 48) |
## Context
Two threads converged. (1) `ruv-neural-viz::ColorMap` — the viridis/cool-warm
palette the rUv-Neural stack uses to render brain-topology graphs — was `std`-only,
so it couldn't run on the ESP32. (2) The onboard WS2812 on the S3 CSI node was dead
weight: the firmware only cleared it on boot (and on the wrong pin for N16R8 — GPIO
38 vs the actual 48, see #962).
The ask: make the LED do something real and honest, using the project's own visual
capability — not a decorative blink. The natural fit is a **40 Hz gamma stimulus**
(the GENUS gamma-entrainment frequency from Alzheimer's light-therapy research)
whose **colour is driven by live sensed motion**, so the node's front panel is both
a known bio-stimulus waveform and a truthful readout of what the CSI is detecting.
## Decision
### Part A — make `ColorMap` `no_std`
`colormap.rs` is self-contained (no cross-crate deps), so expose it on `no_std`
targets. The only blockers were two `std`-only `f64` ops:
- `f64::round` / `f64::abs` → replaced with `core`+`alloc`-safe helpers `fround`
(round via `f64 as i64` truncation — a `core` cast, no `libm`) and `fabs`.
- `Vec`/`String`/`format!` → from `alloc`.
The graph-bound modules (`animation`/`ascii`/`export`/`layout`) and their heavy deps
move behind a default `std` feature; `--no-default-features` builds the crate `no_std`
and exposes only `colormap`. Output is **byte-identical** (8/8 colormap tests pass with
the same RGB values), so this is a pure portability change.
### Part B — the LED stimulus (firmware)
`firmware/esp32-csi-node/main/main.c`, on boot:
- WS2812 on **GPIO 48** (N16R8 / DevKitC-1 v1.1; GPIO 8 on C6).
- An `esp_timer` periodic at **12 500 µs toggles a square wave → 40 Hz, 50 % duty**
(full-on / full-off — a *perceptible* gamma flicker, not a colour drift).
- **ON-phase colour = live CSI motion.** Each ON phase reads `edge_get_vitals().motion_energy`,
normalises it (`/ LED_MOTION_FULLSCALE`, clamped `[0,1]`), and indexes a **60-step
viridis LUT generated from `ColorMap::viridis().map()`** — still = dark purple,
strong motion = yellow.
The LUT is baked from the real crate (Part A makes the same `ColorMap` embeddable
for a future direct FFI path once the ESP Rust toolchain is in CI). The colours are
therefore provably `ruv-neural-viz`'s, and the motion is provably real.
## Honesty (what it is and is not)
- **40 Hz is a real square-wave stimulus** (12.5 ms on / 12.5 ms off), not a label on
a colour sweep. It is *not* tied to any measured 40 Hz brain rhythm — it is an
*output* stimulus at the gamma frequency, not a readout of neural gamma.
- **Colour is a real CSI readout** — `motion_energy` is the on-device phase-variance
motion metric the node already computes; no fabrication. At rest the LED sits at the
purple (low) end and flickers there.
- No therapeutic claim is made. 40 Hz GENUS entrainment is cited as the *origin of the
frequency choice*, not as a validated medical effect of this device.
## Consequences
**Positive**
- The LED is now an honest front-panel: gamma-frequency flicker + a live motion readout.
- `ColorMap` is embeddable (`no_std`), unblocking on-device use of the rUv-Neural
palette beyond this LED.
- Confirms #962's GPIO-48 fix visually (the LED lights on N16R8).
**Negative / risks**
- Changes the *default* firmware behaviour: the onboard LED animates instead of staying
off. Now **gated by `CONFIG_LED_GAMMA_VIZ`** (default `y`); set it `n` for a dark,
lower-power boot (the LED is just cleared) — no source change needed.
- A 40 Hz flicker can be an issue for photosensitive users; document on the enclosure
and disable `CONFIG_LED_GAMMA_VIZ` in those deployments.
- The saturation point is now `CONFIG_LED_MOTION_FULLSCALE_MILLI` (default 250 = 0.25),
operator-tunable; still not auto-calibrated per-environment.
- The colour uses a baked LUT, not the live Rust `ColorMap` (FFI path deferred — needs
the ESP Rust/xtensa toolchain, not yet in CI).
## Validation
- `ruv-neural-viz`: `cargo build` (std) ✓, `cargo test colormap` 8/8 ✓ (identical RGB),
`cargo build --no-default-features` compiles `no_std` ✓.
- Firmware: built (1.13 MB), flashed to ESP32-S3 N16R8 (COM8). Boot log:
`Onboard WS2812: 40 Hz gamma flicker (GENUS), colour=CSI motion via ruv-neural-viz, GPIO 48`;
CSI continues (2738 pps), `motion=0.00` at rest → purple flicker as designed.
- Full on-device (xtensa) Rust build of `ColorMap` not run — ESP Rust toolchain absent.
## References
- ruvnet/ruv-neural#3 (ColorMap no_std), RuView#1126 (submodule bump), #962 (GPIO 48).
- Singer/Tsai GENUS 40 Hz gamma entrainment (origin of the frequency, not a device claim).
@@ -468,3 +468,29 @@ menu "Mock CSI (QEMU Testing)"
depends on CSI_MOCK_ENABLED
default n
endmenu
menu "Onboard LED (ADR-183)"
config LED_GAMMA_VIZ
bool "Onboard WS2812: 40 Hz gamma flicker + CSI-motion colour"
default y
help
Drive the onboard WS2812 as a GENUS-style 40 Hz gamma square wave
(12.5 ms on / 12.5 ms off, 50% duty). The ON-phase colour is live
CSI motion (edge motion_energy) mapped through the ruv-neural-viz
viridis colormap (still=purple, moving=yellow).
Disable to leave the LED off at boot — lower power, no flicker.
NOTE: a 40 Hz flicker can affect photosensitive users; disable or
shield the LED in those environments. Not a medical device.
config LED_MOTION_FULLSCALE_MILLI
int "Motion value (x1000) that saturates the colormap to yellow"
depends on LED_GAMMA_VIZ
default 250
range 1 100000
help
edge motion_energy that maps to the top (yellow) of the viridis
colormap, in milli-units (250 = 0.25). Lower = more sensitive
(reaches yellow with less motion).
endmenu
+71 -5
View File
@@ -144,6 +144,54 @@ static void wifi_init_sta(void)
}
}
#if CONFIG_LED_GAMMA_VIZ
/* Viridis colormap (60 steps), generated from ruv-neural-viz::ColorMap::viridis()
* — the rUv-Neural brain-topology colormap, now no_std (ruvnet/ruv-neural#3 /
* RuView#1126). Used as the ON-phase colour of the 40 Hz gamma flicker below:
* dark-purple (still) -> teal -> green -> yellow (strong motion). */
static const uint8_t VIRIDIS_LUT[60][3] = {
{ 68, 1, 84},{ 67, 6, 88},{ 67, 12, 91},{ 66, 17, 95},{ 66, 23, 99},
{ 65, 28,103},{ 64, 34,106},{ 64, 39,110},{ 63, 45,114},{ 63, 50,118},
{ 62, 56,121},{ 61, 61,125},{ 61, 67,129},{ 60, 72,132},{ 59, 78,136},
{ 59, 83,139},{ 57, 87,139},{ 55, 92,139},{ 53, 96,139},{ 52,100,139},
{ 50,104,139},{ 48,109,139},{ 46,113,139},{ 44,117,140},{ 43,122,140},
{ 41,126,140},{ 39,130,140},{ 37,134,140},{ 36,139,140},{ 34,143,140},
{ 35,147,139},{ 39,151,136},{ 43,154,133},{ 47,158,130},{ 52,162,127},
{ 56,166,124},{ 60,170,121},{ 64,173,119},{ 68,177,116},{ 72,181,113},
{ 76,185,110},{ 81,189,107},{ 85,192,104},{ 89,196,102},{ 93,200, 99},
{102,203, 95},{113,205, 91},{124,207, 87},{134,209, 82},{145,211, 78},
{156,213, 74},{167,215, 70},{178,217, 66},{188,219, 62},{199,221, 58},
{210,223, 54},{221,225, 49},{231,227, 45},{242,229, 41},{253,231, 37},
};
static led_strip_handle_t s_viz_led;
/* motion_energy that saturates the colormap to yellow (CONFIG, milli-units). */
#define LED_MOTION_FULLSCALE ((float)CONFIG_LED_MOTION_FULLSCALE_MILLI / 1000.0f)
/* GENUS-style 40 Hz gamma flicker: full on/off square wave, 50% duty (toggled
* every 12.5 ms → 40 Hz). The ON colour is live CSI motion (edge motion_energy)
* mapped through the ruv-neural-viz viridis LUT — still=purple, moving=yellow.
* So the LED is a real 40 Hz gamma stimulus whose hue tracks sensed motion. */
static void led_gamma_40hz_cb(void *arg)
{
static bool on = false;
on = !on;
if (on) {
edge_vitals_pkt_t v;
float m = edge_get_vitals(&v) ? v.motion_energy : 0.0f;
float norm = m / LED_MOTION_FULLSCALE;
if (norm < 0.0f) norm = 0.0f;
if (norm > 1.0f) norm = 1.0f;
int idx = (int)(norm * 59.0f + 0.5f);
const uint8_t *c = VIRIDIS_LUT[idx];
led_strip_set_pixel(s_viz_led, 0, c[0], c[1], c[2]); /* R,G,B (driver maps to GRB) */
} else {
led_strip_set_pixel(s_viz_led, 0, 0, 0, 0); /* off phase */
}
led_strip_refresh(s_viz_led);
}
#endif /* CONFIG_LED_GAMMA_VIZ */
void app_main(void)
{
/* Initialize NVS */
@@ -173,15 +221,16 @@ void app_main(void)
ESP_LOGI(TAG, "%s CSI Node (ADR-018 / ADR-110) — v%s — Node ID: %d",
target_name, app_desc->version, g_nvs_config.node_id);
/* Turn off onboard WS2812 LED.
* S3 dev boards put the LED on GPIO 38; C6 dev boards on GPIO 8.
* On C6, GPIO 38 doesn't exist (only 0-30) — gate the init by target. */
/* Onboard WS2812. C6 wires the LED to GPIO 8; S3 to GPIO 38 (DevKitC-1 v1.0)
* or GPIO 48 (DevKitC-1 v1.1 / N16R8 — see #962). On S3 we drive 48 (the
* common module). On C6, GPIO 38/48 don't exist (only 0-30) — gate by target.
* Behaviour is set by CONFIG_LED_GAMMA_VIZ (ADR-183): on = 40 Hz gamma flicker
* coloured by CSI motion; off = clear the LED at boot. */
#if defined(CONFIG_IDF_TARGET_ESP32C6)
const int led_gpio = 8;
#else
const int led_gpio = 38;
const int led_gpio = 48;
#endif
led_strip_handle_t led_strip;
led_strip_config_t strip_config = {
.strip_gpio_num = led_gpio,
.max_leds = 1,
@@ -193,9 +242,26 @@ void app_main(void)
.resolution_hz = 10 * 1000 * 1000, // 10MHz
.flags.with_dma = false,
};
#if CONFIG_LED_GAMMA_VIZ
if (led_strip_new_rmt_device(&strip_config, &rmt_config, &s_viz_led) == ESP_OK) {
const esp_timer_create_args_t viz_args = {
.callback = &led_gamma_40hz_cb,
.name = "led_gamma_40hz",
};
esp_timer_handle_t viz_timer;
if (esp_timer_create(&viz_args, &viz_timer) == ESP_OK) {
esp_timer_start_periodic(viz_timer, 12500); // 12.5 ms toggle → 40 Hz square wave
ESP_LOGI(TAG, "Onboard WS2812: 40 Hz gamma flicker (GENUS), colour=CSI motion via ruv-neural-viz, GPIO %d", led_gpio);
}
}
#else
/* Viz disabled — clear the onboard LED at boot and release the RMT channel. */
led_strip_handle_t led_strip;
if (led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip) == ESP_OK) {
led_strip_clear(led_strip);
led_strip_del(led_strip);
}
#endif /* CONFIG_LED_GAMMA_VIZ */
/* ADR-110 P4: 802.15.4 mesh time-sync (C6 only).
* Initialized BEFORE WiFi so it's available even when WiFi STA can't
+18
View File
@@ -0,0 +1,18 @@
{
"permissions": {
"allow": [
"Bash(npx ruview*)",
"mcp__ruview__*"
],
"deny": [
"Read(./.env)",
"Read(./.env.*)"
]
},
"mcpServers": {
"ruview": {
"command": "npx",
"args": ["-y", "@ruvnet/ruview", "mcp", "start"]
}
}
}
@@ -0,0 +1,29 @@
---
name: calibrate-room
description: Run the ADR-151 per-room calibration pipeline — baseline → enroll → extract → train → a bank of small specialists (presence/posture/breathing/heartbeat/restlessness/anomaly).
---
# calibrate-room
Turn a provisioned node + sensing-server into a working room model. Pure-Rust,
edge-deployable (ADR-151). Use the `ruview.calibrate` tool (installed
`wifi-densepose` binary, else `cargo run -p wifi-densepose-cli`).
## Sequence
1. **baseline** — capture the empty room (Welford amplitude + von Mises phase). Leave
the room empty.
`ruview.calibrate {step: "baseline"}`
2. **enroll** — record the occupant(s) doing the target activities.
`ruview.calibrate {step: "enroll"}`
3. **train-room** — train the bank of small specialists from baseline + enrollment.
`ruview.calibrate {step: "train-room"}`
4. **room-watch** — live presence/posture/breathing from the trained room.
`ruview.calibrate {step: "room-watch"}` (or the `room-watch` skill)
## Honesty
The specialists are calibrated to *this* room; cross-room transfer is a separate
problem (LoRA recalibration, ADR-079 P9). Report which room a number came from, and
tag presence/vitals accuracy MEASURED only with a held-out check — run
`ruview.claim_check` on the writeup.
@@ -0,0 +1,30 @@
---
name: onboard
description: Zero-to-sensing path picker for RuView (WiFi-DensePose) — pick docker-demo, repo-build, or live-esp32 and run the next concrete step.
---
# onboard
Get a newcomer from nothing to a working RuView setup. **First fact to set:** WiFi
sensing infers *coarse* pose/presence/breathing from Channel State Information — it
is **not a camera**, and any accuracy number must be MEASURED against a baseline
(use the `verify` skill / `ruview.claim_check` tool). Never present WiFi output as
camera-grade.
## Pick a path
Run `ruview.onboard {path}` or decide from:
1. **docker-demo** — fastest, no hardware. Replays sample CSI into the dashboard.
`docker run -p 8000:8000 ruvnet/wifi-densepose` → open `http://localhost:8000`.
Use to see what it looks like.
2. **repo-build** — for developers. `cd v2 && cargo test --workspace --no-default-features`
(1,031+ tests pass), then `cargo run -p wifi-densepose-cli -- --help`.
3. **live-esp32** — a real install. Flash a node (`provision-node` skill), point it at
the sensing-server, then `calibrate-room`. This is the only path that senses a real room.
## Then
- Live sensing → go to **provision-node**, then **calibrate-room**.
- Evaluating a model/claim → go to **verify** and run `ruview.claim_check` on any
report before you quote a number.
@@ -0,0 +1,49 @@
---
name: provision-node
description: Build, flash, and provision an ESP32-S3/C6 CSI node for RuView — firmware variant choice, ESP-IDF Windows-subprocess flow, NVS/WiFi/channel/MAC-filter overrides.
---
# provision-node
Bring an ESP32 sensing node online.
## 1. Pick a firmware variant
- **s3-8mb** (display build) — ESP32-S3 N16R8 / 16MB; AMOLED optional. The display-detect
fix (#1000) means a *bare* board still captures CSI (MGMT+DATA).
- **s3-4mb** (no-display) — ESP32-S3 4MB; dual-OTA, display disabled.
- **c6** — ESP32-C6 + Seeed MR60BHA2 (60 GHz mmWave + WiFi CSI). The mmwave probe
requires a validated MR60 header (#1107) so an empty UART never false-detects.
Prebuilt binaries: GitHub release `v0.8.1-esp32` (hardware-validated on S3 QFN56 rev v0.2).
## 2. Flash
ESP-IDF v5.4 on Windows is **subprocess-only** (Git Bash/MSYS is unsupported — strip
`MSYSTEM*` env vars). Offsets for the S3 image:
```
esptool --chip esp32s3 -p <PORT> -b 460800 write_flash \
0x0 bootloader.bin 0x8000 partition-table.bin \
0xf000 ota_data_initial.bin 0x20000 esp32-csi-node-s3-8mb.bin
```
(`ruview.node_flash` returns the exact pinned command rather than running an
unattended flash.)
## 3. Provision
```
python firmware/esp32-csi-node/provision.py --port <PORT> \
--ssid "<SSID>" --password "<secret>" --target-ip <server-ip> --target-port 5005
# optional ADR-060 overrides:
python firmware/esp32-csi-node/provision.py --port <PORT> --channel 6 --filter-mac AA:BB:CC:DD:EE:FF
```
Never echo or commit the WiFi password.
## 4. Confirm CSI is flowing
`ruview.node_monitor {port}` — PASS criteria: serial shows `CSI cb #...` callbacks and
(on a bare board) `CSI filter upgraded to MGMT+DATA`. No callbacks → the node isn't
capturing; do not proceed to calibration.
@@ -0,0 +1,33 @@
---
name: train-pose
description: Train/evaluate WiFi pose models honestly — camera-supervised (MediaPipe + CSI) and camera-free (WiFlow), always checked against the mean-pose baseline before any PCK is quoted.
---
# train-pose
Build a CSI→pose model without overstating it. The project has a **retracted 92.9%/100%**
history — the discipline below exists so it never recurs.
## The non-negotiable: mean-pose baseline first
A pose model that always predicts the dataset's *mean pose* already scores ~50% PCK.
**Quote PCK only as a delta over that baseline**, on a held-out split with no subject
or temporal leakage. Example honest result (ADR-181):
> Held-out PCK@20 **59.5%** vs a 50% mean-pose baseline = **+9.4 pp real signal** — MEASURED.
## Paths
- **camera-supervised** (ADR-079) — MediaPipe Pose labels the camera frame; paired CSI
trains the net. Train/infer in one camera frame so the skeleton aligns.
- **camera-free** (WiFlow, ADR-152) — no camera at inference; geometry-conditioned.
- **in-browser** (ADR-181) — WebGPU/WASM trainer; the active backend is shown as a badge
(honest about what's executing).
## Before you publish a number
1. Run the mean-pose baseline on the same split.
2. Report `(model baseline)` in pp, with the split definition (chronological /
blocked-gap / grouped-bucket; no leakage).
3. `ruview.claim_check` the writeup — it flags any untagged or 100%/perfect claim.
4. If it's a benchmark vs SOTA, tag MEASURED-EQUIVALENT only with the reproducer.
@@ -0,0 +1,42 @@
---
name: verify
description: Prove a RuView result is real — run the deterministic SHA-256 proof and the witness bundle (ADR-028), and lint any claim for MEASURED-vs-CLAIMED honesty.
---
# verify
The "prove everything" skill. Nothing ships as validated without this.
## Deterministic proof (Trust Kill Switch)
`ruview.verify` runs `archive/v1/data/proof/verify.py`: it feeds a reference signal
through the production pipeline and hashes the output against
`expected_features.sha256`. Must print **VERDICT: PASS**. If numpy/scipy changed the
hash, regenerate with `verify.py --generate-hash` then re-verify.
## Witness bundle (ADR-028)
For a release-grade attestation:
```
bash scripts/generate-witness-bundle.sh
cd dist/witness-bundle-ADR028-*/ && bash VERIFY.sh # must be 7/7 PASS
```
Contains the Rust test log, the proof + expected hash, firmware SHA-256 manifest, and
crate versions — a recipient can re-verify with one command.
## Claim honesty
Run `ruview.claim_check {text}` on any report, README section, PR body, or model card
before quoting accuracy. It flags:
- untagged accuracy numbers (must be MEASURED / CLAIMED / SYNTHETIC),
- MEASURED claims with no reproducer cited,
- the retracted "100%/perfect accuracy" framing.
## Firmware-specific
A firmware fix is **not** "hardware-validated" without a captured boot log on real
silicon (e.g. the `v0.8.1-esp32` rev-v0.2 validation: `running headless so CSI
captures (#1000)` + `CSI filter upgraded to MGMT+DATA` + a no-false-detect mmwave
probe). Do not merge or release on a build-passes signal alone.
+39
View File
@@ -0,0 +1,39 @@
{
"schema": 1,
"generator": "metaharness 0.1.15 + ADR-182 hardening",
"template": "vertical:ruview",
"name": "@ruvnet/ruview",
"vars": {
"name": "@ruvnet/ruview",
"description": "RuView WiFi-sensing operator agent harness",
"host": "claude-code"
},
"hosts": [
"claude-code"
],
"files": {
".claude/settings.json": "b0ea971383716f18b89db73010b8f0ea0f1b16bdec4cd1068245772ba1c27bdd",
".claude/skills/calibrate-room/SKILL.md": "6a6c8211a7109feb76620c618963c10ad9a9f633ffce7676e631a80a1181986d",
".claude/skills/onboard/SKILL.md": "22323732fe746b38b77a7c8c052e952dff2fe87ae939ba125379125827385f21",
".claude/skills/provision-node/SKILL.md": "5ffe5a75873e873b80758d9c81005774d4191317227f2e9aa4345cbce3f29751",
".claude/skills/train-pose/SKILL.md": "b3ee95bfb0b678eb3d101138b9ea0e7cab3db3a9906d19c4059f9cca0598e87b",
".claude/skills/verify/SKILL.md": "c0314d5ead465d9089b6a4917fd125051a5be20dc07ba92d5b601fcaada32e19",
"CLAUDE.md": "7ecdb2b9d9abcf4aa22dd3ce553b60216a135e147893a59fa944fc1a8c81f5ef",
"LICENSE": "631f94984f626818d42ecf717aa6e8e0afd4f9f355ca706bd2effafbd1416d06",
"README.md": "b77d30428de8efb6758f2ca3eb22e84849013b2c0e6c601d488d2ea5a6f0da44",
"bin/cli.js": "b0d74690cff4329dfe342271fc475eaa140b767bdb66b37cf4992ad209012fe8",
"package.json": "2af49561ef0d59cafc4b99885816e580635b2d2ad329dfe17c69b9df6f8afceb",
"skills/calibrate-room.md": "6a6c8211a7109feb76620c618963c10ad9a9f633ffce7676e631a80a1181986d",
"skills/onboard.md": "22323732fe746b38b77a7c8c052e952dff2fe87ae939ba125379125827385f21",
"skills/provision-node.md": "5ffe5a75873e873b80758d9c81005774d4191317227f2e9aa4345cbce3f29751",
"skills/train-pose.md": "b3ee95bfb0b678eb3d101138b9ea0e7cab3db3a9906d19c4059f9cca0598e87b",
"skills/verify.md": "c0314d5ead465d9089b6a4917fd125051a5be20dc07ba92d5b601fcaada32e19",
"src/guardrails.js": "1631cea02c4354fe6126c576300faf5f8b68ae2f5e2e3a658c99eb25a7403e55",
"src/mcp-server.js": "e51379f5ebb0b7b4670c7412714e559931ef1be8df20551f8f7309b53f0fb7af",
"src/tools.js": "b558f61bb202abf5a967ce3a6ccaea351f2d186238cf49c7fc151d1de028eee8"
},
"meta": {
"surface": "cli+mcp",
"adr": "ADR-182"
}
}
+1
View File
@@ -0,0 +1 @@
6c6c1431c37472494c9b309c8b5d761dd4fc41e30313baead6320831fb982e57 manifest.json
+34
View File
@@ -0,0 +1,34 @@
# RuView harness — agent operating notes
You are operating **RuView** (WiFi-DensePose), a camera-free WiFi-CSI sensing system.
## The one rule: prove everything
This project was accused of AI-slop; the fix is hard discipline. Before you quote ANY
accuracy number:
1. It must be tagged **MEASURED** (with a reproducer named), **CLAIMED**, or **SYNTHETIC**.
2. Pose PCK is quoted only as a **delta over the mean-pose baseline** on a leakage-free
held-out split. (A mean-pose predictor already scores ~50% PCK.)
3. Run `ruview.claim_check` on any report/PR/model-card. It flags untagged numbers and
the retracted "100%/perfect accuracy" framing.
4. Firmware is "hardware-validated" only with a captured **boot log on real silicon**
never on a build-passes signal.
## Tools
`ruview.onboard`, `ruview.claim_check`, `ruview.verify`, `ruview.node_monitor`,
`ruview.calibrate`, `ruview.node_flash`. All fail-closed. Mutating/hardware tools
(`node_flash`) require explicit confirmation and are Windows/ESP-IDF gated.
## Skills
`onboard` · `provision-node` · `calibrate-room` · `train-pose` · `verify`
(`npx @ruvnet/ruview skill <name>`).
## Don'ts
- Don't present WiFi sensing as camera-grade.
- Don't echo or commit WiFi passwords / secrets.
- Don't merge or release firmware without a real boot log.
- Don't report a PCK without its mean-pose baseline.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 ruvnet
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+60
View File
@@ -0,0 +1,60 @@
# `npx @ruvnet/ruview` — RuView WiFi-sensing operator harness
An AI agent harness that knows how to operate **RuView** (WiFi-DensePose): onboard a
newcomer, provision an ESP32 CSI node, calibrate a room, train pose models, and —
crucially — **refuse to overstate accuracy**. Minted from the RuView monorepo via
[`metaharness`](https://www.npmjs.com/package/metaharness) and hardened per **ADR-182**.
WiFi sensing infers *coarse* pose/presence/breathing from Channel State Information.
It is **not a camera**. Every accuracy number this harness emits must be MEASURED
against a baseline — that rule is enforced in code (`ruview.claim_check`).
## Quick start
```bash
npx @ruvnet/ruview # onboard — pick a setup path
npx @ruvnet/ruview claim-check --text "we hit 100% accuracy" # the honesty guardrail
npx @ruvnet/ruview verify # run the deterministic proof (VERDICT: PASS)
npx @ruvnet/ruview doctor # self-check (tools + optional kernel/host)
npx @ruvnet/ruview --help
```
The operator tools are pure Node and run with **zero install weight**. The
`@metaharness/kernel` + host adapter are `optionalDependencies` — only `doctor` /
`install` use them, only if present.
## Tools (`ruview.*`)
Exposed both as CLI verbs and as an MCP server (`npx @ruvnet/ruview mcp start`):
| Tool | What it does |
|------|--------------|
| `ruview.onboard` | Pick docker-demo / repo-build / live-esp32; print the next command |
| `ruview.claim_check` | Lint text for untagged / overstated accuracy claims (guardrail) |
| `ruview.verify` | Run `verify.py` deterministic proof → VERDICT |
| `ruview.node_monitor` | Assert CSI is flowing on an ESP32 (read-only) |
| `ruview.calibrate` | ADR-151 room pipeline (baseline→enroll→train-room→room-watch) |
| `ruview.node_flash` | Build+flash firmware (Windows/ESP-IDF; mutating, guarded) |
Every tool is **fail-closed**: missing repo / python / binary / port → an honest
negative, never a fabricated success.
## Skills
Host-neutral playbooks in `skills/` (`onboard`, `provision-node`, `calibrate-room`,
`train-pose`, `verify`). `npx @ruvnet/ruview skill <name>` prints one.
## Use as a Claude Code MCP server
The bundled `.claude/settings.json` registers the `ruview` MCP server
(`npx -y @ruvnet/ruview mcp start`). Drop this package's `.claude/` into a repo, or run
`npx @ruvnet/ruview install --host claude-code`.
## Hosts
claude-code (bundled), and via metaharness host adapters: codex, opencode, copilot,
pi-dev, hermes, rvm, github-actions.
## License
MIT © ruvnet
+181
View File
@@ -0,0 +1,181 @@
#!/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.
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');
const res = 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 = 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); });
}
+65
View File
@@ -0,0 +1,65 @@
{
"name": "@ruvnet/ruview",
"version": "0.1.0",
"description": "RuView WiFi-sensing operator agent harness — onboard, calibrate, train, and verify camera-free WiFi-CSI sensing, with the project's MEASURED-vs-CLAIMED honesty guardrail enforced. Minted via metaharness (ADR-182).",
"type": "module",
"bin": {
"ruview": "bin/cli.js"
},
"exports": {
".": "./src/tools.js",
"./guardrails": "./src/guardrails.js"
},
"files": [
"bin/",
"src/",
"skills/",
".claude/",
".harness/",
"CLAUDE.md",
"README.md",
"LICENSE"
],
"scripts": {
"test": "node --test test/*.test.mjs",
"doctor": "node ./bin/cli.js doctor",
"mcp": "node ./bin/cli.js mcp start"
},
"optionalDependencies": {
"@metaharness/kernel": "^0.1.0",
"@metaharness/host-claude-code": "^0.1.0"
},
"keywords": [
"wifi-sensing",
"wifi-densepose",
"ruview",
"csi",
"channel-state-information",
"pose-estimation",
"presence-detection",
"esp32",
"agent-harness",
"metaharness",
"mcp",
"mcp-server",
"claude-code",
"ambient-intelligence"
],
"engines": {
"node": ">=20.0.0"
},
"license": "MIT",
"author": "ruvnet",
"homepage": "https://github.com/ruvnet/RuView#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/ruvnet/RuView.git",
"directory": "harness/ruview"
},
"bugs": {
"url": "https://github.com/ruvnet/RuView/issues"
},
"publishConfig": {
"access": "public"
}
}
+29
View File
@@ -0,0 +1,29 @@
---
name: calibrate-room
description: Run the ADR-151 per-room calibration pipeline — baseline → enroll → extract → train → a bank of small specialists (presence/posture/breathing/heartbeat/restlessness/anomaly).
---
# calibrate-room
Turn a provisioned node + sensing-server into a working room model. Pure-Rust,
edge-deployable (ADR-151). Use the `ruview.calibrate` tool (installed
`wifi-densepose` binary, else `cargo run -p wifi-densepose-cli`).
## Sequence
1. **baseline** — capture the empty room (Welford amplitude + von Mises phase). Leave
the room empty.
`ruview.calibrate {step: "baseline"}`
2. **enroll** — record the occupant(s) doing the target activities.
`ruview.calibrate {step: "enroll"}`
3. **train-room** — train the bank of small specialists from baseline + enrollment.
`ruview.calibrate {step: "train-room"}`
4. **room-watch** — live presence/posture/breathing from the trained room.
`ruview.calibrate {step: "room-watch"}` (or the `room-watch` skill)
## Honesty
The specialists are calibrated to *this* room; cross-room transfer is a separate
problem (LoRA recalibration, ADR-079 P9). Report which room a number came from, and
tag presence/vitals accuracy MEASURED only with a held-out check — run
`ruview.claim_check` on the writeup.
+30
View File
@@ -0,0 +1,30 @@
---
name: onboard
description: Zero-to-sensing path picker for RuView (WiFi-DensePose) — pick docker-demo, repo-build, or live-esp32 and run the next concrete step.
---
# onboard
Get a newcomer from nothing to a working RuView setup. **First fact to set:** WiFi
sensing infers *coarse* pose/presence/breathing from Channel State Information — it
is **not a camera**, and any accuracy number must be MEASURED against a baseline
(use the `verify` skill / `ruview.claim_check` tool). Never present WiFi output as
camera-grade.
## Pick a path
Run `ruview.onboard {path}` or decide from:
1. **docker-demo** — fastest, no hardware. Replays sample CSI into the dashboard.
`docker run -p 8000:8000 ruvnet/wifi-densepose` → open `http://localhost:8000`.
Use to see what it looks like.
2. **repo-build** — for developers. `cd v2 && cargo test --workspace --no-default-features`
(1,031+ tests pass), then `cargo run -p wifi-densepose-cli -- --help`.
3. **live-esp32** — a real install. Flash a node (`provision-node` skill), point it at
the sensing-server, then `calibrate-room`. This is the only path that senses a real room.
## Then
- Live sensing → go to **provision-node**, then **calibrate-room**.
- Evaluating a model/claim → go to **verify** and run `ruview.claim_check` on any
report before you quote a number.
+49
View File
@@ -0,0 +1,49 @@
---
name: provision-node
description: Build, flash, and provision an ESP32-S3/C6 CSI node for RuView — firmware variant choice, ESP-IDF Windows-subprocess flow, NVS/WiFi/channel/MAC-filter overrides.
---
# provision-node
Bring an ESP32 sensing node online.
## 1. Pick a firmware variant
- **s3-8mb** (display build) — ESP32-S3 N16R8 / 16MB; AMOLED optional. The display-detect
fix (#1000) means a *bare* board still captures CSI (MGMT+DATA).
- **s3-4mb** (no-display) — ESP32-S3 4MB; dual-OTA, display disabled.
- **c6** — ESP32-C6 + Seeed MR60BHA2 (60 GHz mmWave + WiFi CSI). The mmwave probe
requires a validated MR60 header (#1107) so an empty UART never false-detects.
Prebuilt binaries: GitHub release `v0.8.1-esp32` (hardware-validated on S3 QFN56 rev v0.2).
## 2. Flash
ESP-IDF v5.4 on Windows is **subprocess-only** (Git Bash/MSYS is unsupported — strip
`MSYSTEM*` env vars). Offsets for the S3 image:
```
esptool --chip esp32s3 -p <PORT> -b 460800 write_flash \
0x0 bootloader.bin 0x8000 partition-table.bin \
0xf000 ota_data_initial.bin 0x20000 esp32-csi-node-s3-8mb.bin
```
(`ruview.node_flash` returns the exact pinned command rather than running an
unattended flash.)
## 3. Provision
```
python firmware/esp32-csi-node/provision.py --port <PORT> \
--ssid "<SSID>" --password "<secret>" --target-ip <server-ip> --target-port 5005
# optional ADR-060 overrides:
python firmware/esp32-csi-node/provision.py --port <PORT> --channel 6 --filter-mac AA:BB:CC:DD:EE:FF
```
Never echo or commit the WiFi password.
## 4. Confirm CSI is flowing
`ruview.node_monitor {port}` — PASS criteria: serial shows `CSI cb #...` callbacks and
(on a bare board) `CSI filter upgraded to MGMT+DATA`. No callbacks → the node isn't
capturing; do not proceed to calibration.
+33
View File
@@ -0,0 +1,33 @@
---
name: train-pose
description: Train/evaluate WiFi pose models honestly — camera-supervised (MediaPipe + CSI) and camera-free (WiFlow), always checked against the mean-pose baseline before any PCK is quoted.
---
# train-pose
Build a CSI→pose model without overstating it. The project has a **retracted 92.9%/100%**
history — the discipline below exists so it never recurs.
## The non-negotiable: mean-pose baseline first
A pose model that always predicts the dataset's *mean pose* already scores ~50% PCK.
**Quote PCK only as a delta over that baseline**, on a held-out split with no subject
or temporal leakage. Example honest result (ADR-181):
> Held-out PCK@20 **59.5%** vs a 50% mean-pose baseline = **+9.4 pp real signal** — MEASURED.
## Paths
- **camera-supervised** (ADR-079) — MediaPipe Pose labels the camera frame; paired CSI
trains the net. Train/infer in one camera frame so the skeleton aligns.
- **camera-free** (WiFlow, ADR-152) — no camera at inference; geometry-conditioned.
- **in-browser** (ADR-181) — WebGPU/WASM trainer; the active backend is shown as a badge
(honest about what's executing).
## Before you publish a number
1. Run the mean-pose baseline on the same split.
2. Report `(model baseline)` in pp, with the split definition (chronological /
blocked-gap / grouped-bucket; no leakage).
3. `ruview.claim_check` the writeup — it flags any untagged or 100%/perfect claim.
4. If it's a benchmark vs SOTA, tag MEASURED-EQUIVALENT only with the reproducer.
+42
View File
@@ -0,0 +1,42 @@
---
name: verify
description: Prove a RuView result is real — run the deterministic SHA-256 proof and the witness bundle (ADR-028), and lint any claim for MEASURED-vs-CLAIMED honesty.
---
# verify
The "prove everything" skill. Nothing ships as validated without this.
## Deterministic proof (Trust Kill Switch)
`ruview.verify` runs `archive/v1/data/proof/verify.py`: it feeds a reference signal
through the production pipeline and hashes the output against
`expected_features.sha256`. Must print **VERDICT: PASS**. If numpy/scipy changed the
hash, regenerate with `verify.py --generate-hash` then re-verify.
## Witness bundle (ADR-028)
For a release-grade attestation:
```
bash scripts/generate-witness-bundle.sh
cd dist/witness-bundle-ADR028-*/ && bash VERIFY.sh # must be 7/7 PASS
```
Contains the Rust test log, the proof + expected hash, firmware SHA-256 manifest, and
crate versions — a recipient can re-verify with one command.
## Claim honesty
Run `ruview.claim_check {text}` on any report, README section, PR body, or model card
before quoting accuracy. It flags:
- untagged accuracy numbers (must be MEASURED / CLAIMED / SYNTHETIC),
- MEASURED claims with no reproducer cited,
- the retracted "100%/perfect accuracy" framing.
## Firmware-specific
A firmware fix is **not** "hardware-validated" without a captured boot log on real
silicon (e.g. the `v0.8.1-esp32` rev-v0.2 validation: `running headless so CSI
captures (#1000)` + `CSI filter upgraded to MGMT+DATA` + a no-false-detect mmwave
probe). Do not merge or release on a build-passes signal alone.
+106
View File
@@ -0,0 +1,106 @@
// SPDX-License-Identifier: MIT
// RuView harness guardrails — the "prove everything" rule made executable.
//
// The project was accused of AI-slop; the cultural fix is that every accuracy
// number must be tagged MEASURED (with a reproducer) or CLAIMED/SYNTHETIC, and
// the retracted "100% accuracy" framing must never reappear untagged. This module
// is the static enforcement of that, shared by the `ruview.claim_check` MCP tool,
// the `npx ruview claim-check` CLI, and the claude-code pre-output hook.
/** Phrases that signal a quantitative accuracy claim. */
const METRIC_TERMS = [
'accuracy', 'pck', 'pck@', 'f1', 'precision', 'recall', 'map', 'auc',
'iou', 'mpjpe', 'error rate', 'detection rate', 'true positive',
];
/** Tags that make a claim honest (case-insensitive). */
const HONEST_TAGS = ['measured', 'claimed', 'synthetic', 'unvalidated', 'baseline'];
/** Reproducer references that count as evidence backing a MEASURED claim. */
const REPRODUCER_HINTS = [
'verify.py', 'witness', 'mean-pose', 'mean pose', 'held-out', 'held out',
'baseline', 'reproduce', 'sha256', 'boot log', 'pck@20 vs', 'expected_features',
];
const PERCENT_RE = /\b(\d{1,3}(?:\.\d+)?)\s?%/g;
// "perfect" / "100%" framing is the specific retracted claim — always high severity.
// NOTE: no trailing \b after "%": "%"→" " is non-word→non-word, so a trailing \b
// never matches and would silently miss "100%". Bare 100% is only damning next to a
// metric term (see claimCheck); the word phrases are inherently accuracy claims.
const PERFECT_PCT_RE = /\b100(?:\.0+)?\s?%/;
const PERFECT_WORD_RE = /perfect accuracy|flawless|never (?:wrong|fails)/i;
/**
* Lint a block of text for untagged or overstated accuracy claims.
* @param {string} text
* @returns {{ok: boolean, findings: Array<{severity:'high'|'medium', line:number, excerpt:string, reason:string, suggestion:string}>}}
*/
export function claimCheck(text) {
const findings = [];
if (typeof text !== 'string' || text.length === 0) {
return { ok: true, findings };
}
const lines = text.split(/\r?\n/);
lines.forEach((raw, i) => {
const line = raw.trim();
if (!line) return;
const lower = line.toLowerCase();
const hasPercent = PERCENT_RE.test(line);
PERCENT_RE.lastIndex = 0; // reset stateful global regex
const mentionsMetric = METRIC_TERMS.some((t) => lower.includes(t));
if (!hasPercent && !mentionsMetric) return;
const tagged = HONEST_TAGS.some((t) => lower.includes(t));
const hasReproducer = REPRODUCER_HINTS.some((h) => lower.includes(h));
const perfect = PERFECT_WORD_RE.test(line) || (mentionsMetric && PERFECT_PCT_RE.test(line));
if (perfect && !lower.includes('retract')) {
findings.push({
severity: 'high',
line: i + 1,
excerpt: clip(line),
reason: 'States perfect/100% accuracy — this is the exact framing the project retracted.',
suggestion: 'Replace with a held-out number vs the mean-pose baseline, tagged MEASURED, or mark the old claim "retracted".',
});
return;
}
// A metric/percent with no honesty tag at all.
if (!tagged) {
findings.push({
severity: 'medium',
line: i + 1,
excerpt: clip(line),
reason: 'Accuracy claim is not tagged MEASURED / CLAIMED / SYNTHETIC.',
suggestion: 'Tag it. If MEASURED, name the reproducer (verify.py, witness bundle, held-out vs mean-pose).',
});
return;
}
// Tagged MEASURED but cites no reproducer — still a gap.
if (lower.includes('measured') && !hasReproducer) {
findings.push({
severity: 'medium',
line: i + 1,
excerpt: clip(line),
reason: 'Tagged MEASURED but cites no reproducer/evidence.',
suggestion: 'Add the evidence path: verify.py VERDICT, witness bundle, or held-out PCK vs the mean-pose baseline.',
});
}
});
return { ok: findings.length === 0, findings };
}
function clip(s, n = 120) {
return s.length > n ? `${s.slice(0, n - 1)}` : s;
}
/** Convenience: a one-line human summary for CLI output. */
export function summarize(result) {
if (result.ok) return 'claim-check: PASS — no untagged or overstated accuracy claims.';
const high = result.findings.filter((f) => f.severity === 'high').length;
return `claim-check: ${result.findings.length} finding(s) (${high} high) — accuracy claims need MEASURED/CLAIMED tags + a reproducer.`;
}
+68
View File
@@ -0,0 +1,68 @@
// SPDX-License-Identifier: MIT
// RuView harness — minimal MCP stdio server (JSON-RPC 2.0 over stdin/stdout).
//
// Dependency-free on purpose: a published `npx ruview` must `mcp start` without
// pulling the full MCP SDK. Implements the subset hosts use: `initialize`,
// `tools/list`, `tools/call`, and the `notifications/initialized` ack. Logs go to
// stderr ONLY — stdout is the JSON-RPC channel and must stay clean.
import { createInterface } from 'node:readline';
import { listTools, runTool } from './tools.js';
const PROTOCOL_VERSION = '2024-11-05';
const SERVER_INFO = { name: 'ruview', version: '0.1.0' };
function send(msg) {
process.stdout.write(JSON.stringify(msg) + '\n');
}
function result(id, res) { send({ jsonrpc: '2.0', id, result: res }); }
function error(id, code, message) { send({ jsonrpc: '2.0', id, error: { code, message } }); }
function log(...a) { process.stderr.write('[ruview-mcp] ' + a.join(' ') + '\n'); }
function handle(msg) {
const { id, method, params } = msg;
switch (method) {
case 'initialize':
return result(id, {
protocolVersion: PROTOCOL_VERSION,
capabilities: { tools: { listChanged: false } },
serverInfo: SERVER_INFO,
instructions: 'RuView WiFi-sensing operator tools. All results are fail-closed; accuracy claims must pass ruview.claim_check.',
});
case 'notifications/initialized':
case 'initialized':
return; // notification — no response
case 'ping':
return result(id, {});
case 'tools/list':
return result(id, { tools: listTools() });
case 'tools/call': {
const name = params?.name;
const args = params?.arguments || {};
const out = runTool(name, args);
// MCP content envelope: text block with the JSON, isError reflects ok=false.
return result(id, {
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
isError: out && out.ok === false,
});
}
default:
if (id !== undefined) error(id, -32601, `Method not found: ${method}`);
}
}
export function startMcpServer() {
log(`starting (protocol ${PROTOCOL_VERSION}, ${listTools().length} tools)`);
const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
rl.on('line', (line) => {
const s = line.trim();
if (!s) return;
let msg;
try { msg = JSON.parse(s); } catch { return log('bad JSON line dropped'); }
try { handle(msg); } catch (err) {
if (msg && msg.id !== undefined) error(msg.id, -32603, String(err && err.message || err));
log('handler error:', String(err));
}
});
rl.on('close', () => { log('stdin closed — exiting'); process.exit(0); });
}
+220
View File
@@ -0,0 +1,220 @@
// SPDX-License-Identifier: MIT
// RuView harness — the `ruview.*` tool registry.
//
// One registry consumed by BOTH the CLI (`npx ruview <tool>`) and the MCP server
// (`npx ruview mcp start`). Every handler returns structured JSON and is
// FAIL-CLOSED: when a prerequisite (the RuView repo, python+pyserial, the
// `wifi-densepose` binary, an ESP32 on a port) is absent, it returns an honest
// negative — never a fabricated success. This mirrors the project's "prove
// everything" rule and the RuField fail-closed posture (ADR-262 §3.3).
import { spawnSync } from 'node:child_process';
import { existsSync, readFileSync } from 'node:fs';
import { join, dirname, resolve } from 'node:path';
import { claimCheck, summarize } from './guardrails.js';
/** Walk up from `start` to find the RuView monorepo root (or null). */
export function findRepoRoot(start = process.cwd()) {
let dir = resolve(start);
for (let i = 0; i < 8; i++) {
const hasProof = existsSync(join(dir, 'archive', 'v1', 'data', 'proof', 'verify.py'));
const hasV2 = existsSync(join(dir, 'v2', 'Cargo.toml'));
if (hasProof || hasV2) return dir;
const parent = dirname(dir);
if (parent === dir) break;
dir = parent;
}
return null;
}
function which(cmd) {
const probe = process.platform === 'win32'
? spawnSync('where', [cmd], { encoding: 'utf8' })
: spawnSync('command', ['-v', cmd], { encoding: 'utf8', shell: true });
return probe.status === 0 ? (probe.stdout || '').trim().split(/\r?\n/)[0] : null;
}
function run(cmd, args, opts = {}) {
const r = spawnSync(cmd, args, { encoding: 'utf8', timeout: opts.timeout ?? 120000, ...opts });
return {
status: r.status,
ok: r.status === 0,
stdout: (r.stdout || '').slice(-8000),
stderr: (r.stderr || '').slice(-4000),
error: r.error ? r.error.message : null,
};
}
const ONBOARD_PATHS = {
'docker-demo': 'Fastest. `docker run -p 8000:8000 ruvnet/wifi-densepose` → open the dashboard. No hardware; replays sample CSI. Good for "what does it look like".',
'repo-build': 'Build from source. `cd v2 && cargo test --workspace --no-default-features` (1,031+ tests). Then `cargo run -p wifi-densepose-cli -- --help`. Good for developers.',
'live-esp32': 'Real sensing. Flash an ESP32-S3 (see `provision-node` skill), point it at the sensing-server, then `calibrate → enroll → train-room → room-watch` (see `calibrate-room`). Good for an actual install.',
};
/**
* The tool registry. Each entry: { title, description, inputSchema, handler }.
* inputSchema is JSON-Schema (object). handler(args) → JSON-serializable result.
*/
export const TOOLS = {
'ruview.onboard': {
title: 'Onboard',
description: 'Pick a RuView setup path (docker-demo | repo-build | live-esp32) and print the next concrete command.',
inputSchema: {
type: 'object',
properties: { path: { type: 'string', enum: Object.keys(ONBOARD_PATHS), description: 'Which setup path. Omit to list all.' } },
},
handler(args = {}) {
const repo = findRepoRoot();
if (args.path && ONBOARD_PATHS[args.path]) {
return { ok: true, path: args.path, next: ONBOARD_PATHS[args.path], in_ruview_repo: !!repo };
}
return {
ok: true,
in_ruview_repo: !!repo,
repo_root: repo,
paths: ONBOARD_PATHS,
recommend: repo ? 'repo-build' : 'docker-demo',
note: 'WiFi sensing infers coarse pose/presence from CSI — it is not a camera. Accuracy claims must be MEASURED vs a baseline (run `ruview.claim_check`).',
};
},
},
'ruview.claim_check': {
title: 'Claim check',
description: 'Static lint: scan text for untagged or overstated accuracy claims (the "prove everything" guardrail). Returns findings.',
inputSchema: {
type: 'object',
required: ['text'],
properties: { text: { type: 'string', description: 'The text to lint (a report, README section, PR body, model card).' } },
},
handler(args = {}) {
const result = claimCheck(String(args.text ?? ''));
return { ...result, summary: summarize(result) };
},
},
'ruview.verify': {
title: 'Verify (witness)',
description: 'Run the deterministic proof (archive/v1/data/proof/verify.py) and report VERDICT. Fail-closed if not in a RuView repo or python is missing.',
inputSchema: {
type: 'object',
properties: { repo: { type: 'string', description: 'RuView repo root. Default: auto-detect from cwd.' } },
},
handler(args = {}) {
const repo = args.repo ? resolve(args.repo) : findRepoRoot();
if (!repo) return { ok: false, reason: 'not_in_ruview_repo', hint: 'Run inside the RuView monorepo or pass {repo}.' };
const proof = join(repo, 'archive', 'v1', 'data', 'proof', 'verify.py');
if (!existsSync(proof)) return { ok: false, reason: 'proof_missing', path: proof };
const py = which('python') || which('python3');
if (!py) return { ok: false, reason: 'python_missing', hint: 'Install python to run the deterministic proof.' };
const r = run(py, [proof], { cwd: repo, timeout: 180000 });
const verdict = /VERDICT:\s*PASS/i.test(r.stdout) ? 'PASS' : (/VERDICT:\s*FAIL/i.test(r.stdout) ? 'FAIL' : 'UNKNOWN');
return { ok: r.ok && verdict === 'PASS', verdict, exit: r.status, tail: r.stdout.slice(-1200), stderr: r.stderr.slice(-400) };
},
},
'ruview.node_monitor': {
title: 'Node monitor',
description: 'Open an ESP32 serial port and assert CSI is flowing (MGMT+DATA). Fail-closed if python+pyserial or the port is absent. Read-only.',
inputSchema: {
type: 'object',
properties: {
port: { type: 'string', description: 'Serial port, e.g. COM8 or /dev/ttyUSB0.' },
seconds: { type: 'number', description: 'Capture window (default 12).' },
},
},
handler(args = {}) {
const port = args.port;
if (!port) return { ok: false, reason: 'no_port', hint: 'Pass {port} (e.g. COM8).' };
const py = which('python') || which('python3');
if (!py) return { ok: false, reason: 'python_missing' };
const dur = Number(args.seconds) > 0 ? Number(args.seconds) : 12;
const script = [
'import sys,time',
'try:',
' import serial',
'except Exception as e:',
" print('NO_PYSERIAL'); sys.exit(3)",
`ser=serial.Serial(${JSON.stringify(port)},115200,timeout=1)`,
'csi=0; n=0; t=time.time()',
`while time.time()-t<${dur}:`,
' ln=ser.readline()',
' if not ln: continue',
" s=ln.decode('utf-8','replace')",
' n+=1',
" if 'CSI cb' in s or 'csi_collector' in s: csi+=1",
" if 'MGMT+DATA' in s: print('UPGRADE_MGMT_DATA')",
'ser.close()',
"print(f'LINES={n} CSI={csi}')",
].join('\n');
const r = run(py, ['-c', script], { timeout: (dur + 10) * 1000 });
if (r.stdout.includes('NO_PYSERIAL')) return { ok: false, reason: 'pyserial_missing', hint: 'pip install pyserial' };
if (!r.ok) return { ok: false, reason: 'port_error', stderr: r.stderr, error: r.error };
const csi = Number((r.stdout.match(/CSI=(\d+)/) || [])[1] || 0);
const upgraded = r.stdout.includes('UPGRADE_MGMT_DATA');
return { ok: csi > 0, csi_callbacks: csi, mgmt_data_upgrade: upgraded, raw: r.stdout.trim() };
},
},
'ruview.calibrate': {
title: 'Calibrate room',
description: 'Run the ADR-151 room pipeline via the wifi-densepose CLI (baseline→enroll→train-room). Fail-closed if the binary is absent.',
inputSchema: {
type: 'object',
properties: {
step: { type: 'string', enum: ['baseline', 'enroll', 'train-room', 'room-watch'], description: 'Which calibration step.' },
args: { type: 'array', items: { type: 'string' }, description: 'Extra CLI args passed through.' },
},
},
handler(args = {}) {
const step = args.step || 'baseline';
const bin = which('wifi-densepose');
const repo = findRepoRoot();
if (!bin && !repo) return { ok: false, reason: 'cli_missing', hint: 'Install the wifi-densepose CLI or run in the repo (cargo run -p wifi-densepose-cli).' };
const passthru = Array.isArray(args.args) ? args.args.map(String) : [];
// Prefer the installed binary; otherwise cargo-run from the repo.
const r = bin
? run(bin, [step, ...passthru], { timeout: 300000 })
: run('cargo', ['run', '-q', '-p', 'wifi-densepose-cli', '--', step, ...passthru], { cwd: repo, timeout: 600000 });
return { ok: r.ok, step, via: bin ? 'binary' : 'cargo', exit: r.status, tail: r.stdout.slice(-1500), stderr: r.stderr.slice(-500) };
},
},
'ruview.node_flash': {
title: 'Node flash',
description: 'Build+flash an ESP32 firmware variant. MUTATING + hardware. Fail-closed off-Windows or without ESP-IDF. Never claims hardware validation without a boot log.',
inputSchema: {
type: 'object',
properties: {
port: { type: 'string', description: 'Target port, e.g. COM8.' },
variant: { type: 'string', enum: ['s3-8mb', 's3-4mb', 'c6'], description: 'Firmware variant.' },
confirm: { type: 'boolean', description: 'Must be true to actually flash (guard).' },
},
},
handler(args = {}) {
if (process.platform !== 'win32') {
return { ok: false, reason: 'unsupported_platform', detail: 'The ESP-IDF flash flow is Windows-subprocess-specific today (see CLAUDE.local.md).' };
}
if (!args.confirm) {
return { ok: false, reason: 'not_confirmed', detail: 'Mutating hardware op — re-call with {confirm:true}.', would_flash: { port: args.port, variant: args.variant || 's3-8mb' } };
}
return { ok: false, reason: 'manual_step_required', detail: 'Flashing uses the pinned ESP-IDF subprocess in CLAUDE.local.md. This tool returns the exact command rather than running an unattended flash.', see: 'skills/provision-node.md' };
},
},
};
/** Run one tool by name; returns the structured result (or an error envelope). */
export function runTool(name, args) {
const tool = TOOLS[name];
if (!tool) return { ok: false, reason: 'unknown_tool', name, available: Object.keys(TOOLS) };
try {
return tool.handler(args || {});
} catch (err) {
return { ok: false, reason: 'tool_threw', name, error: String(err && err.message || err) };
}
}
/** MCP-shaped tool list: [{name, description, inputSchema}]. */
export function listTools() {
return Object.entries(TOOLS).map(([name, t]) => ({ name, description: t.description, inputSchema: t.inputSchema }));
}
+111
View File
@@ -0,0 +1,111 @@
// SPDX-License-Identifier: MIT
// RuView harness tests — Node's built-in test runner (no devDeps to install).
// Run: `node --test test/` (or `npm test`).
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { claimCheck, summarize } from '../src/guardrails.js';
import { TOOLS, runTool, listTools, findRepoRoot } from '../src/tools.js';
import { run } from '../bin/cli.js';
test('guardrail flags the retracted 100% framing as high severity', () => {
const r = claimCheck('Our model reaches 100% accuracy on every pose.');
assert.equal(r.ok, false);
assert.ok(r.findings.some((f) => f.severity === 'high'));
});
test('guardrail flags an untagged percentage accuracy claim', () => {
// "hit", not "measured" — "measured" would (correctly) route to the no-reproducer branch.
const r = claimCheck('We hit 92.9% PCK on the test set.');
assert.equal(r.ok, false);
assert.ok(r.findings.some((f) => /not tagged/i.test(f.reason)));
});
test('guardrail passes a MEASURED claim that cites a reproducer', () => {
const r = claimCheck('Held-out PCK@20 59.5% vs 50% mean-pose baseline = +9.4pp (MEASURED, verify.py).');
assert.equal(r.ok, true, JSON.stringify(r.findings));
});
test('guardrail flags MEASURED with no reproducer', () => {
const r = claimCheck('Presence detection 97% (MEASURED).');
assert.equal(r.ok, false);
assert.ok(r.findings.some((f) => /no reproducer/i.test(f.reason)));
});
test('guardrail ignores non-metric prose', () => {
assert.equal(claimCheck('The ESP32 streams CSI over UDP to the sensing-server.').ok, true);
assert.equal(claimCheck('').ok, true);
});
test('summarize gives PASS/finding text', () => {
assert.match(summarize(claimCheck('nothing here')), /PASS/);
assert.match(summarize(claimCheck('100% accuracy')), /finding/);
});
test('registry exposes the documented tools with schemas', () => {
const names = Object.keys(TOOLS);
for (const n of ['ruview.onboard', 'ruview.claim_check', 'ruview.verify', 'ruview.node_monitor', 'ruview.calibrate', 'ruview.node_flash']) {
assert.ok(names.includes(n), `missing ${n}`);
assert.equal(TOOLS[n].inputSchema.type, 'object');
}
assert.equal(listTools().length, names.length);
});
test('ruview.onboard returns paths and a recommendation', () => {
const r = runTool('ruview.onboard', {});
assert.equal(r.ok, true);
assert.ok(r.paths['live-esp32']);
assert.ok(['repo-build', 'docker-demo'].includes(r.recommend));
});
test('ruview.claim_check tool wraps the guardrail', () => {
const r = runTool('ruview.claim_check', { text: '100% accuracy' });
assert.equal(r.ok, false);
assert.match(r.summary, /honesty|tag|MEASURED|finding/i);
});
test('unknown tool fails closed', () => {
const r = runTool('ruview.does_not_exist', {});
assert.equal(r.ok, false);
assert.equal(r.reason, 'unknown_tool');
});
test('node_monitor fails closed without a port', () => {
const r = runTool('ruview.node_monitor', {});
assert.equal(r.ok, false);
assert.equal(r.reason, 'no_port');
});
test('node_flash refuses without confirm (mutating guard)', () => {
const r = runTool('ruview.node_flash', { port: 'COM8', variant: 's3-8mb' });
assert.equal(r.ok, false);
// either not-confirmed (win32) or unsupported_platform (posix) — both fail-closed
assert.ok(['not_confirmed', 'unsupported_platform'].includes(r.reason));
});
test('verify fails closed when not in a RuView repo', () => {
// point at a tmp dir with no repo markers
const r = runTool('ruview.verify', { repo: process.platform === 'win32' ? 'C:/Windows/Temp' : '/tmp' });
assert.equal(r.ok, false);
assert.ok(['proof_missing', 'python_missing'].includes(r.reason), r.reason);
});
test('CLI run(): claim-check exits non-zero on a bad claim', async () => {
const code = await run(['claim-check', '--text', '100% accuracy']);
assert.notEqual(code, 0);
});
test('CLI run(): doctor exits 0 (tools-only path)', async () => {
const code = await run(['doctor']);
assert.equal(code, 0);
});
test('CLI run(): unknown command exits non-zero', async () => {
assert.notEqual(await run(['definitely-not-a-command']), 0);
});
test('findRepoRoot locates this monorepo from cwd', () => {
// when run from within wifi-densepose, it should find a root; elsewhere null is fine
const root = findRepoRoot();
assert.ok(root === null || typeof root === 'string');
});
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.