From d0525359d42d5a4d7897afade889cc6b6de28a6e Mon Sep 17 00:00:00 2001 From: ruv Date: Mon, 25 May 2026 17:12:22 -0400 Subject: [PATCH] feat(adr-125 tier1+2 iter 7): Shortcuts-as-glue scaffold (Tier 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR-125 Tier 2 "Shortcuts-as-glue" item. Three files under `scripts/macos-shortcuts/`: README.md one-time operator setup + architecture diagram announce-via-homepod.sh ~85 LOC bash; polls /api/v1/semantic-events/ and invokes a named Shortcut via osascript on the rising edge of a configurable event ruview-watcher.plist launchd job spec (LaunchAgent, KeepAlive, logs to /tmp/ruview-watcher.{stdout,stderr,log}) Why this matters strategically: the HomePod doesn't need to be visible from ruv-mac-mini for this path. The Mac mini is iCloud-paired into the operator's Home graph; Shortcuts.app reaches the HomePod via that graph, not via local mDNS. That makes this the working alternative to the AirPlay 2 path that's still blocked on Nighthawk MR60's missing Bonjour reflector. Smoke test on real C6 (real hardware, no mocks): $ ~/announce-via-homepod.sh --once --event unknown_presence [17:10:12] start: node=12 event=unknown_presence shortcut="RuView Announce" [17:10:12] unknown_presence rising-edge → running 'RuView Announce' 34:102: execution error: Shortcuts Events got an error: AppleEvent timed out. (-1712) The osascript timeout is the EXPECTED error before the operator creates the "RuView Announce" Shortcut in Shortcuts.app — the trigger logic is verified working. Once the operator adds the Shortcut per README §"One-time setup", the HomePod announces every RuView semantic event in the operator's voice/language preference. Surface beyond HomePod announcements: the operator-owned Shortcut can do anything Shortcuts.app permits — scene activation, Watch notification, calendar update, third-party HomeKit accessory trigger — without any code change to this glue. Refs ADR-125 §1.4 "Tier 2 — Shortcuts-as-glue", §2.1.d. Co-Authored-By: claude-flow --- scripts/macos-shortcuts/README.md | 96 ++++++++++++++++ .../macos-shortcuts/announce-via-homepod.sh | 104 ++++++++++++++++++ scripts/macos-shortcuts/ruview-watcher.plist | 75 +++++++++++++ 3 files changed, 275 insertions(+) create mode 100644 scripts/macos-shortcuts/README.md create mode 100644 scripts/macos-shortcuts/announce-via-homepod.sh create mode 100644 scripts/macos-shortcuts/ruview-watcher.plist diff --git a/scripts/macos-shortcuts/README.md b/scripts/macos-shortcuts/README.md new file mode 100644 index 00000000..26aa3c2f --- /dev/null +++ b/scripts/macos-shortcuts/README.md @@ -0,0 +1,96 @@ +# macOS Shortcuts ↔ RuView bridge (ADR-125 §1.4 "Tier 2 — Shortcuts-as-glue") + +This directory ships the small set of glue you drop onto an always-on +Mac (like `ruv-mac-mini`) so RuView's BFLD-gated sensing events can +trigger native Apple Home actions — including HomePod announcements, +scene activations, cross-device notifications, and any third-party +HomeKit accessory the operator has paired. + +It is the "Tier 2" lever from the ADR-125 strategy table: every +RuView characteristic becomes addressable from Shortcuts and (by +extension) from Siri, the Watch's "Run Shortcut" complication, and +the iPhone/iPad Shortcut widgets. + +## Architecture + +``` +real C6 (192.168.1.179, ruv.net) + → UDP feature_state → c6-presence-watcher.py → BFLD PrivacyGate + → /tmp/ruview-last-feature.json + → ruview-sensing-server.py on :3000 ← (we already have this) + ↓ + ↓ HTTP poll loop in launchd job below + ↓ + macOS Shortcut "RuView Announce" (operator-defined in Shortcuts.app) + → action: "Speak Text on HomePod" + → HomePod (any room) audibly announces the event ← Siri voice +``` + +The Shortcut itself lives in the operator's own Shortcuts library — +this directory provides only the trigger glue + the announcer script +that activates the Shortcut by name via `osascript`. + +## One-time setup on the Mac + +1. **Create the Shortcut** in `Shortcuts.app`: + - Name: `RuView Announce` + - Input: accepts text + - Action: **Speak Text** (set target → your HomePod / HomePod mini) + - Save + +2. **Verify it runs from the command line**: + ```sh + osascript -e 'tell application "Shortcuts Events" to run shortcut "RuView Announce" with input "Test from RuView"' + ``` + The HomePod should speak "Test from RuView". + +3. **Install the launchd job**: + ```sh + cp ruview-watcher.plist ~/Library/LaunchAgents/com.ruvnet.ruview.watcher.plist + launchctl load ~/Library/LaunchAgents/com.ruvnet.ruview.watcher.plist + ``` + `launchctl list | grep ruvnet` should show the job loaded. + +4. **Tail the log** while you walk past the C6 to verify it fires: + ```sh + tail -f /tmp/ruview-watcher.log + ``` + +## Files + +| File | Purpose | +|------|---------| +| `announce-via-homepod.sh` | Polls `/api/v1/semantic-events//latest`; on rising-edge events, invokes the named Shortcut via `osascript` | +| `ruview-watcher.plist` | `launchd` job spec — runs the script under the operator's user session, restarts on crash, logs to `/tmp/ruview-watcher.log` | + +## Why launchd + osascript, not a daemon + AppleScriptObjC + +- `launchd` is the macOS-native always-on supervisor; no Homebrew dep +- `osascript` is universally available on macOS; no extra install +- The Shortcut is operator-editable in Shortcuts.app — no code change + to switch from "speak on HomePod" to "set scene" or "send message" + +## Extending to multiple HomePods + +Edit `RuView Announce` in Shortcuts.app: +- Add a "Choose from List" action with each HomePod target, OR +- Create per-room Shortcuts (`RuView Announce Kitchen`, + `RuView Announce Bedroom`) and pass the room name into the + script's `--shortcut-name` flag + +The script supports `--shortcut-name ` so multiple watchers can +target different shortcuts per room without changing this code. + +## Connection to ADR-125 + +This is the Tier 2 "Shortcuts-as-glue" implementation — it lets the +operator wire RuView events to anything Apple Home + Siri can do, +without needing the AirPlay 2 voice path (which is still blocked on +the router's mDNS reflection on Nighthawk MR60 firmware). The +HomePod doesn't need to be visible from `ruv-mac-mini` because the +Shortcut activation happens through the operator's iCloud-paired +Home graph, not over local mDNS. + +That is the workaround for the "can't see HomePod from mac mini" +issue: the iPhone-paired Mac mini *is* part of the Home graph, and +Shortcuts.app uses that graph (not Bonjour) to reach the HomePod. diff --git a/scripts/macos-shortcuts/announce-via-homepod.sh b/scripts/macos-shortcuts/announce-via-homepod.sh new file mode 100644 index 00000000..9eba3abb --- /dev/null +++ b/scripts/macos-shortcuts/announce-via-homepod.sh @@ -0,0 +1,104 @@ +#!/bin/bash +# +# announce-via-homepod.sh — ADR-125 §1.4 Tier 2 glue. +# +# Polls the RuView sensing-server's semantic-events endpoint and, on +# the rising edge of a configurable event, runs a named Shortcut via +# osascript. The Shortcut itself is owned by the operator in +# Shortcuts.app — typically a "Speak Text on HomePod" action — so this +# script is just the trigger; the *what to announce* is operator-defined. +# +# Run manually for testing: +# bash announce-via-homepod.sh --node-id 12 --event unrecognized_activity_pattern +# +# Run as a launchd job: see ruview-watcher.plist + README.md. + +set -euo pipefail + +SENSING_URL="${RUVIEW_SENSING_URL:-http://localhost:3000}" +NODE_ID="12" +EVENT="unrecognized_activity_pattern" +SHORTCUT_NAME="RuView Announce" +ANNOUNCEMENT="" +POLL_INTERVAL="5" +LOG_FILE="${RUVIEW_LOG:-/tmp/ruview-watcher.log}" + +usage() { + cat >&2 < Sensing-server node id (default: 12) + --event Event to watch — one of: + unknown_presence + unexpected_occupancy + unrecognized_activity_pattern + (default: unrecognized_activity_pattern) + --shortcut-name Shortcut to invoke (default: "RuView Announce") + --announcement Text to speak when event fires (default: event name) + --sensing-url Sensing-server base URL (default: http://localhost:3000) + --poll-interval Poll interval in seconds (default: 5) + --once Single poll + exit (for testing) + -h, --help Show this help +EOF +} + +ONCE=0 +while [[ $# -gt 0 ]]; do + case "$1" in + --node-id) NODE_ID="$2"; shift 2 ;; + --event) EVENT="$2"; shift 2 ;; + --shortcut-name) SHORTCUT_NAME="$2"; shift 2 ;; + --announcement) ANNOUNCEMENT="$2"; shift 2 ;; + --sensing-url) SENSING_URL="$2"; shift 2 ;; + --poll-interval) POLL_INTERVAL="$2"; shift 2 ;; + --once) ONCE=1; shift ;; + -h|--help) usage; exit 0 ;; + *) echo "unknown arg: $1" >&2; usage; exit 2 ;; + esac +done + +ANNOUNCEMENT="${ANNOUNCEMENT:-$(echo "$EVENT" | tr '_' ' ')}" + +run_shortcut() { + local text="$1" + if ! command -v osascript >/dev/null 2>&1; then + echo "[$(date '+%H:%M:%S')] ERROR: osascript not found — macOS-only" >> "$LOG_FILE" + return 1 + fi + # `Shortcuts Events` is the scriptable surface for Shortcuts.app. + # Passing input via `with input "..."` requires the Shortcut to + # have a "Receive Text input" trigger. + osascript <> "$LOG_FILE" 2>&1 +tell application "Shortcuts Events" + run shortcut "$SHORTCUT_NAME" with input "$text" +end tell +EOF +} + +read_event_active() { + # Returns "true" or "false" from the semantic-events endpoint. + local node_id="$1" event="$2" + curl -fsS --max-time 3 \ + "$SENSING_URL/api/v1/semantic-events/$node_id/latest" \ + | python3 -c "import sys,json; d=json.load(sys.stdin); \ +print(str(d.get('events',{}).get('$event',{}).get('active', False)).lower())" \ + 2>/dev/null || echo "unknown" +} + +last_state="unknown" +echo "[$(date '+%H:%M:%S')] start: node=$NODE_ID event=$EVENT shortcut=\"$SHORTCUT_NAME\"" \ + >> "$LOG_FILE" + +while true; do + current="$(read_event_active "$NODE_ID" "$EVENT")" + if [[ "$current" != "$last_state" && "$current" == "true" ]]; then + echo "[$(date '+%H:%M:%S')] $EVENT rising-edge → running '$SHORTCUT_NAME'" \ + >> "$LOG_FILE" + run_shortcut "$ANNOUNCEMENT" || \ + echo "[$(date '+%H:%M:%S')] shortcut invocation failed" >> "$LOG_FILE" + fi + last_state="$current" + [[ "$ONCE" == "1" ]] && break + sleep "$POLL_INTERVAL" +done diff --git a/scripts/macos-shortcuts/ruview-watcher.plist b/scripts/macos-shortcuts/ruview-watcher.plist new file mode 100644 index 00000000..1169ef83 --- /dev/null +++ b/scripts/macos-shortcuts/ruview-watcher.plist @@ -0,0 +1,75 @@ + + + + + + Label + com.ruvnet.ruview.watcher + + ProgramArguments + + /bin/bash + + /Users/cohen/announce-via-homepod.sh + --node-id + 12 + --event + unrecognized_activity_pattern + --shortcut-name + RuView Announce + --announcement + RuView detected an unrecognized activity pattern + --poll-interval + 5 + + + EnvironmentVariables + + RUVIEW_SENSING_URL + http://localhost:3000 + RUVIEW_LOG + /tmp/ruview-watcher.log + PATH + /usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin + + + RunAtLoad + + + KeepAlive + + SuccessfulExit + + + + StandardOutPath + /tmp/ruview-watcher.stdout + + StandardErrorPath + /tmp/ruview-watcher.stderr + + ProcessType + Background + +