Skip to content

Latest commit

 

History

History
142 lines (107 loc) · 5.93 KB

File metadata and controls

142 lines (107 loc) · 5.93 KB

subctl-buddy — design

A hardware companion for subctl that borrows the claude-desktop-buddy wire protocol verbatim.

Goal

  1. Ambient observe — at a glance, see what's running across all subctl accounts, current rate-limit posture, recent significant events.
  2. Operate in the loop — when subctl needs an operator yes/no (currently delivered via Telegram), the buddy can also ask it and route the decision back. Tap-to-approve from your desk.

Architecture

┌─────────────┐         ┌──────────────┐         ┌──────────────┐
│   subctl    │  poll   │   bridge     │   BLE   │   device(s)  │
│  CLI/state  │ ───────▶│   daemon     │ ───────▶│  M5 buddies  │
│  HTTP/8787  │         │ (Bun/noble)  │         │              │
│             │◀────────│              │◀────────│              │
│  reply API  │ apply   │              │ approve │              │
└─────────────┘         └──────────────┘         └──────────────┘

The bridge daemon is the only new long-running process. The M5 devices use the existing claude-desktop-buddy firmware unchanged for Phase 1 — they're agnostic to whether the JSON heartbeat came from Claude desktop or subctl.

subctl integration surface (Phase 1)

Verified against subctl v3.0.1, integration doc dated 2026-05-24.

Reads

Endpoint Cadence What we pull
subctl status --json 1 s master health, subscribers, prompt_in_flight, active_profile, transcript_msgs
GET http://127.0.0.1:8787/api/state 5 s accounts table, teams, memory tier counts
GET http://127.0.0.1:8787/api/notifications/inbox 2 s open + answered asks

The dashboard's SSE feed at /api/events would replace all three polls; deferred to Phase 2 to ship Phase 1 without an SSE client.

Key naming discipline: subctl v3.x renamed many internal modules to "Evy" but deliberately preserved master.* keys in the JSON wire surface for backwards compatibility. The bridge reads master.* keys as-is and does not alias to evy.*.

Writes (reply to operator-yes/no)

When the buddy is the answerer (vs Telegram):

subctl notify reply --id BLE-<uuid> --answer yes

If the CLI verb name turns out to be different (answer/respond/etc), the bridge falls back to POST /api/notifications/inbox?id=… with {answer: "yes"}. Both paths should land in the same inbox.

The --id namespace is prefixed with BLE- so subctl-side queries can filter buddy-originated questions if needed later.

Wire protocol mapping

Reused fields from REFERENCE.md (Claude desktop buddy):

subctl source buddy field Notes
master.subscribers total active workers across accounts
sum of state.accounts[].running running falls back to prompt_in_flight ? 1 : 0
count of open inbox items waiting unanswered ask-* questions
active_profile + head of inbox msg e.g. "chat · 2 run" or "ask: deploy?"
(Phase 2 — SSE) entries[] recent significant events
master.transcript_msgs tokens / tokens_today drives level-up confetti
head of inbox prompt {id, tool, hint} tool = "ask-yesno", hint = question

New extension field (firmware fork in Phase 2 will read, older firmware ignores):

"subctl": {
  "accounts": [{"name": "primary", "running": 2, "rate_tier": "ok"}, ...],
  "rate_tier_worst": "warn",
  "prompt_in_flight": false,
  "active_profile": "chat"
}

Per-board roles

Board Role
CoreS3 (DIN-mounted, 9V) Primary operator — touch-approve, account columns, rate-limit dial
StickC Plus Ambient desk indicator — current state at a glance, LED alert on prompt
AtomS3R Wearable / pocket alert — minimal UI
Cardputer Remote console — type a freeform message back when away from the Mac
StickC Plus 2 Spare ambient indicator
StampS3 Parked

Phases

Phase 1 — bridge daemon, existing firmware unchanged

  • ✅ Repo scaffold (bridge/, docs/, firmware/)
  • ✅ Bridge poll loop, BLE central, heartbeat builder, reply path
  • ⏳ First connect + smoke test against StickC Plus
  • ⏳ Verify the subctl notify reply verb name; pin one path
  • ⏳ Manual end-to-end: subctl notify ask-yesno "test" --id BLE-x → buddy shows prompt → tap A → subctl receives "yes"

Phase 2 — CoreS3 firmware fork

  • Swap M5StickCPlus library for M5Unified
  • Retarget 320×240 landscape + cap-touch input
  • Read the subctl extension field, draw account columns + rate-limit dial
  • SSE client in bridge replaces the three polls

Phase 3 — Cardputer remote replier

  • Cardputer-only firmware: shows the prompt, lets you type a freeform reply
  • Bridge routes free-form to subctl notify ask-text reply channel

Phase 4 — multi-buddy fanout

  • Bridge supports N peripherals simultaneously, replicates heartbeats to all, accepts replies from any (first answer wins)

Open questions for first-run validation

  1. Reply verb namesubctl notify reply ... is the assumed CLI verb; if it's actually answer / respond / something else, the fallback HTTP POST path handles it. Locking in name once observed live.
  2. /api/notifications/inbox exact shape — bridge accepts both […] and {items: […]}; will adapt on first call.
  3. /api/state.accounts[] shape — same flex handling; refine types in subctl.ts once we see real data.
  4. macOS Bluetooth permission — the first BLE call will prompt for permission for the terminal/Bun binary. Re-running from a different shell may re-prompt.