Skip to content

Specialist Telegram Channels — Design Spec

Status: approved design, build in progress (2026-06-28) Supersedes the global "skills-as-commands" exposure model (ARGUS-145/147) by making exposure per-channel and adding an agent-run execution rung.

Goal

Turn each Telegram channel into a specialist agent: a channel has a role, a scoped command palette (a subset of skills), and a defined place its output lands. A Knowledge channel captures + recalls; a Research channel runs deep research and files cited reports; a Code Review channel audits a selected project and files findings as tasks. Not every skill reaches every channel, and some skills (e.g. flash-tasmota) reach no channel at all.

Why

We already promoted skills to gated, registry-backed capabilities reachable via HTTP / Chat MCP / Telegram command (ARGUS-145/147). But: - Exposure is globaltelegram_enabled is one flag on the skill, so a command shows the same everywhere. Specialists need per-channel palettes. - Only deterministic, in-process commands exist (e.g. /forgejo_ci). The valuable skills (deep-research, deep-audit) spawn an agent that runs for minutes-to-hours and produces a report. They need an agent-run rung, not a tier-2 confirm. ARGUS-159 (tier-2 confirm) does NOT unblock these. - Output has nowhere defined to live. A research report should become an Argus note; audit findings should become tasks. Output routing must be explicit.

The model: a channel is a specialist

Each channel carries:

Field Meaning
role what the channel is for (knowledge / research / code-review)
project fixed default, or selectable per command (code-review picks one)
command palette the subset of skills exposed in this channel
output sink where results land (memory note / tasks / report artifact / channel only)

The technical enabler: Telegram per-chat command scopes (BotCommandScopeChat). We already publish to default/all_private_chats/all_group_chats; we add a per-channel palette published with scope: {type:"chat", chat_id}, so each channel's / menu differs. A skill with global telegram_enabled=false (flash-tasmota) never appears anywhere.

Current state (what exists) and the gaps

Exists: - telegram_channels (migration 0036/0038): chat_id, title, label, project, mode(ingest|respond|both), status(pending|active|ignored), msg_count, ..., chat_session_id. ChannelRegistry (Upsert/Get/Set), HTTP API. - skills registry with executable, exec_tier, telegram_enabled (ARGUS-147) and SkillGate/skillExecGate. - SkillCommandRouter (Dispatch, MenuCommands, PublishMenu) — but Dispatch(ctx, _ int64, ...) ignores chatID and PublishMenu publishes one GLOBAL menu to the broad scopes. - A host-side claude -p worker pattern (the conversational chat-worker daemon on .96, NOT in compose — it needs the Claude Code binary + auth + skills on the host). - Tasks board: multi-project at the data layer (ARGUS-*, HOMELAB-APPS-* via per-project counters) but /ui/tasks shows all projects mixed, no filter.

Gaps to build: 1. Per-channel skill palette + per-chat command scope. 2. The agent-run rung: a command spawns a host claude -p run, async, result captured back. 3. Output routing: report -> note; findings -> tasks. 4. /ui/telegram page to see + assign skills per channel. 5. Per-project board filter so audit findings are usable.

Data model

telegram_channels  (extend)
  + role text NOT NULL DEFAULT 'capture'      -- knowledge|research|code-review|capture
  (project/mode/status already present)

channel_skills     (new join — the per-channel palette, source of truth for scope)
  chat_id     bigint  REFERENCES telegram_channels(chat_id) ON DELETE CASCADE
  skill_name  text    REFERENCES skills(name)               ON DELETE CASCADE
  PRIMARY KEY (chat_id, skill_name)

agent_runs         (new — the agent-run queue + audit trail)
  id          bigserial PK
  chat_id     bigint        -- channel that requested it (output target)
  skill       text          -- e.g. 'deep-research'
  prompt      text          -- the refined query / target
  project     text          -- for code-review: the audited project
  status      text          -- queued|running|done|failed   (CHECK)
  output_kind text          -- note|tasks|report  (how to route the result)
  note_id     text          -- set when output_kind=note and a note was saved
  result      text          -- the report text (or error on failed)
  created_at  timestamptz default now()
  started_at  timestamptz
  finished_at timestamptz

A skill is exposed in a channel iff (chat_id, skill_name) ∈ channel_skills AND the skill passes the global execution gate (executable, runnable tier) AND global telegram_enabled=true. (Per-channel scope narrows; it never overrides the global off-switch — flash-tasmota stays off everywhere.)

Execution: the agent-run rung

