A hardware companion for subctl that borrows the claude-desktop-buddy wire protocol verbatim.
- Ambient observe — at a glance, see what's running across all subctl accounts, current rate-limit posture, recent significant events.
- 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.
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ 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.
Verified against subctl v3.0.1, integration doc dated 2026-05-24.
| 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.*.
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.
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"
}| 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 |
- ✅ 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 replyverb name; pin one path - ⏳ Manual end-to-end:
subctl notify ask-yesno "test" --id BLE-x→ buddy shows prompt → tap A → subctl receives "yes"
- Swap M5StickCPlus library for M5Unified
- Retarget 320×240 landscape + cap-touch input
- Read the
subctlextension field, draw account columns + rate-limit dial - SSE client in bridge replaces the three polls
- Cardputer-only firmware: shows the prompt, lets you type a freeform reply
- Bridge routes free-form to
subctl notifyask-textreply channel
- Bridge supports N peripherals simultaneously, replicates heartbeats to all, accepts replies from any (first answer wins)
- Reply verb name —
subctl notify reply ...is the assumed CLI verb; if it's actuallyanswer/respond/ something else, the fallback HTTP POST path handles it. Locking in name once observed live. /api/notifications/inboxexact shape — bridge accepts both[…]and{items: […]}; will adapt on first call./api/state.accounts[]shape — same flex handling; refine types insubctl.tsonce we see real data.- 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.