---
title: ADR 0005 — Per-manager Telegram bots and `teamctl bot setup`
---

- Status: **accepted** (implemented in v0.6.0)
- Date: 2026-05-02
- Author: Alireza
- Reviewers:

## Context

The shipping Telegram adapter (v0.2.0 → v0.5.1) is one global bot per team. To talk to a manager you DM the bot with `/dm <project>:<role>
<text>`. To set it up you hand-write a top-level `interfaces:` block
in `team-compose.yaml`, paste a token into `.env`, find your chat id via `@userinfobot` (or run the bot once and read the `/start` echo introduced by v0.2.9), and start `team-bot` yourself.

Three friction points keep biting:

1. **`/dm role text` is not "messaging".** It feels like an IRC incantation. Operators forget the role name, mistype the project prefix, and cannot use Telegram's drafts or reply threads in a meaningful way.
2. **Setup is a documentation walk.** The Telegram guide is six steps spread across BotFather, `.env`, YAML, and a separate `team-bot` process. New users either skip it or get stuck partway.
3. **One bot is a routing puzzle.** With three managers (PM, eng_lead, marketing) you see every approval and every reply on the same thread. The v0.2.7 per-manager scoping (`--manager`) helped, but only if you ran multiple bots — and there was no command to set any of them up.

## Decision

1. **One Telegram bot per manager.** Each manager that wants Telegram carries its own `interfaces.telegram` block on the manager definition itself — *not* in the top-level `interfaces:` array. The presence of the block is the only signal needed; the legacy `telegram_inbox: true` and `reports_to_user: true` flags are both retired (the latter was already functionally inert — `reply_to_user` gates on `is_manager`, not the flag).

   ```yaml
   managers:
     pm:
       runtime: claude-code
       interfaces:
         telegram:
           bot_token_env: TEAMCTL_TG_PM_TOKEN
           chat_ids_env: TEAMCTL_TG_PM_CHATS
   ```

2. **`teamctl bot setup` is the wizard.** It enumerates every manager in compose, walks BotFather → token → `/start` → chat id for each, prompts for env-var names (with sensible defaults), persists the values into `.team/.env`, and upserts the `interfaces.telegram` block into `projects/<id>.yaml`. The wizard is **resumable** — already-configured managers skip silently, partials only ask for the missing piece, and YAML-fixed env-var names are reused without re-prompting. A positional `[manager]` argument scopes to one manager (`teamctl bot setup news:head_editor`).

3. **`teamctl up` spawns one `team-bot` per manager-with-`interfaces.telegram`.** Each runs in its own tmux session named `<prefix>bot-<project>-<role>` with `--manager <project>:<role>` so approvals and replies route to exactly that bot. `teamctl down` stops them.

4. **DMing the bot Just Works.** `team-bot` interprets any plain text on a manager-scoped bot as a message to that manager (not as an IRC command). `/dm`, `/pending`, and inline approval buttons stay as escape hatches.

5. **The top-level `interfaces:` array stays for non-Telegram adapters** (Discord, iMessage, CLI, webhook) since those don't fit the 1:1-with-manager model as cleanly. Telegram migrates off it.

## Why not …

- **One bot, smarter routing?** Tried (v0.2.0–v0.5.1). Operators read the resulting thread as a firehose; reply context is lost; per-bot approval routing is what the v0.2.7 `--manager` scoping was already reaching for. Per-bot is the cleaner shape — let Telegram's chat metaphor do the routing instead of inventing one inside the bot.

- **Keep the top-level `interfaces:` array and just teach the wizard to write to it?** Tried during the spike. The top-level array is one level removed from the manager it serves; you have to re-derive the relationship every time you read the YAML. Putting `interfaces.telegram` directly on the manager keeps related fields together (`reports_to_user`, the per-manager interfaces map) and removes a YAML cross-reference. The `interfaces:` *map* on the manager (rather than a bare `telegram:` key) leaves room for `discord:` / `imessage:` siblings without another schema bump.

- **HTTP client dependency for the wizard?** Avoided. Setup shells out to `curl` for `getMe` and `getUpdates`. `curl` ships with macOS and is on every Linux distro we support, so we keep teamctl's dep tree small and don't pull a TLS stack into the CLI for one interactive code path.

## Consequences

- **Migration**: existing teams with a top-level `interfaces:` Telegram entry keep working as long as they keep starting `team-bot` themselves — the schema accepts the legacy block (it lives under `Global.interfaces` for non-Telegram adapters anyway). The wizard ignores it and writes to the new `interfaces.telegram` shape. Both `telegram_inbox: true` and `reports_to_user: true` are removed (they were silently ignored on existing YAML during the 0.5.x series; they're noise to drop now). Presence of `interfaces.telegram` is the new signal. Examples and the dogfood team move to the new shape in 0.6.0.
- **Cost**: one Telegram bot per manager. BotFather is free and unlimited; each bot is one tmux session and one teloxide long-poll. Resource impact is negligible.
- **Security**: every bot has its own token and its own allow-list. A leaked PM token does not expose eng_lead's approvals.
