Product Requirements Document & Implementation Plan · Web (PWA) + iOS
Your Hermes install is a fully-featured agent with a fragmented surface area:
hermes dashboard) — a web UI, but it embeds the terminal
(a PTY running hermes --tui). It's a developer console, not a messaging app..env, memory, toolsets, cron). They are invisible in every UI.There is no place where you can see "these are my agents, these are the threads, this is what ran overnight." Every capability you have is one CLI flag away, and none of it is legible at a glance or reachable from a phone.
| Project | What it proves | What we take / skip |
|---|---|---|
| OpenMausBot Apache-2.0 · 3.9k★ · TS/Kotlin/Swift · v0.1.91 |
A real open-source Grok-Bot analogue. Every sidebar entry is a distinct agent with its own
personality, model, memory thread and computer. Approval cards for risky actions. Channels per
context. Team packages as portable Markdown. BYO engines via ACP and
OpenAI-compatible endpoints. iOS + Android apps. Harness server on 127.0.0.1. |
Take: the information architecture (agents-as-contacts, channels, approval cards,
roster metadata), the ACP integration insight, and the licensing model to study. Skip: re-implementing an agent runtime, Composio, the VM/desktop sandboxing, and the Electron shell. |
| Vellum Assistant MIT · 1.4k★ · iOS/Web/desktop |
Multi-assistant support ("vellum ps — view running assistants", commands take an
assistant ID), device pairing over a tunnel, an SSE event stream as the
documented API surface, 8 memory types, SOUL.md identity files, proactivity loop,
sandbox + permission tiers. |
Take: the multi-assistant account model, pairing UX, event-stream-first API
design, permission tiers. Skip: their runtime entirely — notably, Vellum's own README lists Hermes Agent as one of the things it replaces, confirming the space is real. |
Verified from the repositories' READMEs and directory listings on 1 Oct 2026. No claim above is inferred from marketing copy alone.
A messaging app where every contact is one of your Hermes agents, every channel is a project workspace, and your entire agent fleet is one tap away on your phone.
| Persona | Need | Feature |
|---|---|---|
| Marc, on the couch | Check what ran overnight, steer it | Activity feed, push notifications, resume-thread |
| Marc, in a meeting | Fire off a task, approve a risky action | Approval cards inline in chat, one-tap Allow/Deny |
| Marc, at his desk | Work a project with a focused agent | Channels bound to a profile + working folder + shared instructions |
| Marc, planning | See the whole fleet | Agent roster: model, tools, skills, memory, cost, last-seen |
This is the load-bearing section. The entire plan rests on one verified finding: Hermes already exposes a complete, typed, documented agent protocol over WebSocket, and the same protocol the Electron desktop app uses. We are a third client of that protocol.
ui-tui (Ink) ──stdio JSON-RPC──┐
apps/desktop (Electron) ──WS────┼──► tui_gateway/server.py ──► AIAgent + tools + sessions
Mocchi (our client) ──WS────┘
ws://<host>/api/ws
apps/shared/src/gateway-contract.openrpc.json), each with Pydantic
Params/Result models declared in tui_gateway/contracts/.approval,
clarify, sudo, secret, vault.*, connection.
This is exactly the approval-card mechanism OpenMausBot hand-builds — we get it from the protocol.session.resume + session.events.since +
open_requests(sid) re-render still-open questions after a dropped socket. Essential for mobile.session.create,
session.list, session.resume, session.history,
session.branch, session.interrupt, session.steer,
session.undo, session.compress, session.usage, session.title.agents.list, session.* carries a
profile field, and profiles are bound per activity by a contextvar override.24 routers mounted by hermes_cli/web_server.py, ~200 endpoints, same session token:
| Prefix | What our UI consumes it for |
|---|---|
/api/profiles/* | Agent roster: create, describe, model, SOUL, export, desktop-overlay |
/api/skills, /api/skills/content, /api/skills/toggle | Skill library browser + per-agent toggles |
/api/cron/jobs, /api/cron/blueprints, /api/cron/fire | Automations screen; run-now; blueprints |
/api/events | Activity feed / system event stream |
/api/files/*, /api/fs/* | File browser + viewer + upload from phone |
/api/tools/toolsets/*, /api/tools/terminal/backend | Per-agent tools & terminal backend config |
/api/model/*, /api/providers/* | Model switcher, provider/OAuth onboarding |
/api/audio/* (speak, transcribe, voice-live) | Talk mode — see §5.3 |
/api/pairing/* | Device pairing for the phone |
/api/ops/doctor, /api/status, /api/system/stats | Health & usage |
gateway/platforms/api_server.py serves /v1/chat/completions,
/v1/responses, /v1/models, /api/sessions, /api/runs,
/api/jobs behind API_SERVER_KEY — and under
gateway.multiplex_profiles, secondary profiles live at /p/<profile>/….
That means a single key + URL prefix can address any agent in the fleet from any SDK.
web_server_chat.py): a browser-minted ticket
(single-use, 30 s TTL) passed as ?ticket= or as the
hermes-gateway-ticket.<t> WebSocket subprotocol. A device with a long-lived
credential is not a browser, so Phase 1 adds a small device-token path
(see §7 risk R2)._SESSION_TOKEN (bearer).bookmarks.marcadrian.com. No new infra.openbot/
├── packages/
│ ├── protocol/ # generated OpenAPI/TS types from the Hermes contract (npm script)
│ ├── client/ # transport-agnostic agent client (WS + reconnect + replay + tickets)
│ └── ui/ # shared design system: roster, thread, approval card, channel
├── apps/
│ ├── web/ # React + Vite PWA (served at mocchi.yourdomain)
│ └── ios/ # SwiftUI, shares the OpenAPI contract
└── plugins/
└── mocchi-bridge/ # out-of-tree Hermes plugin: device tokens + optional push
Monorepo, pnpm. One protocol package, two clients — the web app and the iOS app can never disagree about the wire.
Every profile is an "agent". Data from /api/profiles + agents.list + session.most_recent.
| Field | Source |
|---|---|
| Name, avatar, accent colour, emoji | /api/profiles/{name}/description, SOUL.md |
| Model + provider | /api/profiles/{name}/model |
| Last active, unread, pinned | session.most_recent + local prefs |
| Unread badge | Derived from events since last view |
| Tools/skills chips | agents.list, /api/skills |
acceptance Cold start shows the fleet in <1 s from a warm cache,
with a skeleton while /api/profiles resolves. Pull-to-refresh. New-agent FAB deep-links to
profile creation on the desktop dashboard for v1 (creating a profile from mobile is P2).
Styled messages (not a raw terminal): markdown, code fences, collapsible tool-call cards, streaming token rendering, and a live activity strip (tool name, elapsed, running/done/failed).
session.steer / the composer method; interrupt via session.interrupt.complete.slash and
complete.path — i.e. /skills, /model, /cron
all work from the phone./api/chat/image-upload and /api/files/upload.acceptance Reconnect mid-turn: on socket drop and reconnect,
session.resume + session.events.since restores the full transcript and any
in-flight turn, with no duplicated or lost text.
The single most valuable feature, and the cheapest: the protocol already pushes
approval / sudo / secret / clarify / vault.*
requests to the client and blocks the agent thread until answered.
client.capabilities {server_requests: true} on connect or the server fails fast.acceptance A tool asks for approval while the app is backgrounded → push arrives → tapping it opens the thread → one tap answers → the agent unblocks.
A channel = profile + working folder + shared instructions + a thread set. Backed by Hermes chat
workspaces (/api/chat/workspaces) and session.workspace.move. v1 ships
Work / Personal / one channel per real project; the roster inside a channel is a set of profiles.
Per agent: model, active toolsets (/api/tools/toolsets), skills on/off, memory provider,
cron jobs (/api/cron/jobs), token spend (session.usage,
/api/analytics/usage), and a "run this agent now" button. Read-mostly in v1;
mutations are limited to model switch and skill toggle, both of which are already safe endpoints.
Cron as a first-class screen: list, pause/resume, trigger now (/api/cron/fire),
delivery targets (/api/cron/delivery-targets), and blueprint instantiation. Combined with
F3, this is the "it worked while I slept" loop.
/api/fs/list + /api/fs/read-text + /api/files/upload +
/api/git/status. Read a diff on the train, push a fix. Deliberately scoped: review, not
a full IDE.
/api/audio/transcribe for input and /api/audio/speak /
speak-stream for playback of replies. /api/audio/voice-live/session may
support a live mode later.
React 19 + Vite, installable, offline shell. Design language: dark-glass panels, subtle borders, pill-shaped bottom dock — matching the Mocchi bookmarks app you already have, so the fleet feels like the same product family.
| Stack | SwiftUI + URLSessionWebSocketTask, iOS 17+. No third-party chat SDK. |
|---|---|
| Protocol | Generated Swift models from gateway-contract.openrpc.json in the same
protocol package pipeline. |
| Background | v1: foreground + push wake. BGAppRefreshTask for polling the
activity feed. Live-streamed turns while foregrounded only. |
| Distribution | TestFlight first. No App Store dependency; this is a private tool. |
Browser WS auth uses a 30-second single-use ticket minted in-page. A native app has no browser page,
so it needs a long-lived credential. Rather than touching Hermes core, add an
out-of-tree plugin (~/.hermes/plugins/mocchi-bridge) exposing a narrow
device-token endpoint — this is rung 4 on Hermes' own Footprint Ladder, and the plugin API is the
documented, stable extension point.
Flow: desktop dashboard shows a QR with a short code → phone submits it once → device token stored in
the iOS Keychain → all subsequent calls bearer-auth. Revoke via /api/pairing/revoke.
APNs token registered by the device; a Hermes plugin subscribes to agent events and dispatches. Push is only sent for genuinely interrupt-worthy events: approval requests, completed long runs, and cron results — never for ordinary chatter, or you'll mute the app within a week.
hermes update first (2106 commits behind) so we build against current contracts.openbot/ pnpm monorepo; add protocol codegen emitting
TS + Swift from the live contract file.ws://localhost:8080/api/ws with a minted ticket,
call gateway.ready → session.create → send → stream. If this
script works, everything after it is UI.packages/client: WS transport, ticket auth, reconnect w/ backoff,
session.resume + replay, capability handshake.complete.slash.Exit criterion: from your phone's browser you can message any profile, watch it work, and approve its actions.
mocchi-bridge plugin: device tokens, pairing, APNs dispatch.bot_relay.roster / bot_relay.deliver already exist upstream).hermes profile create --clone.| Phase | Duration | Depends on | Value delivered |
|---|---|---|---|
| 0 — Foundation | 3–5 days | hermes update | Wire proven; project scaffolded |
| 1 — Web MVP | ~2 wks | Phase 0 | The whole product, in a browser |
| 2 — Fleet | ~2 wks | Phase 1 | Workspace visibility, cron, files |
| 3 — iOS | ~3 wks | Phase 2 + Apple dev account | Real mobile product with push |
| 4 — Polish | ~1–2 wks | Phase 3 | Voice, teams, theming |
Strongly recommended: stop after Phase 1 and use it for two weeks before committing to iOS. Phase 1 delivers ~80% of the value at ~35% of the cost, and the feedback will make Phases 2–3 much sharper. Committing to native before the web client has proven the IA is the classic mistake.
| # | Risk | Severity / mitigation |
|---|---|---|
| R1 | Upstream churn. 251 methods and a contract that moves; you're 2106 commits behind today, so this accelerates. | High Consume the generated contract, never hand-write types. Keep a Phase-0 integration test that fails loudly on drift. Rebuild contracts as a scheduled job; our breaking-change surface is small because we use ~40 of 251 methods. |
| R2 | Native auth. No browser = no ticket mint. | Med Out-of-tree plugin for device tokens. Worst case, fall back
to the web client in Safari with add-to-home-screen — which is a perfectly good
v1 and is exactly what Phase 1 delivers. |
| R3 | Scope creep into a second agent runtime. | Med Hard non-goal (§4.9). Any PR that reimplements tools, memory or skills is rejected in review. We are a client, full stop. |
| R4 | Security exposure. Your terminal, browser and secrets are one tap away. | High HTTPS only. Device tokens revocable and rotatable. Every approval card shows the exact command/path. Consider a separate read-mostly profile for phone access — profiles already isolate memory and toolsets, so a "viewer" agent is free. |
| R5 | Licensing. OpenMausBot is Apache-2.0 with a source-available
enterprise/ carve-out; Vellum is MIT. |
Low We copy ideas, not code. If any code is ported, Apache-2.0 requires attribution + NOTICE. Hermes itself is the actual dependency — license review happens before any port. This is a feature, not a bug: not forking Hermes is what keeps us safe. |
| R6 | Apple distribution. TestFlight annual limits, review latency. | Low TestFlight is sufficient for a private tool. No store dependency. Web/PWA remains the primary surface if this ever bites. |
| R7 | Notification fatigue. Push on everything → app muted. | Low Interrupt-only policy (§5.4), enforced in the bridge plugin with an explicit allowlist. |
hermes update followed by the Phase-0 test is the
only work required to re-validate the client.hermes-agent.bot_relay.roster/deliver already support agent-to-agent messaging;
surfacing it is cheap but adds product surface — defer until the core is used daily.
Sources. Hermes integration surfaces verified by direct inspection of the installed
source at /home/ubuntu/.hermes/hermes-agent (v0.21.3, commit 4d14aaf) on 1 Oct 2026:
tui_gateway/contracts/, apps/shared/src/gateway-contract.openrpc.json,
hermes_cli/web_routers/, hermes_cli/web_server*.py,
gateway/platforms/api_server.py, tui_gateway/AGENTS.md.
Reference projects read from their public repositories on 1 Oct 2026.
No unverified claim in this document is presented as fact.