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 global — telegram_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)consultschannel_skillsforchatID(not the global flag) to decide if the command is exposed here.- A new
PublishChannelMenu(ctx, tg, chatID)publishes that channel's palette viaBotCommandScopeChat. Called on channel activation and on any palette edit. - The broad-scope
PublishMenustays 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_skillsjoin +telegram_channels.role. (verify: pr) - T2.2 per-channel
Dispatch(chatID)+PublishChannelMenu(per-chat scope). (verify: pr) - T2.3
/ui/telegrampage: 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
/auditwith 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.