A heads-up display for a team's coding agents: one page showing every agent's goal, its current tasks, and the decisions it wants a human to look at. Agents report over MCP. A decision model (Jev) reviews what they post: it flags statements that are unclear, duplicated, overlapping or outdated, and tells the agent, which decides what to do. The model also ranks what the display shows, and files each task under the goal it serves, so the board lists every goal with its tasks under it. Teammates can answer decisions and comment on goals and tasks right on the board; the agent gets them with its next Huddy call.
- Design:
docs/DESIGN.md - Stack: Gleam on Erlang/OTP 29, mist + wisp, one JSON state file, no frontend framework (server-rendered HTML, zone swaps over SSE, ~150 lines of vanilla JS; documents are rendered in the browser with marked + DOMPurify).
Requirements: Erlang/OTP 29, Gleam 1.19, rebar3 (see .tool-versions).
cp huddy.example.json huddy.json # edit: team, base_url, allowlist, email, decision model
export HUDDY_SECRET_KEY=$(openssl rand -base64 48)
export TYPESAFE_API_KEY=... # or OpenRouter: see deploy/huddy.compose.json; or "enabled": false
gleam run -- --config huddy.jsonFor local development, use "base_url": "http://localhost:8080" and
"email": {"from": "...", "transport": "log"}. Login codes are then printed to
the log instead of being sent. Login codes are the only e-mail Huddy sends.
cp deploy/.env.example .env # HUDDY_SECRET_KEY, DECISION_MODEL_API_KEY (OpenRouter), allowed domain
docker compose up --buildHuddy runs at http://localhost:8080, and Mailpit (which catches the sign-in
codes) at http://localhost:8025. The compose config is
deploy/huddy.compose.json; it uses Jev through
OpenRouter (typesafe/jev-1.13).
docker build -t huddy .
docker run -p 8080:8080 \
-v huddy-data:/var/lib/huddy \
-v $PWD/huddy.json:/etc/huddy/huddy.json:ro \
-e HUDDY_SECRET_KEY -e TYPESAFE_API_KEY -e SMTP_PASSWORD \
huddyTo keep the state in S3 instead of a volume, see State in S3.
Put Huddy behind a TLS-terminating reverse proxy. List the proxy in
server.trusted_proxies so rate limits and the last-use IP recorded for MCP
clients see real client IPs (taken from X-Forwarded-For), and have the proxy
redact /mcp/k/… paths from its access logs (they contain secrets). The state
volume must be a local POSIX filesystem (or use S3).
Huddy honours HTTPS_PROXY/NO_PROXY for its outbound calls (decision model,
client metadata). For builds without GitHub access, or behind a TLS-intercepting
proxy, see the comments at the top of the Dockerfile.
Set storage.path to an S3 URL. Backups then go to the backups/ prefix next
to it (or set storage.backup_dir, local or s3://):
"storage": {
"path": "s3://acme-huddy/prod/huddy.json",
"s3": { "region": "eu-west-1" }
}Document files go to the documents/ prefix next to the state object (or set
storage.documents_dir, local or s3://).
Credentials come from AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and
AWS_SESSION_TOKEN, or from storage.s3.access_key_id etc. Region defaults to
AWS_REGION. For MinIO, R2 and other S3-compatible stores, add
"endpoint": "https://minio.internal:9000" (path-style addressing is then the
default; override with path_style). Huddy needs s3:GetObject,
s3:PutObject, s3:DeleteObject and s3:ListBucket. Details:
DESIGN.md §5.7.
Agents talk to https://<your huddy>/mcp (MCP Streamable HTTP; protocol
versions 2026-07-28 and 2025-11-25).
-
OAuth: point the client at the URL and it signs in through your browser.
claude mcp add --transport http huddy https://huddy.acme.dev/mcp
For Codex, add this to
~/.codex/config.toml:[mcp_servers.huddy] url = "https://huddy.acme.dev/mcp"
-
Secret URL: create one in Settings (optionally pinned to an agent name and project, and optionally read-only), then use it as the server URL.
The server's instructions teach agents the protocol. They set goals (one per outcome), keep a few tasks open, post decisions when unsure (marked blocking if they have stopped), act on review findings or dismiss them with a reason, act on teammates' comments, and close items when done.
On the board, anyone on the team can answer a decision (one click per option the agent listed, plus an optional reply) and comment on a goal or task. Any teammate's answer counts, not only the owner's. Closed items can't be commented on.
- Comments reach the agent that owns the item at the top of its next Huddy
tool result. An agent that has stopped to wait calls
huddy_wait, which returns as soon as a comment or finding arrives (or aftertimeout_s, default 50 s); its owner can still answer in the session. - Comments, and review findings, repeat in every result until the agent
acknowledges them with
huddy_ack {ids: ["c7", "f88"]}, so a lost or truncated response loses nothing.huddy_inboxlists everything unacknowledged. - Agents reply in an item's thread with
huddy_update {id, comment}, which can also close the item in the same call. - Each comment on the board shows whether it is still waiting, has been sent but not acknowledged, or has been acknowledged.
Limits: limits.comment_max_chars (default 10000), limits.comments_per_item
(100) and limits.comments_per_user_per_minute (12). Details:
DESIGN.md §17.
Agents can share longer Markdown (designs, reports, diagrams) as documents.
A document is a zip archive with an index.md at its root plus the images it
links to (); plain Markdown works too. Documents
are global: every agent and everyone on the team can list and read all of them,
and any agent can replace or delete any of them.
huddy_doc_uploadreturns a one-time URL the agent uploads the document to:(cd my-doc && zip -r - .) | curl --data-binary @- -H 'Content-Type: application/zip' <url>(or a single.mdfile). The file never passes through the model.iduploads a new version of an existing document.- Document ids look like
12-design-notes: a number and a slug, which the agent picks withslugwhen it creates the document (else it comes from the title). The number alone (12) works too. huddy_doc_deletedeletes one.huddy_doc_downloadreturns a one-time link to the document's zip, exactly as uploaded, forcurl -o 12-design-notes.zip <link>.- Replacing or deleting needs the document's current
revision(a counter that goes up with every version), so an agent never overwrites a change it hasn't seen. Changes to one document run one at a time. huddy_doc_listlists them;huddy_doc_readreturns a document's rawindex.md(revision, files and links are in the structured result).- Goals, tasks and decisions link documents with
docs: ["12-design-notes"]; the board shows the links. /docslists them for humans;/docs/12-design-notesrenders the Markdown in the browser. Humans can delete a document there.- Documents can be shared publicly, by a human from the document's
page or by an agent with
huddy_doc_share: anyone with the link (/shared/<token>) can read the document, and its later versions, without signing in, until someone stops sharing it.huddy_doc_readreturns the link aspublic_url. Set"documents": {"public_links": false}to turn this off. Details: DESIGN.md §16.7.
Each version is stored as one zip, exactly as uploaded, on local disk or in S3
next to the state file (storage.documents_dir); the limit is limits.document_max_mb (default 20 MB
unzipped, 200 files). Details: DESIGN.md §16.
Huddy is a progressive web app: Chrome, Edge and Android offer to install it
from the address bar or menu, and Safari on iOS and macOS adds it with Add to
Home Screen / Add to Dock. It then opens in its own window, and its
notifications come from the app. The board itself always comes from the
server; when the server can't be reached the app shows an offline page
instead. Browsers only install sites served over HTTPS (or localhost).
One JSON file. Every field is shown in
huddy.example.json and explained in
DESIGN.md §3. Any string
value can be {"$env": "NAME"}.
gleam test # unit + end-to-end tests (boots the app on local ports)
gleam format --check src test
gleam build --warnings-as-errorsThe bundled fonts (Excalifont, Nunito) are licensed under the SIL OFL 1.1, see
priv/static/FONTS-LICENSE.txt. The bundled
Markdown renderer (marked) and sanitizer (DOMPurify) for documents are loaded
from jsDelivr, pinned by version and Subresource Integrity hash.