conductor-telegram
v0.12.1
Published
Telegram bot for remote oversight of Conductor workspaces. Built by Belong.net.
Maintainers
Readme
conductor-telegram
Remote oversight for Conductor workspaces via Telegram. Run AI agents, approve decisions, and monitor progress from your phone.
Built by Belong.net
For an independently hosted gateway using only Conductor's native API, use the OVH cloud-only deployment and migration guide. Existing installations continue to default to hybrid mode.
Quickstart
npm i -g conductor-telegram
conductor-telegram setup
conductor-telegramThat's it. The setup wizard walks you through Telegram bot creation, configuration, and MCP plugin installation.
How it works
┌──────────────┐ ┌─────────────────┐ ┌─────────────────────┐
│ Telegram │◄───►│ conductor- │◄───►│ Local Conductor │
│ (you) │ │ telegram bot │ │ workspaces/agents │
└──────────────┘ └──────┬────┬─────┘ └──────────┬──────────┘
│ │ │
official API │ │ ┌──────────────┐ │
│ └──►│ SQLite (WAL) │◄──┘
▼ └──────────────┘
Conductor CloudThe bot polls local Conductor sessions every 5 seconds and Cloud sessions every 15 seconds, forwarding agent messages to Telegram. When an agent uses the MCP server to ask a question, the bot surfaces it as an interactive Telegram message with buttons or free-form reply.
In cloud-only mode, agent replies, questions, progress reports, and artifact links render Markdown as Telegram rich text, including bold, italics, strikethrough, inline code, code blocks, and clickable web links. Long replies keep their formatting across messages; answer buttons appear on the last message of a long question. Local file links remain readable as a label and path, and formatting that cannot be rendered safely falls back to literal text.
Each message that starts, continues, or controls Cloud work gets one status card. The acknowledgement arrives silently and is edited in place as the task is handed to the agent and when it finishes. Failures update the card to Not done: …; a failure reported more than 15 seconds after the acknowledgement also sends a notification. Agents in workspaces the gateway created are told about the Telegram MCP tools; agents in workspaces discovered from Conductor are asked to answer inline instead.
Architecture
src/
├── cli/ # CLI entry points
│ ├── index.ts # Command parser and dispatcher
│ ├── setup.ts # Interactive configuration wizard
│ ├── config.ts # Config loading (flags > env > config.json > defaults)
│ ├── doctor.ts # System validation and diagnostics
│ └── install-plugin.ts # MCP plugin installer
├── bot/ # Telegram bot
│ ├── index.ts # Bot init, polling loops, message forwarding
│ ├── commands.ts # All command and callback handlers
│ ├── launcher.ts # Agent spawning and session management
│ ├── polling-policy.ts # Cloud recovery scheduling and notice publication
│ ├── middleware.ts # Authentication guard
│ ├── format.ts # Markdown→HTML, styled buttons, escaping
│ ├── forum.ts # Forum topic lifecycle
│ └── callback-server.ts # Webhook/callback handling
├── cloud/ # Cloud-only gateway (TELEGRAM_RUNTIME_MODE=cloud-only)
│ ├── runtime.ts # Gateway startup, lease, and worker wiring
│ ├── commands.ts # Commands, repo topic routing, and callbacks
│ ├── engine.ts # Workspace launch, delivery, provider recovery
│ ├── catalog.ts # Conductor project catalog lookups
│ ├── sync.ts # Discovery of Cloud workspaces created elsewhere
│ ├── poller.ts # Independent session pollers
│ ├── telegram.ts # Durable outbound Telegram queue and sender
│ ├── bridge.ts # Scoped HTTPS file/MCP bridge and health probes
│ └── store.ts # Additive gateway queue and state tables
├── mcp/ # MCP server (runs inside workspaces)
│ └── server.ts # report_status, report_artifact, request_human
├── store/ # Database layer
│ ├── db.ts # SQLite init, schema, migrations
│ └── queries.ts # CRUD operations
├── integrations/
│ └── conductor-api.ts # Supported Conductor Cloud API transport
├── lanes/
│ ├── config.ts # Optional lanes.json loading
│ ├── decide.ts # Pure queue/nudge/create decisions
│ ├── pipeline.ts # Review, finals, merge, validation, and hygiene
│ ├── scheduler.ts # Backwards-compatible in-process scheduler
│ ├── manifest.ts # Strict Manifest v2 loader and prompt hashing
│ ├── controller.ts # Fenced deterministic delivery controller
│ ├── state-store*.ts # Command Center HTTP + explicit test SQLite seams
│ └── worker.ts # Independent Mac/OVH lease worker
└── types/
└── index.ts # TypeScript interfacesTelegram bot commands
| Command | Usage | Description |
|---------|-------|-------------|
| /setup | /setup | Check setup diagnostics and apply current chat |
| /run | /run <repo> <prompt> | Start a Cloud-first workspace with a local fallback |
| /cloud | /cloud <project> <prompt> | Start a ☁️ Conductor Cloud workspace via the API (no local checkout needed) |
| /projects | /projects [name] | List cloud projects, or one project's recent workspaces |
| /link | /link [project] (inside a repo topic, cloud-only mode) | Show or change the Conductor project a repo topic routes to |
| /fleet | /fleet [hours] | Org-wide cloud activity report from transcript search (default 24h, max 168) |
| /lanes | /lanes [pause\|resume\|retry\|provider-disable\|archive-approval\|shadow\|cutover\|rollback] | Durable lane status and audited controls when Manifest v2 is configured; legacy scheduler controls otherwise |
| /rename | /rename <name> (inside a topic or as a reply) | Rename the current cloud workspace via the API |
| /renamethread | /renamethread <name> (inside a topic or as a reply) | Rename the current cloud thread via the API |
| /review | /review <workspace> [instructions] (hybrid mode) | Launch a local code review session |
| /review | /review [PR number or URL] (inside a topic, cloud-only mode) | With native reviews enabled, review that workspace's pull request. Bare /review finds the PR; /review 500 and a GitHub URL also work |
| /send | /send <workspace> <message> | Send a follow-up message to a running agent |
| /threads | /threads [workspace] | List Conductor threads, switch the default thread, or start a new thread |
| /skills | /skills [workspace] | List built-in gstack skills plus workspace skills parsed from CLAUDE.md or AGENTS.md |
| /skill | /skill <workspace> <name> [instructions] | Invoke a specific workspace skill |
| /gstack | /gstack <workspace> [instructions] | Use GStack skills (ship, qa, browse, etc.) |
| /ship, /qa, /investigate, /retro, /health, /checkpoint, /document_release, /land_and_deploy, /office_hours, /design_review | /ship [instructions] (reply or use inside a topic) | Shortcuts for well-known gstack skills, registered in Telegram's slash menu. /land and /document are short spellings of the last two |
| /workspaces | /workspaces | List all tracked workspaces |
| /prs, /ship_status | /prs | Show PR, check, merge, and stale-branch status for tracked workspaces |
| /decisions | /decisions | Show unanswered agent questions for this chat |
| /status | /status | Show active workspace summary |
| /ping | /ping | Bot liveness check (uptime, heartbeat, version) |
| /stop | /stop <name> | Stop a running workspace |
| /repos | /repos | List available repositories (tap to select) |
| /help | /help | Show help message |
Ways to target work from Telegram:
- Reply to any forwarded workspace message with text, media,
/send,/review,/skills,/skill,/gstack, or any skill shortcut. If that message came from a specific Conductor thread, the reply goes back to that exact thread. - Send inside the workspace's forum topic — skill shortcuts and
/skill//gstackpick up the topic's workspace automatically. Plain messages go to the workspace's active Conductor thread. - Send inside a repo topic — in forum mode, tap Topic beside a repo in
/reposto create a durable repo topic. Text, photos, screenshots, generic files, and voice notes sent there start a new workspace for that repo without guessing from the message. In cloud-only mode the topic routes itself: its repository name is matched against the Conductor project catalog by project name or by the repository name in each project's remote, and a single match is recorded so every later message in that topic goes straight there. No match or more than one match asks with a picker instead of guessing, and says plainly that the message was not sent. Every message that names a project names the repository it points at, so a wrong route is visible the first time it happens. Use/linkto see or change where a topic routes. One topic is one workspace: the first task adopts the topic, and later messages continue that same workspace instead of opening another, so an album or a split paste reaches one workspace rather than one per message./run <project> <task>rolls the topic onto new work. A workspace living in a repo topic never renames or closes it, so the topic keeps its repository's name. - Hashtag a skill anywhere in a message (text or voice) — e.g.
#ship fix the failing testorcan you #qa this flow please. The bot rewrites the message into a skill-invocation prompt for the target workspace. Voice transcripts are scanned for hashtags too.
Conductor 0.72+ threads are mirrored into the same Telegram workspace topic. When a workspace has multiple visible Conductor sessions, forwarded messages include a 🧵 thread label. Use /threads in the topic to switch the active thread or start a new one.
In cloud-only mode, the bot can also discover Cloud workspaces created in Conductor or through another integration and attach each one to a Telegram forum topic. Set TELEGRAM_CLOUD_SYNC_CHAT_ID to the forum group ID. Existing transcript history is not replayed; the topic receives a connection note, the latest visible reply, and then new activity. Replies preserve the exact Conductor session and model. See the Cloud workspace sync specification for behavior, setup, migration, and the planned guided onboarding flow.
Conductor Cloud workspaces use Conductor's official API when CONDUCTOR_API_KEY is configured. Telegram can create cloud workspaces (/cloud), browse projects (/projects), send messages, create ordinary threads, poll transcripts/status, rename workspaces and threads, search org-wide transcripts (/fleet), cancel sessions, and archive workspaces without writing Conductor's private database. Without an API key, cloud workspaces remain observe-only through the desktop app's local mirror.
Cloud workspaces created with /cloud are driven entirely over the API — discovery, prompt delivery, and polling work even when the Conductor desktop app is closed or absent. Project arguments to /cloud and /projects accept a list number from /projects, a project id, an exact name, or a unique name prefix. When the bot itself runs inside a Conductor cloud workspace, it honors CONDUCTOR_API_URL and attributes its requests via an X-Conductor-Session-Id header taken from CONDUCTOR_SESSION_ID — both injected by the cloud workspace environment, not user config.
Repo-targeted Telegram launches are Cloud-first. /run, repo-topic messages, and AI-routed new tasks use Cloud automatically when CONDUCTOR_API_KEY is configured and exactly one Cloud project matches the local repository's origin URL (SSH and HTTPS forms are treated as the same repository). Missing or ambiguous origins always fall back locally; in that hybrid mode automatic routing never guesses from a project name or remote basename. The cloud-only gateway has no local checkout to read an origin from, so repo topics there route on the topic's own repository name instead, matched whole against project names and against the repository name in each project's remote, and ask whenever that is not unique. /run and /cloud name a project for a single task there: they adopt an unlinked topic and say so, and never re-point a topic that is already linked. /link is the one way to change it. The bot states when it falls back to a local workspace because Cloud is unconfigured, project lookup failed, no project matched, or Telegram attachments require the local file bridge. /cloud remains available when you want to choose a Cloud project explicitly.
If a local prompt later fails because its CLI login disappeared, the bot can take the same Telegram workspace over through Cloud. Automatic replay is limited to launcher-confirmed startup authentication failures before any assistant or tool activity. The worktree must be clean and its exact commit must already exist on an origin branch; the bot rechecks that branch after Cloud provisioning and before it sends work. Because the public Cloud API does not expose the provisioned checkout SHA, the handoff carries the expected SHA and tells the Cloud agent to verify HEAD before any side effect. Dirty files, unpushed commits, and partially executed prompts are never silently replayed. The Cloud binding and first prompt are persisted as a recoverable pending launch before delivery; later overlapping requests use a durable, ordered outbox with stable message identities. Stop intent and uncertain cleanup also survive restarts, so a canceled pending launch cannot be replayed later. A Stop or Archive the API rejects is retried across restarts, but only until it is clearly hopeless: because a pending terminal request blocks later sends, one that cannot succeed is retired with an explanatory message rather than gating the workspace forever. Restricted read-only reviews remain local until the public Cloud API exposes equivalent permission-policy enforcement.
Cloud commands act on your whole Conductor organization with the configured CONDUCTOR_API_KEY. In a group chat, set OWNER_USER_ID so only you can create (/cloud), rename, query (/projects, /fleet), or run the lanes scheduler (/lanes) — without it, every member of the configured group shares that privilege.
The official API is still beta. Cloud operations therefore use runtime response and resource-identity validation, bounded retries only for idempotent requests, throttled non-overlapping polls, and persisted message-ID cursors that are never mixed with desktop SQLite row IDs. Enforced review permission policies are not exposed by the API, so hybrid-mode cloud /review attempts fail closed. Cloud-only mode can opt into native reviews, which use normal Conductor permissions.
Photos, screenshots, voice notes, and audio files sent as replies are staged or transcribed for the agent. General-topic messages that the bot can only infer now ask for confirmation before starting or routing work.
Durable lanes controller (Manifest v2)
Manifest v2 is the production orchestration path. It is disabled by default and runs as an independent conductor-telegram lanes worker process, so Telegram polling failures cannot stop delivery. Runtime workspace/session IDs and PR URLs live only in Command Center/Postgres. The versioned manifest contains repositories, prompt paths and hashes, dependency milestones, provider policy, merge policy, and deterministic validation only.
The Mac worker is the preferred lease holder and the OVH service is a standby. A 75-second renewable fenced lease, 20-second heartbeat, 30-second active poll, 3-minute idle poll, and 15-minute full reconciliation ensure that only one worker mutates Conductor or a Git host. The returning Mac does not preempt a live OVH lease. Every external mutation is preceded by a deterministic durable action intent; messages, attestations, and notices bind their exact SHA-256 body. A lost response must reconcile the exact external payload before it can retry, so a lookalike tag cannot be mistaken for the commissioned result.
Copy docs/lanes.manifest-v2.example.json to ~/.conductor-telegram/lanes.manifest.v2.json. Manifest v2 accepts only the approved provider/model/cap map (claude/fable-5-1 at 3, codex/gpt-5.6-sol at 2, cursor/grok-4.7 at 2), rejects runtime IDs and dependency cycles, and verifies every prompt SHA-256 both on startup and immediately before a commissioned delivery. Recurring schedules are daily, weekly, or every <positive integer><m|h|d>.
Production workers require LANES_STATE_BACKEND=http, COMMAND_CENTER_API_BASE_URL, COMMAND_CENTER_API_KEY, CONDUCTOR_API_KEY, BOT_TOKEN, and OWNER_CHAT_ID. The OVH service is headless—it never polls or consumes Telegram updates—but it retains send-only credentials so the active lease holder can always emit a deduplicated safety alert. The Mac Telegram process additionally receives the separate BELONG_HUMAN_APPROVAL_KEY; that human key is forcibly removed from both lane-worker environments. LANES_MANIFEST_SOURCE_REF may carry the canonical Git revision; when omitted, both workers use the same content-addressed manifest SHA-256 rather than host-specific file paths. Add GITLAB_TOKEN only when a lane uses the GitLab adapter. SQLite requires both LANES_STATE_BACKEND=sqlite and LANES_STANDALONE=1; it is intended only for standalone tests and is never an HTTP fallback.
The production HTTP/Postgres schema and transaction contract are maintained by the pinned Command Center implementation documented in the lanes integration contract. Apply and verify that control-plane slice before enabling this worker; this repository does not run ad-hoc SQL against Command Center.
The durable CLI is:
conductor-telegram lanes worker
conductor-telegram lanes status --json
conductor-telegram lanes reconcile
conductor-telegram lanes import-legacy --source /path/to/legacy-queue.json --dry-run
conductor-telegram lanes import-legacy --source /path/to/legacy-queue.json --applyRun the dry-run first. Pinned legacy IDs are candidates only: the importer prefers the workspace linked to the current PR head, then the newest unarchived candidate with verified progress, and quarantines ambiguity. If an exact merged PR is the only authoritative truth and all of its old workspace candidates are archived, the importer adopts the merged SHA without reviving or binding an archived workspace. An apply requires the controller lease to be free and the exact manifest revision already active; it renews that lease during the authoritative rescan and revalidates the fence immediately before writing. Untagged adopted workspaces remain untrusted for hygiene and need one exact, 24-hour Telegram archive batch approval (/lanes archive-approval batch).
On the Mac, save the production credentials in the configured Doppler environment, then install the independent launchd job:
conductor-telegram service install --with-lanes \
--doppler-project <project> \
--doppler-config <config>
conductor-telegram doctorThe OVH standby unit is packaging/systemd/conductor-telegram-lanes.service. Its root-owned /etc/conductor-telegram/lanes.env must point at the same HTTP state plane and manifest revision. The service is headless and does not run Telegram polling.
The worker stages a deterministic revision such as growth-v2-<20 hex> and logs it. Start with /lanes shadow <revision>. A shadow full reconciliation is read-only and reports missing/ambiguous projects, managed/duplicate workspace names, binding drift, exact PR/head drift, and read errors. Active full reconciliation applies the same inventory checks to every manifest repository and managed workspace, including unknown run tags and archived/live drift; an early corrective action does not postpone the unfinished sweep for another 15 minutes. After that comparison, credential doctor, legacy import report, disposable canary, and CI all pass, the single human cutover is /lanes cutover <revision>. /lanes rollback disables dispatch without deleting state or workspaces; it never enables the legacy Python shepherd. /lanes resume can recover only a safety pause that originated in active mode; shadow and rollback require their explicit cutover sequence. A manifest revision cannot replace the active policy while an older revision still has nonterminal work.
Delivery requires an adversarial reviewer distinct from the author, then two current-head final attestations from distinct providers. Attestations are accepted only from commissioned attempts with the exact nonce, run, stage, provider, head SHA, tag, and body hash, and terminal PR/review/validation output is not consumed until its Conductor session is idle. Merge is bound to owner/repository/base/head branch/head SHA and requires the configured named checks to pass; both Git hosts reassert the expected head after a merge response. Completion requires CI on the merged SHA and the manifest's exact command/probe evidence; commentary markers alone do not count. Terminal workspace grace starts at the original terminal transition, and an untagged archive approval is bound to an immutable, expiring list containing the current workspace ID. Archive hygiene performs a second live-session preflight after recording its intent and immediately before mutation; a newly working session pauses the controller instead of being archived. The controller observes repository-native CI but never deploys, publishes, performs outreach, spends money, or changes secrets.
Legacy lanes scheduler
An optional in-process scheduler keeps at most one working Cloud lane per configured provider, using that provider's model, from an ordered queue with dependencies. It is inert unless a config file is present at LANES_CONFIG or ~/.conductor-telegram/lanes.json. Copy docs/lanes.example.json and replace the placeholders (L1, https://github.com/example-org/example-repo, example model ids).
Each tick (every intervalMinutes, and on /lanes run) looks up matching workspaces including archived workspaces by [lane:<id>:…] name containment, or by an explicit sessionId / workspaceId; names containing [abandoned are ignored. A listing outage — including a partial per-project fallback outage — skips the tick instead of creating a duplicate workspace. A lane is working when its newest session is working (even if the transcript is empty or unread), done when the last idle-turn assistant text contains a GitHub pull-request URL (tool payloads and reasoning/thinking items are ignored), initializing when no user message has been recorded yet, paused when it exists and is idle or in error but not done, unknown when status cannot be read or an idle transcript cannot be read, and not created otherwise. Providers under maxActive resume or create dependency-ready work. /lanes pause suppresses scheduled Cloud walks before any listing.
Each lane may add an optional delivery object, with any combination of review, finals, merge, and validation. merge requires finals, and validation requires merge, so the scheduler can prove its preconditions:
reviewcreates one adversarial reviewer on the first free provider inrotationother than the author, waits for its GitHub review, then messages the author once.finalsruns two sequential final reviewers on distinct non-author providers. Their GitHub review body starts withFINAL-REVIEW (<real model>): {"verdict":"approve"|"changes", ...}. A change verdict returns the lane to its author and starts a fresh round after the next pushed turn.mergerefreshes GitHub directly and requires a repo-bound open PR, the exact reviewed head, two commissioned approving GitHub attestations from distinct providers on that head, passing checks, and a conflict-free merge state. It does not depend on GitHub's aggregate approval state, so same-account validators remain verifiable. It usesmethod(squash,merge, orrebase) and acceptsMERGED BY AGENTSonly when its full SHA matches GitHub's merge commit. Conflicts go back to the author for a rebase.validationruns the exact configuredverificationonce against the merged base on a provider distinct from the author and merge executor. AVALIDATED (<real model>)orVALIDATION FAILED (<real model>)marker is advisory until a matching terminal Conductor command/tool receipt is present; passing completion also requires green CI on the merged SHA. Failed validation may open a narrowly scoped repair PR but never deploys directly.
Stage prompts are paths relative to lanes.json and support {{laneId}}, {{laneTitle}}, {{prUrl}}, {{round}}, {{slot}}, {{model}}, {{previousFinalReview}}, {{mergeMethod}}, {{mergeHeadSha}}, {{deployNotes}}, {{replayNotes}}, and {{verification}}. Required safety/marker instructions are appended by the scheduler. The example config and prompt templates live in docs/lanes.example.json and docs/prompts/.
The scheduler keeps its delivery and nudge ledger in SQLite. Two nudges without a newer assistant response mark a session dead; recovery starts a new session in the same workspace and always targets that workspace's newest session. Rate-limit reset timestamps found in assistant transcripts suppress work until the reset, and the next available provider in a stage rotation acts as the stand-in using its real model in markers.
Hygiene runs after each active tick. It archives finished review/final/merge/validation workspaces and all lane workspaces after merge, never archives a working newest session, and never recreates a matching archived workspace. /lanes archive runs the same hygiene immediately. /lanes merge <id> forces a policy check but cannot bypass final approvals or dependency/conflict rules. /lanes shows author → review → finals → merge → validation progress per lane. All /lanes forms remain owner-only through the existing middleware; merges, validations, and archive batches send one-line Telegram notices.
Prompt paths in the config are relative to the config file's directory.
Manual Telegram setup
If you want to configure the bot manually instead of using the CLI wizard, the bot supports two operating modes:
- Private chat mode: talk to the bot directly in a one-on-one chat.
- Forum topic mode: run the bot in a Telegram supergroup with Topics enabled so each workspace gets its own topic.
Private chat mode
- Create a bot with BotFather and copy the token into
BOT_TOKEN. - Temporarily set
OWNER_CHAT_ID=0. - Start the bot.
- Open a direct chat with the bot and send
/startor/setup. - If the bot shows a
Use This Chatbutton, tap it. The bot will save this private chat automatically. - Leave
OWNER_USER_IDempty. - Restart the bot only if you are running it with hardcoded env vars outside the CLI.
Forum topic mode
Add the bot to your target group, make it admin, then run setup in that group.
- Create a Telegram supergroup.
- Enable
Topicsin the supergroup settings. - Add the bot to the supergroup.
- Promote the bot to admin with permission to create/manage topics and send messages.
- Temporarily set
OWNER_CHAT_ID=0andOWNER_USER_ID=0. - Start the bot.
- Send
/setupin the target supergroup. - If the bot shows a
Use This Chatbutton, tap it. The bot will save this supergroup and your Telegram user automatically. - Restart the bot only if you are running it with hardcoded env vars outside the CLI.
New workspaces will create one forum topic per workspace automatically. Repo topics can also be created from /repos and reused as the stable home for that repository's current workspace. If topic creation fails because the chat is not a forum or the bot lacks permissions, the bot falls back to normal chat messages.
If the bot is already configured for your private chat, you can also add it to a new group and send /setup there from the same Telegram account. The bot will show what is missing and can switch itself into group/forum mode from that chat without first resetting OWNER_CHAT_ID.
Bootstrap mode
When OWNER_CHAT_ID=0, the bot temporarily allows /start, /help, and /setup before auth is configured. This is the intended bootstrap mode for letting the bot configure the active chat for you.
CLI commands
conductor-telegram Start the bot (foreground)
conductor-telegram setup Interactive configuration wizard
conductor-telegram doctor Validate config, token, paths, and connectivity
conductor-telegram status Show configuration health
conductor-telegram lanes worker Run the independent fenced lane controller
conductor-telegram lanes status --json Read the durable control-plane snapshot
conductor-telegram lanes reconcile Run one full reconciliation pass
conductor-telegram lanes import-legacy Plan/apply the one-time legacy adoption
conductor-telegram install-plugin Install MCP server into Claude Code
conductor-telegram help Show all commands
conductor-telegram --version Show versionConfiguration
Config is stored at ~/.conductor-telegram/config.json (created by setup).
Precedence: CLI flags > environment variables > config.json > defaults
| Flag | Env Var | Description |
|------|---------|-------------|
| --token | BOT_TOKEN | Telegram bot token |
| --chat-id | OWNER_CHAT_ID | Your Telegram chat ID |
| --db-path | DB_PATH | SQLite database path |
| --doppler-project | CONDUCTOR_TELEGRAM_DOPPLER_PROJECT | Doppler project used by foreground and launchd runtimes |
| --doppler-config | CONDUCTOR_TELEGRAM_DOPPLER_CONFIG | Doppler config used by foreground and launchd runtimes |
| | OWNER_USER_ID | Your Telegram user ID (required for forum mode) |
| | TELEGRAM_CLOUD_SYNC_CHAT_ID | Forum group that receives Cloud workspaces created outside Telegram (cloud-only mode) |
| | TELEGRAM_CLOUD_SYNC_INPUT | commands during gateway coexistence, or all for ordinary text and voice replies (default: all) |
| | CONDUCTOR_WORKSPACES_DIR | Conductor workspaces directory |
| | CONDUCTOR_REPOS_DIR | Repository directory |
| | CONDUCTOR_DB_PATH | Conductor's own database path |
| | TELEGRAM_DEFAULT_AGENT_TYPE | Default agent: claude or codex |
| | TELEGRAM_DEFAULT_MODEL | Default model for agents |
| | TELEGRAM_REVIEW_AGENT_TYPE | Agent type for /review sessions |
| | TELEGRAM_REVIEW_MODEL | Model for /review sessions |
| | TELEGRAM_AGENT_PERMISSION_MODE | Legacy Claude permission mode (default: acceptEdits) |
| | CONDUCTOR_API_BASE_URL | Conductor API origin (default: https://api.conductor.build) |
| | CONDUCTOR_API_KEY | Bearer API key for supported Conductor Cloud operations |
| | CONDUCTOR_CLOUD_BACKEND | auto (use API when keyed), api (require key), or off |
| | LANES_CONFIG | Path to the optional lanes scheduler JSON (default ~/.conductor-telegram/lanes.json) |
| | LANES_MANIFEST | Path to strict Manifest v2 (default ~/.conductor-telegram/lanes.manifest.v2.json) |
| | LANES_MANIFEST_SOURCE_REF | Optional canonical Git source revision; defaults to the cross-host manifest SHA-256 |
| | LANES_STATE_BACKEND | Durable state backend: production must be http; sqlite is explicit standalone/test only |
| | COMMAND_CENTER_API_BASE_URL | Command Center origin for the durable lane state API |
| | COMMAND_CENTER_API_KEY | Service credential for the durable lane state API |
| | BELONG_HUMAN_APPROVAL_KEY | Separate credential used by Telegram for cutover/rollback/archive approvals; excluded from the worker |
| | GITLAB_TOKEN | GitLab API token, required only by manifests containing GitLab lanes |
| | TELEGRAM_WHISPER_MODEL | whisper.cpp model name or path (default: base) |
Conductor app settings are read from ~/.conductor/settings.toml first, with the legacy Conductor DB settings table as fallback. The bot uses Conductor's default/review model settings, Codex thinking levels, Claude effort levels, and git branch prefix settings when Telegram-specific env vars are not set.
To keep runtime secrets in Doppler, persist only the non-secret project/config references:
conductor-telegram service install \
--doppler-project <project> \
--doppler-config <config>
conductor-telegram doctorThe installer verifies the persistent Doppler identity available to launchd, removes each Doppler-managed value from config.json, and writes a plist containing only the Doppler executable, project/config references, and an explicit allowlist of environment names—not values or a Doppler service token. The allowed names are BOT_TOKEN, OWNER_CHAT_ID, OWNER_USER_ID, TELEGRAM_CLOUD_SYNC_CHAT_ID, TELEGRAM_CLOUD_SYNC_INPUT, CONDUCTOR_API_BASE_URL, CONDUCTOR_API_KEY, CONDUCTOR_CLOUD_BACKEND, COMMAND_CENTER_API_BASE_URL, COMMAND_CENTER_API_KEY, BELONG_HUMAN_APPROVAL_KEY, GITLAB_TOKEN, LANES_MANIFEST, and LANES_MANIFEST_SOURCE_REF. The lane worker deliberately receives every applicable value except BELONG_HUMAN_APPROVAL_KEY; only the Telegram process can commission human-gated controls. Use BOT_TOKEN exactly; TELEGRAM_BOT_TOKEN is not an alias.
start, status, and doctor automatically re-enter the configured Doppler runtime. Run service install again after adding a new allowed secret name. A value-only rotation needs only a service restart.
If Doppler is not configured, keep CONDUCTOR_API_KEY only in the bot's mode-0600 config file or service environment. It is excluded from child-agent environments and must not be copied into repositories, Conductor workspace environment variables, prompts, or MCP configuration.
Existing .env files are auto-detected and can be imported during setup.
MCP server
The MCP server runs inside Conductor workspaces and gives agents these tools:
| Tool | Description |
|------|-------------|
| report_status | Report progress back to Telegram (status label + message) |
| report_artifact | Report a deliverable: PR, commit, or file |
| request_human | Ask the operator a question, optionally with button choices |
The request_human tool blocks (polls for up to 5 minutes) until the operator answers via Telegram — either by tapping a button or replying with free-form text.
Install with conductor-telegram install-plugin or during setup.
Database
SQLite database at ~/.conductor-telegram/conductor-telegram.db with WAL mode for concurrent writes from the bot and multiple MCP server instances.
Tables:
| Table | Purpose |
|-------|---------|
| workspaces | Tracked workspace state, status, repo path, Telegram thread |
| events | Status updates, artifacts, and human requests from MCP |
| decisions | Questions posed to the operator with answers |
| telegram_message_links | Maps Telegram messages to workspaces for reply routing |
| thread_cursors | Per-Conductor-session forwarding cursors for thread fan-out |
| bot_heartbeat | Process liveness, boot count, and last exit details |
| meta | Durable Cloud launches, ordered messages, stop/archive intents, work leases, recovery notices, and the lanes pause flag |
| pr_records | GitHub PR/check/merge state verified by repo + branch |
| merge_intents | Expiring requester-bound confirmations for an exact PR head SHA |
| repo_topics | Durable Telegram forum topics mapped to repos for no-guess launch routing |
| route_attempts | Redacted routing audit log for routed, failed, confirmed, and cancelled attempts |
| lane_actions | Create/nudge history for the optional lanes scheduler |
| lane_delivery_state | Durable per-lane review, final, merge, and validation stage state |
| lane_session_health | Unanswered-nudge and reset-time ledger for lane sessions |
| lane_provider_outages | Provider rate-limit reset times used by stage rotations |
Development
git clone https://github.com/belongnet/conductor-telegram.git
cd conductor-telegram
npm install
# Run in development mode
npm run dev # CLI entry point
npm run dev:bot # Bot directly
npm run dev:mcp # MCP server
# Build
npm run build
# Type check
npm run typecheck
# Run tests
npm testRequires Node.js v22+. See CONTRIBUTING.md for branching, commit style, and PR guidelines. Intentionally deferred repository work is tracked in TODOS.md.
Troubleshooting
Run conductor-telegram doctor to check all components:
$ conductor-telegram doctor
Node.js ✓ v22.14.0 (required >=22)
Config ✓ ~/.conductor-telegram/config.json (0600)
Bot token ✓ @MyBot connected
Database ✓ ~/.conductor-telegram/conductor-telegram.db
Conductor ✓ ~/Library/Application Support/com.conductor.app/conductor.db
Conductor Cloud API ✓ api-key authenticated; 3 cloud project(s) visible
GitHub CLI ✓ gh version 2.x.x
MCP Plugin ✓ ~/.claude/plugins/conductor-telegram-mcp installed
Repos ✓ ~/conductor/repos (4 repositories)Common issues:
- "Bot token is invalid": Token may be revoked. Create a new one with @BotFather and run
conductor-telegram setup. - "better-sqlite3 failed to load": Run
npm rebuild better-sqlite3. If that fails, install Xcode CLI tools:xcode-select --install. - "Conductor DB not found": Install Conductor or set
conductorDbPathin config.
Upgrading
npm i -g conductor-telegram@latest
conductor-telegram doctorConfig is preserved across upgrades. The doctor command validates everything still works. Release notes live in CHANGELOG.md.
Mac gateway deployment
A gateway host keeps itself current. Enrolling is opt-in:
conductor-telegram service install --with-updaterThis registers a third LaunchAgent, net.belong.conductor-telegram.updater, alongside the bot and its watchdog (plus the independent lanes job when --with-lanes is selected). A plain service install never enrolls the updater, so published-package users keep the normal npm i -g conductor-telegram@latest upgrade contract. Every minute the agent fetches origin/main into a canonical checkout at ~/.conductor-telegram/gateway/repo (cloning it on first run, so a fresh machine self-bootstraps), and whenever the remote moves it redeploys:
scripts/gateway-update.sh # the poller — copied to ~/.conductor-telegram/bin/ at install
scripts/deploy-mac-gateway.sh # the deploy it triggersEach deploy runs the Node 22 typecheck/tests/build in the checkout, then packs a release tarball and installs that globally — the live gateway is a copy, so nothing the deploy does to the checkout (git reset, npm ci) can touch running code before all gates pass. Only then does it reinstall and restart the launchd service and run doctor as a configuration/connectivity gate. A failure before the install step leaves the previous gateway untouched and running; a failure at the doctor stage means the new build is already live and gets retried. Failed revisions retry on a 30-minute backoff until a new push lands. Deploys have a hard 40-minute timeout, refuse non-fast-forward (force-pushed) branch tips, and report success/failure straight to the owner's Telegram chat when the bot token is in config.json. All three agents are RunAtLoad, so a reboot restarts the bot and immediately catches up on any pushes it slept through. Progress lands in ~/.conductor-telegram/update.log; service status shows the deployed revision; service stop is respected — auto-deploys will not resurrect a bot the operator stopped.
Polling means no self-hosted runner, no inbound webhook, and no GitHub credentials on the machine — the repo is public, so the updater fetches anonymously. Note the flip side: enrolling a machine means anyone who can push to main can run code on it within a minute. Saved Doppler project/config references survive each reinstall, so deployment never needs to materialize secret values in the checkout or launchd plist.
Updater environment overrides, baked into the agent when set at service install --with-updater time: CONDUCTOR_TELEGRAM_GATEWAY_HOME (state + checkout root, default ~/.conductor-telegram/gateway), CONDUCTOR_TELEGRAM_GATEWAY_REMOTE (git URL), CONDUCTOR_TELEGRAM_GATEWAY_BRANCH (default main), CONDUCTOR_TELEGRAM_GATEWAY_LOG (default ~/.conductor-telegram/update.log).
License
MIT - Built by Belong.net
