mirror of
https://github.com/ruvnet/RuView
synced 2026-08-11 20:41:44 +00:00
e6f26e9ac9
* docs(adr): deep review of the RuView npm surface — ADR-263/264/265 optimization strategies
ADR-263 — @ruvnet/ruview@0.1.0 harness review (O1–O9):
- HIGH: claim-check CLI fails open on empty input (no --text/--file -> PASS exit 0)
- HIGH: MCP stdio server head-of-line blocking (spawnSync verify/calibrate up to 600s)
- MEASURED: optionalDependencies triple the cold npx install (4 pkgs/620kB/71 files
vs 1 pkg/172kB/22 files with --omit=optional) for a path that never imports them
- maxBuffer truncation, python -c port interpolation, version drift, duplicate skills,
guardrail METRIC_TERMS substring false positives ('map'/'F1' — found by dogfooding
claim-check on these very ADRs), zero CI
ADR-264 — @ruvnet/rvagent@0.1.0 + @ruv/ruview-cli review (O1–O9), verified against
the published registry tarball:
- HIGH: exports.require -> dist/index.cjs which is never built nor published
- MEASURED: 44 dead source-map files = 62,698B of the 188kB unpacked payload
- stdio-only server described as dual-transport; mixed dot/underscore tool names;
double Zod validation + hand-duplicated advertised schemas; 2-fd leak per training
job; unbounded body in the unwired HTTP scaffold; dead detectCogBinary candidates;
ruview bin-name collision
ADR-265 — cross-cutting npm distribution strategy: npm-packages.yml CI matrix
(test + pack-content/size gate + tarball-install smoke test), publish-from-CI-only
with npm provenance, version single-sourcing from package.json, bin/namespace
ownership (ruview bin belongs to @ruvnet/ruview), claim-check on package READMEs.
Docs only — no runtime code changed. Index/CHANGELOG/CLAUDE.md/README counts updated.
Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01WrGfTGKv1oWZ6iwXZACULz
* fix(npm): implement ADR-263/264/265 — harness fail-closed + async MCP, rvagent packaging/transport/naming, npm CI+provenance gate
ADR-263 (@ruvnet/ruview 0.2.0), O1-O9:
- claim-check fails closed on empty input (CLI exit 2, empty_text tool error)
- MCP stdio server dispatches tools/call asynchronously (promise-based spawn);
ping answers while a 3s fake verify runs — pinned by new e2e test
- optionalDependencies dropped: cold npx installs exactly 1 package
(MEASURED: was 4 pkgs/620kB/71 files via npm i in a clean prefix)
- bounded rolling output tails replace spawnSync 1MiB maxBuffer
- node_monitor port passed via sys.argv, never spliced into python -c source
- serverInfo.version read from package.json; resources/prompts stubs
- skills single-sourced: prepack sync script generates .claude/skills/ copies
- which() = memoized dep-free PATH scan
- tools underscore-canonical (ruview_claim_check, ...) + dotted aliases
- guardrail precision: word-boundary map/f1/auc/iou, code-span + F1/O2 label
scrubbing, quantitative-claims-only; packaging reproducer hints
- 30/30 tests (was 17), incl. concurrency e2e + fail-open regression pins
ADR-264 (@ruvnet/rvagent 0.2.0), O1-O9:
- exports fixed: types-first, phantom dist/index.cjs require target removed
- tarball map-free: 127,704B unpacked / 46 files / 0 maps (MEASURED,
npm pack --dry-run; was 188kB incl. 44 maps referencing unshipped src)
- Streamable HTTP actually wired behind RVAGENT_HTTP_PORT: one transport +
one MCP server per session (mcp-session-id routing), 1MiB body cap (413),
port-aware localhost origin gate; dual-transport description now true
- tools renamed underscore-canonical with dotted router-only aliases
- single Zod validation gate; advertised inputSchema generated from the same
Zod source (zod-to-json-schema)
- train_count: parent log fds closed (was leaking 2/job); job records
persisted to <jobsDir>/<id>.json (job_status survives restarts); bounded
log-tail reads
- detectCogBinary probes its candidates instead of dead-coding them
- version from package.json; @types/express dropped; @types/jest -> 29
- README rewritten to match reality (no phantom subcommands/policy layer)
- 99/99 jest tests (incl. new session/body-cap suite + previously-broken
manifest suite); stdio handshake + HTTP session flow smoke-tested live
ADR-265 D1-D4:
- .github/workflows/npm-packages.yml: 3-package x Node 20/22 gate — tests,
version-literal grep (D3), pack-content/size gate, tarball-install smoke
test (catches the ADR-264 F1 class), README claim-check (D4)
- .github/workflows/ruview-npm-release.yml: publish from CI only with
npm publish --provenance
- @ruv/ruview-cli bin renamed ruview-cli (ruview bin belongs to
@ruvnet/ruview); version single-sourced
- ci.yml NODE_VERSION 18 -> 20
ADR statuses updated to Accepted/implemented; harness manifest re-pinned;
ADR-263/264/265 + both package READMEs pass claim-check.
Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01WrGfTGKv1oWZ6iwXZACULz
* perf(rvagent): lazy-load HTTP transport + memoize generated tool schemas
stdio time-to-first-response ~242ms -> ~189ms (-22%; MEASURED, median of
repeated initialize round-trips against dist/index.js in this container).
- ./http-transport.js now imported lazily inside the RVAGENT_HTTP_PORT
branch: it chain-loads the MCP SDK streamableHttp module (~48ms MEASURED
via per-module import() timing) which the default stdio path never uses
- toolInputJsonSchema memoized per tool: schemas are static for the process
lifetime; under the session-per-server HTTP model every session calls
tools/list, so stop re-walking the Zod tree each time
No behavior change: 99/99 jest tests; HTTP session flow re-smoke-tested
through the lazy import path (initialize -> 200 + mcp-session-id).
Profiled @ruvnet/ruview too and left it alone: 50ms CLI startup vs ~29ms
bare 'node -e ""' floor on the same box (MEASURED) — already near the
interpreter floor with zero dependencies.
Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01WrGfTGKv1oWZ6iwXZACULz
* ci(ruview-cli): pass jest --passWithNoTests so the private no-test package doesn't fail the npm-packages matrix
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(npm): address 10 verified review findings in harness + rvagent before 0.2.0 publish
harness/ruview (@ruvnet/ruview):
- guardrails: digit gate now sees numbers inside code spans; F1-style
metric tokens followed by ':' or a nearby number are no longer scrubbed
(fail-open regressions in the honesty gate)
- mcp-server: tools/call requests serialize through a FIFO promise chain
(hardware/mutating tools never overlap) while ping/tools/list stay
immediate; stdin close drains in-flight responses before exit
- tools: which() no longer memoizes negative lookups
tools/ruview-mcp (@ruvnet/rvagent):
- index: realpath invoked-directly guard — library import no longer
connects a stdio transport to the consumer's process
- http-transport: explicit allowedOrigins is exact-match only (localhost
any-port convenience applies only with no configured allowlist);
session map gains maxSessions=64 + 5min idle TTL sweep
- train-count: job records persist the child pid and reconcile stale
'running' status after a server restart (exit-code marker or dead pid)
- config: cog binary candidates ordered by process.arch
.github/workflows/ruview-npm-release.yml: port the full ADR-265 D1 gate
(version-literal check, unpacked-size budget, tarball-install smoke test)
from npm-packages.yml so the publish path enforces what the header claims.
Tests: harness 30→36, rvagent 99→112, all passing.
Co-Authored-By: claude-flow <ruv@ruv.net>
---------
Co-authored-by: Claude <noreply@anthropic.com>
170 lines
10 KiB
Markdown
170 lines
10 KiB
Markdown
# ADR-264: `@ruvnet/rvagent` MCP Server + `@ruv/ruview-cli` — Deep Review + Optimization Strategy
|
||
|
||
| Field | Value |
|
||
|-------|-------|
|
||
| **Status** | Accepted — **implemented** (O1–O9, `@ruvnet/rvagent@0.2.0`): `exports` fixed (types-first, no phantom `.cjs`), map-free tarball (127,704 B unpacked / 46 files / 0 maps — MEASURED, `npm pack --dry-run`, from 188 kB), Streamable HTTP **wired** behind `RVAGENT_HTTP_PORT` with per-session transports + 1 MiB body cap + port-aware origin gate, underscore tool names with dotted router aliases, single Zod validation gate with generated JSON Schemas, fd-leak fixed + persisted job records + bounded log tails, probing `detectCogBinary`, package.json-sourced version, `ruview-cli` bin renamed. 99/99 jest tests (MEASURED); both transports smoke-tested live |
|
||
| **Date** | 2026-07-02 |
|
||
| **Deciders** | ruv |
|
||
| **Codename** | **RUVIEW-NPM-REVIEW-2** |
|
||
| **Supersedes / amends** | none (reviews the ADR-104/ADR-124 artifacts; feeds ADR-265 distribution strategy) |
|
||
|
||
## Context
|
||
|
||
Two TypeScript npm packages expose RuView sensing to agents and shells:
|
||
|
||
- **`@ruvnet/rvagent@0.1.0`** (`tools/ruview-mcp/`) — SENSE-BRIDGE, the MCP
|
||
server over the sensing-server HTTP API + cog binaries: 12 tools
|
||
(csi/pose/count/registry/train/job + ADR-124 BFLD/presence/vitals). Published
|
||
(188 kB unpacked — MEASURED, `npm view @ruvnet/rvagent`). Deps:
|
||
`@modelcontextprotocol/sdk` + `zod`.
|
||
- **`@ruv/ruview-cli@0.0.1`** (`tools/ruview-cli/`) — `private: true` yargs CLI
|
||
mirroring the same capabilities; intentionally duplicates `http.ts`/`cog.ts`/
|
||
`config.ts` (~150 lines) to stay standalone.
|
||
|
||
This ADR records a deep review of both: packaging correctness (verified against
|
||
the **published** tarball, not just the source tree), protocol/interop, resource
|
||
lifecycle, and the honesty of the package's own self-description — the same
|
||
MEASURED-vs-CLAIMED bar the project applies to accuracy numbers.
|
||
|
||
## Findings
|
||
|
||
### F1 (HIGH, broken export): `require` condition points at a file that does not exist
|
||
|
||
`package.json` `exports["."].require = "./dist/index.cjs"`, but the build is
|
||
plain `tsc` (ESM only) and **the published 0.1.0 tarball contains no
|
||
`index.cjs`** (verified by listing the registry tarball). Any CJS consumer doing
|
||
`require('@ruvnet/rvagent')` resolves to a nonexistent file →
|
||
`ERR_MODULE_NOT_FOUND`. Additionally the `types` condition is listed **after**
|
||
`import`/`require`; TypeScript requires `types` first or it may be ignored under
|
||
`moduleResolution: bundler/node16`.
|
||
|
||
### F2 (MEDIUM, tarball bloat): a third of the published package is dead source maps
|
||
|
||
The 0.1.0 tarball ships **44 `.map` files = 62,698 B** against 78,209 B of
|
||
actual `.js` (MEASURED, extracted registry tarball). `src/` is not published, so
|
||
every `sourceMappingURL` points at `../src/*.ts` that consumers do not have —
|
||
the maps can never resolve. Also `files` lists `CHANGELOG.md`, which does not
|
||
exist in `tools/ruview-mcp/` (npm silently skips it), so the advertised file set
|
||
is partly fictional.
|
||
|
||
### F3 (MEDIUM, honesty): the package description claims a transport it does not start
|
||
|
||
The description reads "**dual-transport MCP server (stdio + Streamable HTTP)**",
|
||
but `main()` in `src/index.ts` wires **stdio only**. `http-transport.ts` is a
|
||
complete, tested scaffold that nothing imports at runtime — there is no flag,
|
||
env var, or subcommand that starts it. By this project's own rule this is a
|
||
CLAIMED capability presented as shipped. Either wire it (`--http` /
|
||
`RVAGENT_HTTP_PORT` gate) or de-claim the description until it is.
|
||
|
||
### F4 (MEDIUM, interop + inconsistency): two tool-naming conventions, one of them dot-based
|
||
|
||
Six tools use `ruview_snake_case`; six (ADR-124 additions) use
|
||
`ruview.dotted.names`. Same interop caveat as ADR-263 F9 (host tool-name
|
||
regexes commonly `^[a-zA-Z0-9_-]{1,64}$`), plus the split convention makes the
|
||
tool surface look like two products. Standardize on underscores and accept the
|
||
dotted forms as aliases for one deprecation cycle.
|
||
|
||
### F5 (MEDIUM, double work + drift): every tool input is validated twice from two hand-maintained schemas
|
||
|
||
`CallToolRequestSchema` handler runs `TOOL_INPUT_SCHEMAS[name].safeParse(args)`,
|
||
then each tool handler runs its own `schema.parse(args)` again — two full Zod
|
||
passes per call. Separately, the `inputSchema` JSON advertised via `tools/list`
|
||
is **hand-written** and duplicates the Zod schema field-by-field (defaults,
|
||
min/max, descriptions) — schema drift between what is advertised and what is
|
||
enforced is a matter of time. Parse once at the gate, pass the typed result to
|
||
handlers, and generate the advertised JSON Schema from the Zod source
|
||
(`zod-to-json-schema` at build time, or Zod 4's native `z.toJSONSchema` when the
|
||
SDK's peer range allows).
|
||
|
||
### F6 (MEDIUM, resource lifecycle): `train_count` leaks 2 fds per job; job registry is process-local
|
||
|
||
`trainCount` opens `logFdOut`/`logFdErr` with `openSync` and never closes them
|
||
in the parent — the spawned cargo child inherits duplicates, but the parent's
|
||
descriptors stay open for the MCP server's lifetime: 2 leaked fds per training
|
||
job. `jobRegistry` is an in-memory `Map`, so `ruview_job_status` after a server
|
||
restart reports "not found" for a training run that is still burning GPU (the
|
||
source comments acknowledge this; the fix — persist `~/.ruview/jobs/<id>.json`,
|
||
already the documented layout — is small). Also `jobStatus` re-`import`s
|
||
`node:fs` on every poll and reads the entire log to return 20 lines.
|
||
|
||
### F7 (MEDIUM, security/robustness of the HTTP scaffold): unbounded body + one shared session transport
|
||
|
||
`http-transport.ts` buffers the request body with no size cap (memory DoS the
|
||
moment it is wired to a socket), reuses a **single**
|
||
`StreamableHTTPServerTransport` with `sessionIdGenerator` for all clients (the
|
||
SDK's stateful mode expects one transport per session — a second client's
|
||
`initialize` collides), and the Origin allowlist is exact-match
|
||
(`http://localhost` will not match a real browser origin `http://localhost:5173`).
|
||
Must be fixed **before** F3 wires it in; bearer-token + 127.0.0.1 defaults are
|
||
already right.
|
||
|
||
### F8 (LOW, dead/misleading code): `detectCogBinary` always returns the bare name
|
||
|
||
It builds a 4-candidate appliance-path array and then returns
|
||
`candidates[candidates.length - 1]` — i.e. always `name` — without checking
|
||
existence. The candidates are dead weight that reads as if path detection
|
||
happens. Either probe with `existsSync` or delete the array.
|
||
|
||
### F9 (LOW, drift + hygiene): hardcoded versions, unused/mismatched devDeps, bin-name collision
|
||
|
||
`PACKAGE_VERSION = "0.1.0"` (index.ts) duplicates package.json;
|
||
`@types/express` is unused (`http-transport` uses `node:http`); `@types/jest@30`
|
||
against `jest@29`; `ruview-cli` hardcodes `.version("0.0.1")`. And
|
||
`@ruv/ruview-cli` claims the **`ruview`** bin name, which collides with
|
||
`@ruvnet/ruview`'s bin (ADR-182) if both are ever installed globally —
|
||
ADR-263/265 give the `ruview` name to the harness; the CLI must rename or fold.
|
||
|
||
## Decision
|
||
|
||
- **O1 (F1):** fix `exports`: drop the `require` condition (ESM-only is fine for
|
||
a bin-first package) or add a real CJS build; put `types` first. Add a CI
|
||
smoke test that does `npm pack` + `node -e "import('<tarball install>')"`.
|
||
- **O2 (F2):** publish without maps: `declarationMap: false`, `sourceMap: false`
|
||
in a `tsconfig.build.json` used by `prepack` (or add `!dist/**/*.map` to
|
||
`files`). Remove the phantom `CHANGELOG.md` entry or create the file.
|
||
Acceptance: unpacked size ≤ ~125 kB (from 188 kB — MEASURED, `npm pack --dry-run`).
|
||
- **O3 (F3, F7):** wire the HTTP transport behind an explicit opt-in
|
||
(`RVAGENT_HTTP_PORT` or `--http`), after F7 fixes: per-session transport map
|
||
keyed by `mcp-session-id`, 1 MiB body cap, origin matching that honors ports
|
||
(compare `URL.origin` prefixes or document exact origins). Until then, change
|
||
the description to "stdio MCP server (Streamable HTTP scaffold, unwired)".
|
||
- **O4 (F4):** rename dotted tools to underscore (`ruview_bfld_last_scan`, …),
|
||
keep dotted aliases in the call router for one release, note it in the README.
|
||
- **O5 (F5):** single validation gate: the registry maps name → Zod schema →
|
||
typed handler; advertised `inputSchema` generated from Zod at build time.
|
||
- **O6 (F6):** close parent fds after spawn (`closeSync` post-`spawn` — the
|
||
child holds its own copies), persist job records to
|
||
`<jobsDir>/<id>.json`, and read log tails with a bounded read.
|
||
- **O7 (F8):** make `detectCogBinary` actually probe (`existsSync` over the
|
||
candidates) — it is the entire reason the function exists.
|
||
- **O8 (F9):** single-source versions from package.json; drop `@types/express`;
|
||
align `@types/jest` with jest 29 (or move to `node:test` like the harness and
|
||
drop the jest toolchain entirely — it is the heaviest devDep in both
|
||
packages).
|
||
- **O9 (F9, scope):** fold `@ruv/ruview-cli` into `rvagent` as a second bin
|
||
(`rvagent-cli`) sharing `http/cog/config`, or keep it private-forever and say
|
||
so in its README. Its `ruview` bin name is surrendered to `@ruvnet/ruview`
|
||
either way.
|
||
|
||
## Consequences
|
||
|
||
- CJS consumers stop hitting a guaranteed-broken export path (F1 is the only
|
||
finding that fails for every consumer of that entry point deterministically).
|
||
- The published artifact shrinks ~33% (MEASURED, F2 tarball listing: 62,698 B
|
||
of maps in a 188 kB unpacked payload) and stops advertising files/transports
|
||
it does not contain — the package description itself passes the project's
|
||
claim-check bar.
|
||
- One schema source ends advertised-vs-enforced drift and halves per-call
|
||
validation cost; naming unification makes the 12-tool surface read as one
|
||
product and survive strict host tool-name validation.
|
||
- Long-lived MCP servers stop accumulating fds during training campaigns, and
|
||
job polling survives restarts.
|
||
- Costs: the alias cycle (O4) briefly doubles the advertised tool count unless
|
||
aliases are router-only (recommended: router-only, advertise underscore names
|
||
exclusively); folding the CLI (O9) retires a package name already in use in
|
||
scripts, so it needs a deprecation note.
|
||
- Verification for the implementing PR: `npm pack --dry-run` asserted file list
|
||
(no `.map`, no phantom entries), pack-size budget in CI (ADR-265), jest/`node
|
||
--test` suite green, and a tarball-install smoke test for both `import` and
|
||
the `rvagent` bin.
|