Deterministic commands (/forgejo_ci) answer in-process. Agent-run commands (/research, /audit) cannot — they need the Claude Code binary, auth, and the skill's Workflow, none of which live in the hub container. So the rung is split:

Telegram /research <q>
   -> hub: gate (channel palette + exec gate) -> enqueue agent_runs(status=queued)
   -> hub: reply "research queued (run #N)"        [fast, in-process]

host agent-runner (daemon on .96, like the chat-worker; NOT in compose)
   -> POST claim: oldest queued run -> status=running          [token-gated]
   -> exec: claude -p "use the <skill> skill on: <prompt>"     [minutes-hours]
   -> POST complete: { result, status }                        [token-gated]

hub: on complete -> route output (note|tasks) -> post result to chat_id

Why a queue (not a synchronous spawn): the run is long, the hub container can't spawn it, and a durable row gives a verifiable audit trail (no silent failure — a failed run is visible, and the channel is told). The runner is the only non-unit-testable piece (it shells out to claude -p); everything in the hub (enqueue, claim, complete, routing) is Go-testable with a fake runner.

Output routing

output_kind on the run decides where the result lands: - note (research): save the report as an Argus memory note tagged project=research (the knowledge layer), then post a summary + the note id back to the channel. - tasks (code-review): parse the audit findings and CreateItem one task per finding under project=<audited project> (verify=pr by default — a fix PR closes them), then post the count + ids back. - report (fallback): post the text to the channel, no persistence.

Per-channel command scoping

  • Dispatch(ctx, chatID, cmd, args) consults channel_skills for chatID (not the global flag) to decide if the command is exposed here.
  • A new PublishChannelMenu(ctx, tg, chatID) publishes that channel's palette via BotCommandScopeChat. Called on channel activation and on any palette edit.
  • The broad-scope PublishMenu stays as the default/fallback for unconfigured chats.

The /ui/telegram config page

DB-backed config means a management page: - Lists every channel: label, role, project, mode, status. - For each, a checkbox grid of skills (from the registry) — checked = in this channel's palette. Toggling writes channel_skills and republishes that channel's per-chat menu. - Only globally telegram_enabled skills are offered (the global off-switch is respected; flash-tasmota never appears).

Business logic stays server-side: the page renders DB state and POSTs toggles; the hub owns gating + republish.

The three channels

Channel role palette execution output sink
Knowledge knowledge passive ingest; /recall <q> in-process memory (knowledge layer)
Research research /research <q> agent-run (deep-research) cited report -> note (project=research) + posted back
Code Review code-review /audit (pick project), /review <PR> agent-run (deep-audit) findings -> tasks (project=target) + per-project board view

Build order (features) + verify contracts

Sequenced so each step proves something before the next leans on it. The novel, risky piece (agent-run) goes first; the config page is routine CRUD after.

Feature 1 — Agent-run execution rung (slice 1: Research end-to-end)

The thin end-to-end vertical: prove enqueue -> host run -> output-into-Argus on one channel, hardcoded. - T1.1 agent_runs table + Enqueue/Claim/Complete store methods. (verify: pr — store tests green) - T1.2 /research <q> command -> enqueue + "queued" reply, gated on the research skill being executable+exposed. (verify: pr) - T1.3 host-runner claim/complete HTTP API (token-gated). (verify: pr) - T1.4 on complete: save report as an Argus note (project=research) + post to the channel. (verify: pr) - T1.5 host agent-runner script invoking claude -p deep-research; deploy + smoke. (verify: approval — host daemon, no CI)

Feature 2 — Channel-config framework + /ui/telegram

  • T2.1 schema: channel_skills join + telegram_channels.role. (verify: pr)
  • T2.2 per-channel Dispatch(chatID) + PublishChannelMenu (per-chat scope). (verify: pr)
  • T2.3 /ui/telegram page: list channels, assign skills, toggle -> republish. (verify: pr)

Feature 3 — Code Review channel

  • T3.1 per-project board filter on /ui/tasks (rides on the 163 search). (verify: pr)
  • T3.2 /audit with project picker (inline buttons; reuse callback machinery). (verify: pr)
  • T3.3 deep-audit run -> findings filed as tasks under the audited project. (verify: pr)

Non-goals (this epic)

  • Tier-2 confirm-then-run (ARGUS-159) — orthogonal; agent-run is its own rung.
  • Running arbitrary Bash/SSH from Telegram — the runner only invokes named, registry-gated skills.
  • Multi-user/tenant scoping — single operator.