diff --git a/docs/adr/ADR-133-homecore-assist-ruflo.md b/docs/adr/ADR-133-homecore-assist-ruflo.md new file mode 100644 index 00000000..6a712e89 --- /dev/null +++ b/docs/adr/ADR-133-homecore-assist-ruflo.md @@ -0,0 +1,176 @@ +# ADR-133: HOMECORE-ASSIST — Voice/Intent Pipeline + Ruflo Agent Bridge + +| Field | Value | +|-------|-------| +| **Status** | Proposed | +| **Date** | 2026-05-25 | +| **Deciders** | ruv | +| **Codename** | **HOMECORE-ASSIST** | +| **Relates to** | [ADR-126](ADR-126-ruview-native-ha-port-master.md) (HOMECORE master), [ADR-127](ADR-127-homecore-state-machine-rust.md) (HOMECORE-CORE), [ADR-130](ADR-130-homecore-rest-websocket-api.md) (HOMECORE-API), [ADR-124](ADR-124-rvagent-mcp-ruvector-npm-integration.md) (SENSE-BRIDGE) | +| **Tracking issue** | TBD | +| **Crate** | `v2/crates/homecore-assist` | + +--- + +## 1. Context + +Home Assistant's Assist pipeline (`homeassistant/components/assist_pipeline/`) provides +voice-to-intent-to-response processing. It chains: + +1. **STT** (speech-to-text) — Whisper, cloud, or satellite +2. **NLU** (natural language understanding) — intent recognition via regex/slots +3. **Intent handler** — maps intent to a HA service call +4. **TTS** (text-to-speech) — synthesises the response for the caller + +HA's intent model (`homeassistant/helpers/intent.py`) is keyword/regex based. Every +intent is a named template with slot definitions and a handler that dispatches to HA +services. The built-in intents (`homeassistant/components/conversation/default_agent.py`) +cover `HassTurnOn`, `HassTurnOff`, `HassLightSet`, `HassNevermind`, `HassCancelAll`, +`HassGetState`, `HassGetWeather`, and many others. + +HOMECORE needs a wire-compatible Assist pipeline so that: +- The HA iOS/Android companion app's "Assist" button works against HOMECORE. +- The HOMECORE-API WebSocket `assist` command (ADR-130 §2.2) has a handler. +- The ruflo agent toolchain (ADR-124) can provide LLM-grade intent disambiguation as a + drop-in upgrade path for the P1 regex recognizer. + +### 1.1 Ruflo integration approach + +Ruflo's agent runner exposes an MCP-over-stdio interface (`node ruflo-agent.js`). +HOMECORE-ASSIST manages a long-lived subprocess (Q3 Windows concern below), sends +utterance JSON, and receives intent JSON back. In P1 we ship only the trait surface +and a `NoopRunner` stub; the real subprocess management is P2. + +### 1.2 Ruvector semantic intent matching (P2) + +`ruvector-core` provides embedding + cosine-similarity primitives. P2 will add a +`SemanticIntentRecognizer` that embeds the utterance and compares it to a HNSW index +of intent exemplars, falling back to the P1 regex recognizer when similarity < 0.75. +This is the mechanism that allows "dim the lights" to match `HassLightSet` without an +explicit regex entry. + +--- + +## 2. Design + +### 2.1 Module layout (`v2/crates/homecore-assist/`) + +| Module | Contents | +|--------|----------| +| `intent` | `IntentName` newtype, `Intent` (name + slots), `IntentResponse` (speech + optional card + optional data) | +| `recognizer` | `IntentRecognizer` trait; `RegexIntentRecognizer` (P1); `SemanticIntentRecognizer` stub (P2) | +| `handler` | `IntentHandler` trait; built-in handlers: `HassTurnOn`, `HassTurnOff`, `HassLightSet`, `HassNevermind`, `HassCancelAll` | +| `runner` | `RufloRunner` trait + `RufloRunnerOpts`; `NoopRunner` (P1 stub); real subprocess runner (P2) | +| `pipeline` | `AssistPipeline`: wires recognizer → handler → response; exposes `async fn process(utterance, language) -> IntentResponse` | + +### 2.2 Built-in intent handlers (P1) + +| Handler | HA service call | Slot | +|---------|-----------------|------| +| `HassTurnOn` | `homeassistant.turn_on` / `light.turn_on` / `switch.turn_on` | `entity_id` | +| `HassTurnOff` | `homeassistant.turn_off` / `light.turn_off` / `switch.turn_off` | `entity_id` | +| `HassLightSet` | `light.turn_on` | `entity_id`, `brightness` (0–255), `color_name` | +| `HassNevermind` | — (no-op, returns acknowledgement) | — | +| `HassCancelAll` | — (fires `homeassistant_stop_all_scripts` domain event) | — | + +### 2.3 IntentResponse + +```rust +pub struct IntentResponse { + pub speech: String, + pub card: Option, + pub data: Option, +} + +pub struct Card { + pub title: String, + pub content: String, +} +``` + +### 2.4 RufloRunner trait + +```rust +#[async_trait] +pub trait RufloRunner: Send + Sync + 'static { + async fn spawn(&mut self, opts: RufloRunnerOpts) -> Result<(), AssistError>; + async fn send_request(&self, payload: serde_json::Value) -> Result; + async fn shutdown(&mut self) -> Result<(), AssistError>; +} +``` + +`RufloResponse` is `{ intent: Option, speech: Option }`. + +### 2.5 Pipeline + +```rust +pub struct AssistPipeline { + recognizer: R, + handler: H, + runner: Option>, +} + +impl AssistPipeline { + pub async fn process(&self, utterance: &str, language: &str, hc: &HomeCore) + -> Result; +} +``` + +--- + +## 3. Questions & Answers + +### Q1 — Why not reuse HA's existing `homeassistant.helpers.intent` via PyO3? + +PyO3 bridges add a GIL lock on every cross-language call; the Assist pipeline processes +hundreds of short utterances per day from voice satellites. A native Rust recognizer is +simpler and faster. Python HA can still connect as an external integration via MQTT or +the HOMECORE WebSocket API. + +### Q2 — How does `RegexIntentRecognizer` handle ambiguity? + +Patterns are tried in registration order; the first match wins. Slot extraction uses +named capture groups. A future P2 upgrade can run all patterns, score them by slot +completeness, and return the highest-scoring match. + +### Q3 — Windows subprocess teardown (ruflo runner subprocess on Windows) + +`tokio::process::Child` on Windows does not automatically kill the child process when +the `Child` struct is dropped — `SIGTERM` is not a Windows concept, and `TerminateProcess` +is not called automatically. Options for P2: + +1. Call `child.start_kill()` in a `Drop` impl (requires a `Runtime` handle — tricky in sync Drop). +2. Wrap `Child` in an `Arc>>` and call `kill()` in an `async fn shutdown()`. +3. Use a Windows job object to bind the subprocess lifetime to the parent process. + +**P2 decision**: implement option 2 (explicit `async shutdown()`) + register a `tokio::signal` +handler for `Ctrl+C` / `SIGINT` that calls `shutdown()` before exit. Document the Windows caveat +in the crate README and in `runner.rs`. Job object approach (option 3) is deferred to P3 only +if option 2 proves insufficient in fleet testing. + +### Q4 — Why is `SemanticIntentRecognizer` a P2 stub? + +The ruvector HNSW index requires the vector store to be populated at startup with intent +exemplars. That startup path requires deciding on a serialization format (HNSW index files +vs. an in-memory array at compile time), which intersects with ADR-084 (RabitQ) and ADR-067 +(ruvector v2.0.5). P2 will define the exemplar format and populate the index. + +--- + +## 4. Consequences + +- **Positive**: HOMECORE-API `assist` WebSocket command gets a functional backend. +- **Positive**: Ruflo LLM pipelines can upgrade intent matching by swapping the `RufloRunner` impl. +- **Positive**: P1 ships with zero new heavy dependencies (no subprocess spawning, no ML runtime). +- **Negative**: Regex matching has limited coverage; long-tail utterances will return "I'm not sure". +- **Deferral**: ruvector semantic recognizer and real subprocess runner both land in P2. + +--- + +## 5. Implementation phases + +| Phase | Scope | +|-------|-------| +| **P1** (this ADR) | `intent`, `recognizer` (regex), `handler` (5 built-ins), `runner` (trait + noop), `pipeline` (end-to-end wiring), 10–15 tests | +| **P2** | Real `tokio::process::Child` runner with Windows-safe teardown; `SemanticIntentRecognizer` with ruvector HNSW | +| **P3** | STT/TTS bridge, satellite protocol, cloud fallback | diff --git a/docs/design/HOMECORE-FRONTEND-design-recon.md b/docs/design/HOMECORE-FRONTEND-design-recon.md new file mode 100644 index 00000000..13023229 --- /dev/null +++ b/docs/design/HOMECORE-FRONTEND-design-recon.md @@ -0,0 +1,301 @@ +# HOMECORE-FRONTEND Design Recon — ADR-131 + +**Source:** cognitum-one/v0-appliance dashboard at `http://cognitum-v0:9000/` +**Captured:** 2026-05-25 by browser-recon agent (session `20260525-181819-adr131-recon`) +**Pages fetched:** dashboard, cogs, seeds, edge, analytics, settings, cluster, tailscale, aidefence, guide (all HTTP 200) +**Auth:** dashboard is unauthenticated; `/api/*` requires bearer token — all recon confined to dashboard pages + +--- + +## 1. Color Palette + +The entire UI is dark-only. There is no light mode and no `prefers-color-scheme` media query anywhere in the stylesheet. Every surface is drawn from a tight family of near-black navy blues with two accent hues: a cool teal (`--primary`) and a green (`--accent`). + +### Core tokens (hex conversions from HSL source) + +| CSS variable | HSL value | Hex | Role | +|---|---|---|---| +| `--background` | `220 25% 6%` | `#0b0e13` | Page background, modal overlay base | +| `--foreground` | `210 20% 92%` | `#e6eaee` | Body text, headings | +| `--primary` | `185 80% 50%` | `#19d4e5` | Teal — active nav underline, CTA borders, ring focus, brand slash | +| `--primary-foreground` | `220 25% 6%` | `#0b0e13` | Text on filled primary buttons | +| `--accent` | `142 70% 50%` | `#26d867` | Green — secondary CTA, success state, deploy button text | +| `--accent-foreground` | `220 25% 6%` | `#0b0e13` | Text on filled accent buttons | +| `--secondary` | `220 20% 14%` | `#1c212a` | Button fill, pill-tab background | +| `--card` | `220 20% 10%` | `#14171e` | Card surface (also popover) | +| `--surface-elevated` | `220 20% 12%` | `#181c24` | Slightly elevated card variant | +| `--surface-overlay` | `220 20% 8%` | `#111318` | Modal scrim, sticky navbar | +| `--muted` | `220 15% 15%` | `#20242b` | Muted chip backgrounds, scrollbar track | +| `--muted-foreground` | `215 15% 55%` | `#7b899d` | Secondary text, labels, timestamps | +| `--border` | `220 15% 18%` | `#272b34` | All borders (at 50% opacity by default) | +| `--destructive` | `0 65% 50%` | `#d22c2c` | Error state, danger button | +| `--ring` | `185 80% 50%` | `#19d4e5` | Focus ring (same hue as primary) | + +### Semantic status colors (inline, not variables) + +| State | Color | Hex | Usage | +|---|---|---|---| +| Online / success | `hsl(142 70% 50%)` | `#26d867` | `.badge.online`, `.dot.up`, `.heat-cell.up` | +| Warning | `hsl(38 80% 60%)` | `#e69940` | `.badge.unpaired`, `.hero-dot.warn`, banner backgrounds | +| Error / offline | `hsl(0 65% 50%)` | `#d22c2c` | `.badge.offline`, `.badge.danger`, `.dot.down` | +| Info (log line) | `hsl(205 80% 65%)` | `#4db8f5` | Log viewer `.info` class | +| Paired | `hsl(185 80% 50%)` | `#19d4e5` | `.badge.paired` (same as primary) | + +--- + +## 2. Typography + +### Font families + +The CSS declares two font families via CSS custom properties: + +- `--font-display: 'Outfit', system-ui, sans-serif` — all headings, nav items, buttons, card titles, KPI values. Outfit is a modern geometric sans loaded locally (no Google Fonts outbound call; the source comment says "ship from local chrome.css fallback"). +- `--font-mono: 'JetBrains Mono', monospace` — timestamps, port numbers, version strings, table cells, log output, KPI labels, chip text. + +### Type scale + +| Token name / usage | Size | Weight | Notes | +|---|---|---|---| +| Hero title (`h1.hero-title`) | `clamp(1.5rem, 2.4vw, 2.1rem)` | 600 | Fluid, capped at ~33.6px | +| Page h1 (`.page`) | `1.5rem` (24px) | 600 | All inner pages | +| Section heading (`.row-h h2`) | `1.125rem` (18px) | 700 | Section openers on Cogs/Dashboard | +| Card title (`.card-title`) | `0.9375rem` (15px) | 600 | | +| Body / button | `0.8125rem` (13px) | 400/500 | Default body, nav links, buttons | +| Secondary body / lede | `0.875rem` (14px) | 400 | Page lede text | +| Small label | `0.75rem` (12px) | 400–600 | Table cells, modal sub-text | +| Micro label | `0.6875rem` (11px) | 600 | Section eyebrows, uppercase KPI labels, badge text | +| Mono micro | `0.625rem` (10px) | 400 | Heatmap cells, chip category text | + +Letter-spacing: `0.1em` on section eyebrows (`.section h2`), `0.08em` on filter-rail headings and chip category text, `-0.02em` on all `h1–h4` display headings. Line-height for body is `1.5`; lede text uses `1.45`. + +--- + +## 3. Layout Primitives + +### Page shell + +``` +┌─────────────────────────────────────────────────────────┐ +│ .appbar (sticky, z-50, backdrop-filter:blur(8px)) │ +│ [brand-mark] [brand-text] [nav links scrollable] │ +├─────────────────────────────────────────────────────────┤ +│ .wrap (max-width: 1400px, padding: 1.5rem 1.25rem) │ +│ ┌── .hero (full-width, gradient bg, radial accents) │ +│ ├── .kpi-grid (auto-fill, min 170px columns) │ +│ ├── .section > h2 (eyebrow) + content │ +│ └── .grid / .grid-2 / .grid-3 (auto-fit) │ +├─────────────────────────────────────────────────────────┤ +│ footer.appfoot (border-top, centered text) │ +└─────────────────────────────────────────────────────────┘ +``` + +**Appbar:** `position: sticky; top: 0; z-index: 50`. Background is the page background at 90% opacity with 8px blur backdrop-filter, so the page content bleeds through. Nav links overflow-scroll horizontally with a right-fade mask gradient. + +**Active nav state:** primary-colored text + a 2px bottom border line (`::after` pseudo-element) positioned at bottom: -2px of the link. Hover reveals secondary background fill on the link. + +**Content wrap:** max-width 1400px, centered, 1.25rem horizontal padding. Inner page sections are separated by margin-bottom spacing in multiples of 0.75rem (base unit = 12px at 16px root). + +### Cogs page: app-store sub-navigation + +The Cogs page adds a sticky secondary nav bar (`.subnav`) at `top: 3.25rem` (just below the appbar). Tabs are borderless buttons with a 2px bottom underline indicator when active. A `flex: 1` spacer pushes a gear icon to the right edge. + +### Card patterns + +Three card variants, all sharing the same surface gradient and border: + +1. **Standard card (`.card`)** — `background: var(--gradient-card)` (linear 180deg from `--surface-elevated` to `--surface-overlay`), 1px border at 50% opacity, `--radius` (0.75rem), `box-shadow` 8px/32px dark drop shadow. +2. **KPI card (`.kpi`)** — 38px icon square left + text right, same gradient, 1rem/1.125rem padding, smaller vertical rhythm. +3. **Empty-state card (`.empty-card`)** — dashed 1px border (instead of solid), centered text, optional compact variant. The headline in `.empty-card h3` uses the primary teal, body explains what to do next. + +### Spacing rhythm + +Base unit is 4px. Gaps between grid items are universally `0.75rem` (12px). Card padding is `1.25rem` (20px) for standard, `0.875rem` (14px) for compact. Section margin-bottom is `1.5rem` (24px). The hero section uses `1.75rem` (28px) horizontal padding. + +--- + +## 4. Component Vocabulary + +### Navigation components + +- **Appbar** — sticky top bar with brand + horizontal nav links. Brand mark is a 32px rounded SVG icon square. +- **Nav link** — 0.4rem × 0.7rem padding, 0.4rem radius, transitions on color + background. Active state: primary text + 2px underline pseudo-element. Mobile: wraps below brand row at 720px. +- **Sub-nav / secondary tab bar** (`.subnav`) — app-store style horizontal tab strip, sticky under appbar. Used exclusively on Cogs. +- **Pill tabs** (`.pill-tabs` + `.pill-tab`) — smaller rounded-rect tab group for in-card filter switching. Active state fills with primary color. +- **Page tabs** (`.page-tabs`) — used on Analytics for domain view switching. Underline-style, same pattern as sub-nav but at content level. + +### Card & data display + +- **Card** (`.card`) — base data container with gradient surface, subtle border, shadow. +- **KPI tile** (`.kpi`, `.kpi-tile`) — metric display with icon, label (uppercase micro mono), large value, and optional sub-line. Two variants: `.kpi` (icon-left layout) and `.kpi-tile` (stack layout, used on Seeds/Edge/AIDefence). +- **Node card** (`.node`) — cluster member card with mono metadata rows. Key-value pairs in `.node-meta` with dimmed label prefix (`.l` class). +- **Cog card** (`.cog`) — product-catalog card with emoji icon, name, description, category chips, and a "Get" pill button. Hover lifts 2px with primary glow border. +- **Pick card** (`.pick-card`) — horizontal-scroll featured card (220px fixed width), snap-scroll container. Smaller emoji + name + category + pill CTA. +- **Category tile small** (`.cat-tile-sm`) — 180px min-width grid item, emoji + name + count. +- **Category tile large** (`.cat-tile-big`) — 16:9 aspect-ratio card, full-bleed with gradient per category. +- **Nav tile** (`.nav-tile`) — dashboard home navigation card with icon square, title, description, and a chevron arrow that translates +2px on hover. +- **Architecture action card** (`.arch-card`, `.arch-action-card`) — setup wizard launcher cards on the dashboard. + +### Status & feedback + +- **Badge** (`.badge`) — pill with 1px border, 11px mono text. Variants: `role-master` (teal), `role-worker` (green), `online` (green), `offline` (red), `unknown` (muted), `paired` (teal), `unpaired` (amber), `danger` (red). +- **Dot** (`.dot`) — 8px circle status indicator. `.up` glows green with box-shadow, `.down` is red, default is muted gray. +- **Hero dot** (`.hero-dot`) — 7px circle in the dashboard hero status row. Same three states: `.ok` (green glow), `.warn` (amber glow), `.down` (red glow). +- **Op-pill** (`.op-pill`) — "operational status" pill with colored dot inside. Used in dashboard architecture hub. +- **AI pill / status chip** (`.pill` on AIDefence, `.md-badge` in cluster) — inline classification badge at 0.68rem. States: `.ok`, `.warn`, `.bad`. +- **Chip** (`.chip`) — tiny category/difficulty label, all-caps, 0.5625rem, pill-shaped. Category-colored variants (`.cat-ai`, `.cat-health`, `.cat-security`, etc.) each get a hue-appropriate 15% opacity background. + +### Actions + +- **Button** (`.btn`) — 0.5rem × 0.875rem padding, 0.4rem radius, secondary fill. Variants: `.primary` (filled teal, 600 weight, box-shadow), `.outline` (transparent fill), `.danger` (red tint), `.sm` (compact). +- **Hero button** (`.hero-btn`) — slightly larger, display-font, 0.9rem padding, glass-effect dark fill. `.primary` variant uses the green accent gradient. +- **Pill CTA** (`.get`, `.pget`) — full pill-radius (9999px), primary-tint background at rest, fills solid on hover. Used on cog cards and pick cards. +- **Gear button** (`.gear-btn`) — icon-only square button, transparent at rest, border appears on hover. +- **Context menu** (`.ctx-menu`) — dark card dropdown (min-width 180px), each item is a full-width button with secondary hover fill. +- **Copy button** (`.copy-btn`) — positioned absolute in `.copy-row`, 0.7rem opacity at rest, `.copied` state turns green/accent. + +### Forms & inputs + +- **Input** — all ``, `