Applies the document contract from
docs/plans/2026-08-20_stack-docs-flatten-and-alignment.md section 3, partially:
`kind` and `status` only. `parent` is deliberately held until the flatten in
section 4 lands, so that 127 documents do not have to be re-pointed by hand
when docs/fleet/NORTH_STAR.yaml moves to docs/NORTH_STAR.yaml.
Scope, measured on origin/next at 63069149:
127 live docs = all *.md under docs/ minus docs/archive/ minus docs/_old_structure/
104 stamped here
19 held operator judgement (plan section 9), worklist in the same PR
3 held the SUPERSEDED TASKS.md stamps, which cite the moving path
1 untouched docs/fleet/FLEET-DOCTRINE.md, already stamped in W1
Kinds applied: 54 guide, 34 record, 9 spec, 6 tracking, 1 projection.
Every row carries a confidence and a one-line rationale in the worklist.
Two collisions with the existing state, both flagged rather than resolved:
1. docs/README.md:150-160 already documents a front-matter convention
(title/type/audience/status/source_of_truth) with its own allowed values.
It is applied to 4 of 127 files. Its `status` vocabulary is
current|draft|deprecated|historical; the new contract's is active|superseded-by.
The key collides. This commit lets the new contract win and rewrites
`status: current` to `status: active` on those 4 files, keeping their other
legacy keys untouched. No code reads any of them: `git grep source_of_truth`
outside docs/ returns nothing. docs/README.md still prescribes the old
convention and is an operator row, so it is not edited here.
2. Two of the plan's 20 operator rows are YAML files, not markdown
(docs/fleet/examples/roster-v2.yaml, docs/openapi-tess.yaml), and the
contract's front-matter form has no defined meaning for a .yaml document.
That gap also applies to docs/fleet/NORTH_STAR.yaml, the source of truth
itself. Raised in the worklist.
A third row from the plan, docs/fleet/north-star.md, no longer exists: W1
renamed it to docs/fleet/FLEET-DOCTRINE.md.
Verification: 104/104 parse with the expected kind and status in front matter;
the check was shown to reject a wrong kind before it was trusted. The diff
removes 4 lines total, all of them `status: current`.
9.9 KiB
kind, status
| kind | status |
|---|---|
| guide | active |
Discord conversations
Status: Current Discord workflow for an administrator-provisioned, authorized guild channel.
Telegram shared-contract parity, Matrix channel conversations, and a gateway-wide shared adapter registry are not current features. See Current versus planned before using any older channel instructions.
Audience: People conversing with an agent through Discord.
This workflow assumes an administrator has configured the Discord bot, gateway connection, allowlists, and a logical-agent binding. Users cannot create a binding or authorize themselves from Discord.
Current versus planned
| Surface | Status | What you can rely on |
|---|---|---|
| Discord guild messages | Current live routing | Authorized messages route to the configured logical agent; parent/thread delivery is tested, but ordinary durable history is not guaranteed. |
| Telegram | Not shared-contract parity | A raw legacy plugin exists, but its current source has no equivalent documented authorization, pairing, route, or focused package tests. |
| Matrix | Not implemented as a channel workflow | No current gateway channel adapter and test boundary establishes a Matrix conversation workflow. |
| Shared channel registry | Not implemented | The gateway's current registry hosts lifecycle wrappers; it does not provide universal channel routing or health. |
Start in a configured parent channel
Send a normal message in the administrator-configured parent text channel. You do not need to mention the bot for an ordinary turn.
For an authorized user, Mosaic:
- checks the guild, parent channel, user allowlist, pairing, role, and rate limit;
- keeps the response target in the parent channel; and
- routes the turn to the binding's logical agent using a stable conversation address.
No Discord thread is created for this untagged parent-channel case. The response is sent back to that same channel.
Messages from an unconfigured guild/channel, an unallowlisted user, an unpaired user, or a user without a role that can send are ignored without creating a thread or dispatching to the gateway. Bot-authored messages are ignored. Direct messages are not handled by the current guild ingress path.
Start a threaded topic with a mention
Mention the bot in a parent channel when you want a separate topic:
@Mosaic investigate the deployment failure
The current Discord adapter creates a public thread for the message, removes the bot mention from the content sent to the agent, and targets the response to that thread. If the message already has a Discord thread attached, the adapter reuses it instead of creating another one.
Authorization happens before thread creation. If the user, guild, parent channel, pairing, role, or rate check fails, no thread is created. If Discord cannot create or fetch the requested thread, the message is not dispatched because Mosaic cannot guarantee the requested response destination.
Continue inside a thread
Reply in the existing authorized thread without mentioning the bot again. The adapter:
- authorizes the message against the configured parent text channel;
- keeps the thread as the response target; and
- never attempts to create a nested thread.
A category above the text channel is not used as the authorization parent. Only the actual configured text-channel parent grants thread inheritance.
The stable conversation address is formed from the configured logical agent, channel name, and response channel/thread, for example:
<logical-agent-id>:discord:<response-channel-id>
It does not contain Claude, Codex, Pi, OpenCode, a model, a provider, a process, or a native runtime-session ID. The gateway owns runtime selection behind that route, so changing the runtime/provider does not require a new Discord address.
Durability limitation
Treat the current Discord path as live routing and delivery, not guaranteed durable conversation history. The Discord conversation address above is an external route string, while persisted conversation/message rows use UUID conversation IDs. No current external-route-to-UUID mapping was found. The gateway can continue dispatching after a persistence/binding failure, so a reply may appear in Discord without durable history or restart/resume continuity.
Do not rely on Discord as the sole record of a conversation. Durable history requires an implementation that maps the external route to a UUID, surfaces persistence failure, and proves fresh-message persistence and restart recovery in an integration test.
Attachments
An authorized message may contain text, attachments, or an attachment without text. The current adapter maps attachments into the shared message shape and preserves the native attachment ID, name, URL, content type, and optional size.
The gateway accepts only bounded attachment metadata: at most 10 attachments, HTTPS URLs without credentials, query strings, or fragments, and bounded ID, name, URL, MIME-type, size, and total metadata values. An unsafe or malformed attachment is rejected before the message is acknowledged or dispatched. Binary content is not embedded in the gateway message; the attachment remains a validated external reference.
Runtime controls
The current Discord text controls are:
/approve
/stop <approval>
They remain on the current parent/thread route and do not create a new topic. Approval and stop require an already enrolled durable session; ordinary Discord chat does not prove that enrollment occurred. These are privileged operations: the paired user must have the admin role and a provisioned mosaicUserId, the gateway must have a tenant configured for the control path, and the enrolled durable session must still belong to the bound logical agent. A stop must present the exact approval reference created for that target; approval consumption is one-time.
If these checks fail, the control operation is denied or produces no successful control result. Do not assume that being able to read a channel grants control authority.
Response and delivery behavior
The gateway emits raw stream events to the current Discord compatibility path. The plugin buffers agent:start/agent:text output and sends the completed response on agent:end; this is not a claim of token-by-token Discord rendering.
Outbound Discord text is split at a 1,900-character boundary. Transient rate-limit, server, and network failures are retried up to three attempts with one deterministic nonce per correlation/chunk; permanent delivery failures are not retried. A response route is checked against the configured logical-agent/channel binding before Discord is contacted.
If a message gets no response
Check these possibilities with the administrator:
- You are in a direct message, an unconfigured guild/channel, or a thread whose parent is not configured.
- Your Discord user ID is missing from the user allowlist or
pairedUsers. - Your pairing is
viewer, which cannot send ordinary agent turns. - The per-user/channel message or mention-thread limit was reached.
- The bot is not connected to Discord or the gateway Socket.IO
/chatnamespace. - A mentioned topic could not create/fetch its thread.
- The gateway rejected the signed envelope, route, attachment, or replayed native message ID.
/approveor/stop <approval>was attempted without the required admin pairing, tenant, pre-enrolled durable session, or exact approval.
These failures are intentionally fail-closed; an unauthorized or unverifiable message should not create a thread or agent side effect.
Not current: Telegram and Matrix
Do not substitute Telegram or Matrix instructions for this workflow:
- The current Telegram plugin uses raw Telegraf and Socket.IO messages, maps a chat to
telegram-<chatId>, accepts text only, and does not establish the Discord-style service-token, allowlist, pairing, shared-route, or attachment boundary. - No current Matrix gateway channel adapter, channel binding, user authorization flow, or focused channel tests establish a Matrix conversation workflow.
- The current gateway plugin list is lifecycle-only; it is not proof that every channel shares this Discord behavior.
Those are parity/design gaps, not alternate user workflows.
Evidence and related pages
- Channel protocol architecture — current shared types, Discord compatibility path, and explicit parity boundary.
- Discord ingress security — administrator configuration, authentication, authorization, and failure controls.
plugins/discord/src/index.ts— native Discord routing and delivery implementation.plugins/discord/src/index.test.ts— parent, mention, existing-thread, authorization, attachment, rate, egress, and health tests.apps/gateway/src/plugin/discord-ingress.security.spec.ts— gateway signature, replay, binding, attachment, approval, and stop tests.apps/gateway/src/__tests__/integration/tess-cross-surface.integration.test.ts— Discord control-flow test with explicit durable-session pre-enrollment; it is not fresh-message persistence evidence.- User Guide