Skip to content

add-channel ​

Give one work item a room of its own.

bash
the-loop add-channel slack@#tmp-issue-375 --work-item github:OWNER/REPO#375
the-loop add-channel slack@C0TMP375 --work-item github:OWNER/REPO#375
the-loop add-channel slack://C0TMP375 --work-item github:OWNER/REPO#375 --no-comment

What it does ​

channels.slack.channel is the operator's one channel: every work item this deployment touches gets a thread in it, and anyone who wants to follow one work item has to follow all of them. That is the wrong shape for the work item people spin a channel up for — the migration with six stakeholders, the incident with a #tmp-… room (issue-375).

This declares a collaboration channel on one work item. Two things follow, and only these two:

  1. The conversation lives there. The work item's thread root is opened in the declared channel instead of the central one, so every update the-loop posts about it lands in the room the people are in. If its conversation had already started elsewhere, the next update opens a root in the new room and leaves a pointer in the old thread.
  2. Every message there is about that work item. A top-level message in the room is a reply on the work item — not a new issue — and so is a message in a thread the-loop did not open. A dedicated room has one subject.

It grants nobody anything. Who may speak is still channels.slack's principals allow-list; who may direct the loop is still routing.authorizedUsers; who may be input on one work item is still the collaborator roster. A channel is a place, not a person — so the order of "declare the room" and "invite the people" never matters.

Three effects, in this order:

  1. Resolves a name to an id, then writes the declaration into the work item's portable record (<state.root>/portable/<slug>.json, collaborationChannels section), with who declared it, when, and through which surface. The record stores the id — what the ingress routes on — and keeps the name beside it for display.
  2. Posts the same keyword — the-loop add-channel slack@C0TMP375 — back on the work item, carrying the loop-prevention marker, so the thread reads identically whether the declaration came from the terminal or from a comment. Best-effort: a failing post is reported and the declaration stands.
  3. Records control.command with the work item, the channel, the actor and the effect (declared / already-declared), plus the channel it replaced if any.

The grammar ​

<type>@<target>, and <type>://<target> for the same thing; what is stored and printed is always the first form.

TypeTargetNotes
slackthe channel's name (#tmp-issue-375, or bare) or its conversation id (C… public, G… private, D… a DM)A name is resolved to an id when you declare it, and the id is what is stored — so a later rename changes nothing. Resolving needs the app's channels:read / groups:read scopes and the bot to be able to see the channel; a name that resolves to neither is refused rather than stored. An id needs no scope and no lookup.
jiraa Jira issue key, e.g. OPS-12An output-only mirror (issue-475): the events channels.jira.subscribe names are posted on that issue, and nothing written there reaches the session. Refused unless integrations.jira is configured and the key's project is listed under integrations.jira.projects — a project without a repository is a mirror-only room. See Channels — Jira.

A type the-loop has no adapter for is refused at declaration time rather than stored and silently ignored. Adding one later — WhatsApp, say — is a row in the type table plus an adapter, not a new grammar.

Flags ​

FlagDefaultMeaning
TYPE@TARGET …requiredOne or more channels, e.g. slack@C0TMP375.
--work-itemrequiredThe work item the channel is declared on, e.g. github:OWNER/REPO#375.
--listenmentionsWhat the room hears: mentions (only messages carrying @the-loop) or all (every message, as a room did before issue-389). Recorded on the declaration; a re-declaration replaces it.
--portable-dir<state.root>/portableWhere the declarations live.
--comment / --no-commentonPost the keyword back to the work item.

Exit codes ​

CodeWhen
0at least one channel was newly declared
1nothing changed — every channel named was already declared
2a malformed ref, an unknown type, a name that resolves to no channel this bot can see, a work-item ref that will not parse, or a channel another work item holds: nothing was written and nothing posted

Notes ​

  • All or nothing. Every ref is validated and resolved before any of them is written, so a second channel the bot cannot see leaves the first unapplied.
  • One channel per type per work item. Declaring a second Slack channel moves the conversation rather than adding one; the output names what it replaced.
  • One work item per channel. A channel another work item declares is refused — attributing a room's messages must not be a guess. Undeclare it there first.
  • A declaration is scoped to the work item's active life: cleared when the item closes, and the-loop sessions reset forgets it. the-loop cleanup keeps it, as it keeps the control record.
  • Poll mode reads less than Socket Mode. conversations.history returns top-level messages, so a reply inside a thread the-loop did not open is seen only under channels.slack.read.mode: socket. The room is baselined on first sight, so declaring a channel never replays its backlog.
  • The same declaration can be made from the ticket, by an authorized user commenting the-loop add-channel slack@C0TMP375. The keyword is configurable at routing.control.keywords.add-channel; setting it to "" disables the word.
  • Runs in-process, not through the control-plane service — deliberately, like ask and add-collaborator.
  • Authorization on this path is shell access to the machine running the-loop. The stricter test — a named login in routing.authorizedUsers — is the comment path's, because that is where an untrusted author can reach.

Released under the MIT License.