Agent skill

Channel Extension

by Dpro-at in Dpro-at/Tel-Agent

A skill your agent uses when adding a new messaging channel to Tel-Agent or changing an existing one — a transport under api/channels/, its official extension manifest, its settings card, tests…

AGPL-3.0Auto-check passed

Install Channel Extension

skills CLI
$ npx skills add Dpro-at/Tel-Agent --skill channel-extension -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install Dpro-at/Tel-Agent channel-extension --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/Dpro-at/Tel-Agent.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.claude/skills/channel-extension .claude/skills/channel-extension && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
channel-extension
GitHub stars
1.1k
Token cost
~4.1k tokens
SKILL.md length
1,712 words
Files
1
Skills in repo
2
Repo updated
First seen
Licence
AGPL-3.0

At a glance

A skill your agent uses when adding a new messaging channel to Tel-Agent or changing an existing one — a transport under api/channels/, its official extension manifest, its settings card, tests…

  • Works in 6 steps: The address is a long random path… → Verify the platform's signature over the… → One refusal for every reason. Unknown… → …
  • Adding a new messaging channel to Tel-Agent
  • SKILL.md covers What a channel consists of, The transport module surface, The setup descriptor and Rules that are not negotiable, plus 2 more sections
  • Calls ruff, pytest and mypy; reaches developers.line.biz

What it does

Channel Extension is an agent skill from Dpro-at/Tel-Agent. Use when adding a new messaging channel to Tel-Agent or changing an existing one — a transport under api/channels/, its official extension manifest, its settings card, tests, health and docs. Covers the file checklist, the two inbound shapes (dial-out and the public door), the credential and signature rules that are not negotiable, the declarative setup descriptor the generic card draws from, and the test shape every channel ships with.

Its SKILL.md is about 4.1k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.

It works with Discord. The repository describes itself as: AI phone assistant | open-source. The licence is AGPL-3.0.

When your agent uses it

  • Adding a new messaging channel to Tel-Agent
  • Changing an existing one — a transport under api/channels/
  • Its official extension manifest
  • Its settings card

Example prompts

  • “/channel-extension”

Requirements

  • Python 3

Workflow steps

6 steps, taken from the first numbered list in SKILL.md.

  1. The address is a long random path (secrets.token_urlsafe(24)), unique per channel,
  2. Verify the platform's signature over the raw body bytes, before parsing anything,
  3. One refusal for every reason. Unknown path, wrong signature, disabled channel,
  4. Acknowledge first, answer after. Return the platform's expected 200 immediately;
  5. Dedup by the platform's own message id before storing. Retries are normal.
  6. Signal, Matrix, Mattermost, IRC, iMessage and every other dial-out channel expose

What it can do on your machine

Read from SKILL.md and the folder at commit 931afa4. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    Shell commands in SKILL.md call:

    • ruff
    • pytest
    • mypy
    • npm
    • npx

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    Hosts in commands or code, which the agent is likely to contact:

    • developers.line.biz

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Channel Extension loads about 4.1k tokens when it runs. Until then it costs about 115 tokens; SKILL.md has 1,712 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~115
When it runs · the whole SKILL.md, loaded when a task matches
~4.1k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from Dpro-at/Tel-Agent at commit 931afa4, republished under its AGPL-3.0 licence (© Dpro-at). 1,712 words, ~4,141 tokens.

Download SKILL.mdSave it as .claude/skills/channel-extension/SKILL.md (or your agent's skills folder).
name
channel-extension
description
Use when adding a new messaging channel to Tel-Agent or changing an existing one — a transport under api/channels/, its official extension manifest, its settings card, tests, health and docs. Covers the file checklist, the two inbound shapes (dial-out and the public door), the credential and signature rules that are not negotiable, the declarative setup descriptor the generic card draws from, and the test shape every channel ships with.

Building a channel extension

You are adding a messaging channel to Tel-Agent. A channel is a route a customer uses to reach a business — someone is typing on the other end. If nobody outside the business is on the other end, it is an integration, not a channel, and it is reached through the HTTP tool instead (Rule 5 in CLAUDE.md). Do not build it here.

Read first, in this order: CLAUDE.md (the contract), docs/SPEC.md §B9 (secrets) and §B13 (channels), then api/channels/discord.py end to end. Discord is the model every channel copies: a dial-out transport with a supervisor loop, an answering policy, storage with dedup, and delivery-before-storage. For a channel that receives webhooks, also read api/channels/meta_chat.py and api/routes/meta_chat_channels.py — that is the public door, and its guards are the whole of this product's exposed surface.

Write everything from the platform's official documentation and from the shape of the existing modules. Write it fresh. Never paste code from another product into this repository, never name another product anywhere in it, and never add a runtime dependency — httpx, websockets, cryptography and the standard library cover every platform we ship.

What a channel consists of

One kind, one transport module, one manifest, one descriptor, one test file. In order:

#PathWhat goes there
1api/channels/<kind>.pyThe transport. Module surface below.
2api/extensions/builtin/<kind>.pyMANIFEST + register(context). Copy the Discord one; change slug, name, description. Category channels, origin official, scopes conversations.write, messages.read, messages.write, hooks message.received, ui_slots conversation.detail.
3api/extensions/builtin/__init__.pyAppend the module to BUILTIN.
4api/models/conversation.pyThe kind is already in CHANNEL_KINDS (one migration added every kind of this wave). If yours is not, add it and an Alembic revision that widens the channel_kind CHECK constraint — tests/test_migrations.py runs it on SQLite and PostgreSQL.
5api/channels/generic.pyAdd the module's import path to _DECLARED. The registry imports it on first read; that is what serves its card and its routes.
6api/main.pyDial-out channels: append transport.loop(sessionmaker) to background. Door channels: nothing — the generic public route dispatches by kind.
7tests/test_<kind>_channel.pyThe test file. Shape below.
8web/components/brands/marks.tsx + source/<kind>.svgThe mark, when the platform has a logo. Record the source and licence in web/components/brands/README.md. A channel with no owner (SMS, IRC) gets a drawn glyph in web/components/shell/channel-mark.tsx.
9web/app/[locale]/settings/settings.tsxAdd the kind to GENERIC_CHANNELS. The card draws itself from the descriptor.
10locales/en/web.jsonThe display name under "channels". Other languages follow through the translation pipeline; do not touch thirty-four files.
11docs/SPEC.md §B13, README.md icon row, docs/brand/channels/One row, one icon, one table line.

The transport module surface

Every module exposes the same names, so the generic route, the health rollup and the tests can treat them alike. Signatures are the ones api/channels/discord.py has; keep them.

python
KIND = "<kind>"
SETUP = Setup(...)  # the descriptor, see below
INBOUND = "dial_out"  # or "door"


def make_client() -> httpx.AsyncClient: ...  # tests monkeypatch this
async def probe(client, credentials: dict[str, str]) -> str:
    ...
    # "Test connection". Returns the identity the platform reports (bot name, number,
    # account) — the card shows it. Raises ChannelRefused on a bad credential.


async def send_text(client, credentials, target: str, text: str) -> None: ...
def message_text(event, identity) -> str | None: ...  # the answering policy: None = ignore
async def ingest(db, channel, event) -> int | None:
    ...
    # store one inbound line; None when it was a duplicate or not for us


async def respond(sessionmaker, channel_id: int, message_id: int) -> None: ...
def schedule_reply(sessionmaker, channel_id: int, message_id: int) -> None: ...


# optional
def credentials_changed(channel_id: int) -> None:
    ...
    # What the module is told after the operator writes this channel's fields, so it can
    # drop anything it cached on the strength of the old ones — an access token bought
    # with a secret that has just been replaced would otherwise keep working until it
    # expired, which is a rotation that did not take effect. A module that caches nothing
    # declares nothing; `api/routes/generic_channel.py` calls it only when it is there.


def reply_target(conversation) -> str | None:
    ...
    # The address `send_text` needs for this thread, when it is not the conversation's
    # own `external_id` — a room id, a thread key, a mailbox. Declare nothing and
    # `generic.reply_target_of` falls back to `external_id`. It is what the human
    # takeover route delivers to as well, so a channel that answers somewhere other
    # than where the customer wrote from owes this one function and nothing else.


# dial-out only
async def loop(sessionmaker) -> None: ...  # the supervisor, reconciles every 15 s


# door only
async def receive(db, channel, request: Request) -> Response:
    ...
    # verify, dedup, store, schedule, acknowledge. One refusal for every failure.

credentials is the decrypted JSON of Channel.credentials_encrypted — a dict keyed by the descriptor's field names. Never read it on the inbound hot path for anything other than a signature check; per-channel state that every message needs lives in settings_json, which is plain.

Shared helpers — use these, do not copy Discord

api/channels/generic.py holds the half of a transport that is the same on every channel. A declarative channel writes the platform-specific half only: its descriptor, its signature check, probe, send_text, message_text, ingest, and its receive or loop. split_text is a line delegating to split_on_words below. Everything below is already written.

HelperWhat it does
channels()The registry, populated on first read. module_for(kind) and dial_out_modules() go through it, so no import order can change what an installation has. Never read CHANNELS directly.
public_url_for(request, settings)The address the platform called, for a signature computed over it. PUBLIC_BASE_URL when the installation has one, else the forwarded headers — the one place in this product where those are read, and the reasoning is in the function. The settings card builds its webhook_url on the same base, so the two cannot disagree.
conversation_for(db, channel, external_id, title=None)This customer's open thread, and whether it was just started.
store_line(db, conversation, role, text, language=None)One line into the transcript.
seen_before(conversation, kind, message_id)Dedup. A ring of the last 32 platform ids in state_json[kind], because a platform does not retry in order.
announce(db, channel, conversation, message, started)The conversation.started and message.received hooks.
preview(text)One line of a customer's words, for a notification.
reply_target_of(module, conversation)Where a reply goes: the module's reply_target, else external_id.
split_on_words(text, limit)The cut every channel shares: pieces no longer than limit, never inside a word, never silently short, and a single word longer than the limit cut rather than dropped. A module's split_text is one line handing this its own MESSAGE_MAX.
deliver(module, client, credentials, target, text)One answer out, cut into the platform's messages through the module's split_text.
schedule_reply(sessionmaker, module, channel_id, message_id)The answer as its own task, held so it cannot be collected mid-reply.
respond(sessionmaker, module, channel_id, message_id)The whole answer path: its own session, takeover read before generating and again before sending, delivery before storage, health timing.

A module still exposes respond and schedule_reply itself — the registry requires them — but each is one line handing the shared one this module:

python
def _self() -> ModuleType:
    return sys.modules[__name__]


async def respond(sessionmaker, channel_id: int, message_id: int) -> None:
    await generic.respond(sessionmaker, _self(), channel_id, message_id)

The human takeover route needs no branch per channel: api/routes/conversations.py delivers through the registry for any kind on it.

The setup descriptor

The card, the write route and the test button are generic. What makes one channel's card differ from another's is this object, declared once in the transport module:

python
from api.channels.setup import Field, Setup

SETUP = Setup(
    kind="line",
    title="LINE",
    note="An official account from your own LINE Developers console answers people who message it.",
    guide_url="https://developers.line.biz/en/docs/messaging-api/getting-started/",
    fields=(
        Field(
            "channel_secret",
            "Channel secret",
            secret=True,
            help="From the Basic settings tab of your channel.",
        ),
        Field(
            "channel_access_token",
            "Channel access token",
            secret=True,
            help="Issue a long-lived token on the Messaging API tab.",
        ),
    ),
    verified_live=False,  # True once a real message has gone through on a customer's account
)
  • secret=True fields are write-only: the API returns a masked preview (last four characters), never the value. secret=False fields are returned as stored.
  • required=False fields may be left empty. The card shows them under "Optional".
  • The English label and help are what the card shows unless a locale key ch_<kind>_<field> / ch_<kind>_<field>_help exists. Do not add locale keys for a new channel yourself; the translation pipeline does.
  • verified_live=False prints "built against the published API, not yet verified with a live account" on the card. It is honest and it stays until someone flips it after a real message.
  • A door channel's card also shows its public URL, built from Channel.webhook_path.

The generic routes are GET /api/channels/{kind} (descriptor + state), PUT /api/channels/{kind} (fields, enabled), POST /api/channels/{kind}/test, and for doors GET|POST /public/{kind}/{webhook_path}. They live in api/routes/generic_channel.py and api/routes/public_channel.py; you do not write routes for a new channel.

Show full SKILL.md (689 more words)Show less

Rules that are not negotiable

Credentials — §B9. Encrypted at rest in Channel.credentials_encrypted, one JSON object for all of a channel's secrets. Write-only: an empty string clears the field, a value starting with • or * is an echoed mask and is ignored, anything else is stored. Clearing the last required secret switches the channel off. Storing requires the installation's encryption key (409 encryption_key_missing without it). Never log a credential, never return one, never put one in a URL. Every write is audited.

The customer's own application. Tel-Agent holds no shared platform app. Every credential comes from the customer's own developer account. If a platform only works through an application the vendor owns, it cannot be a Tel-Agent channel.

The door — for every channel that receives webhooks.

  1. The address is a long random path (secrets.token_urlsafe(24)), unique per channel, stored in Channel.webhook_path. The route is /public/{kind}/{webhook_path}.
  2. Verify the platform's signature over the raw body bytes, before parsing anything, with hmac.compare_digest. A JWT is verified against the issuer's published keys with cryptography, checking iss, aud, exp. No signature scheme, no channel.
  3. One refusal for every reason. Unknown path, wrong signature, disabled channel, malformed body — all of them return the same 403 not_recognised. The door leaks nothing about what exists behind it.
  4. Acknowledge first, answer after. Return the platform's expected 200 immediately; generate and deliver the reply in a scheduled task. Platforms retry slow webhooks and you will answer twice.
  5. Dedup by the platform's own message id before storing. Retries are normal.
  6. Signal, Matrix, Mattermost, IRC, iMessage and every other dial-out channel expose nothing. Prefer dial-out whenever the platform offers it; the LAN installation is the product's reason to exist.

Answering policy. A direct message is always answered. A group, room, channel or space message is answered only when the bot is addressed, and the mention is stripped before the text reaches the model. A message from the bot itself, or from another bot, is never answered.

Delivery before storage. respond() generates the whole reply, delivers it, and only then writes the agent line. If delivery raises, nothing is written — a transcript must never show an answer the customer did not receive. Human takeover is checked before generating and again before sending.

Reconnection. Dial-out loops reconnect with exponential backoff from 5 s to 300 s and report their state through api/channels/health.py so the health page can say "connected" or "reconnecting" truthfully.

Text limits. Every platform has one. Split on word boundaries at the platform's limit; never truncate silently.

Nothing new in pyproject.toml. If you believe a platform genuinely cannot be done without an SDK, stop and say so in the report instead of adding it.

The test file

tests/test_<kind>_channel.py, modelled on tests/test_discord_channel.py. A fake platform, not mocked functions:

python
class FakeLine:
    def __init__(self) -> None:
        self.sent: list[dict] = []
        self.refuse = False

    def handler(self, request: httpx.Request) -> httpx.Response: ...  # route by path
    def client(self) -> httpx.AsyncClient:
        return httpx.AsyncClient(
            base_url="https://line.test", transport=httpx.MockTransport(self.handler)
        )

The stage fixture builds a workspace, a channel row with credentials, three users at admin / reception / viewer, monkeypatches make_client, and starts the real app. Copy it.

Every channel's file has at least these, named for what they prove:

  • a direct message is answered, and a message from a bot never is
  • a group message is answered only when addressed, and the mention is stripped
  • a repeated event is dropped by the platform's own id
  • a taken-over thread gets no generated reply
  • a refused send leaves no agent line in the record
  • the secret goes in and only a mask comes out
  • removing the secret switches the channel off with it
  • a viewer reads and never writes
  • the test button reports the platform's identity, and reports refusal
  • door channels: a wrong signature, an unknown path and a disabled channel all get the same refusal; the platform's verification handshake succeeds
  • dial-out channels: the loop starts a connection for an active channel and stops it when the channel is disabled

Run the suite for your file and the shared ones, then the gate:

bash
pytest tests/test_<kind>_channel.py tests/test_channel_health.py tests/test_extensions.py -q
ruff check . && ruff format --check . && mypy .
npm --prefix web run lint && npx --prefix web tsc --noEmit

Before the commit

  • No secret, no real phone number, no product other than the platform itself named.
  • English only. Conventional commit: feat(channels): add <kind>.
  • The card renders with no credentials saved, with a secret saved (mask shown), and after a test — check all three in the browser before calling it done.

© Dpro-at, AGPL-3.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

Just SKILL.md in .claude/skills/channel-extension of Dpro-at/Tel-Agent.

Open the folder on GitHubat commit 931afa4

Compare with similar skills

Channel Extension next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Channel Extension compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Channel Extension this skillDpro-at/Tel-Agent1.1k—~4.1kAutomated safety check: PassAGPL-3.0
Agent Browser CLIvercel-labs/agent-browser44k24 repos~864Automated safety check: PassApache-2.0
Electron App Automationvercel-labs/agent-browser44k5 repos~1.7kAutomated safety check: PassApache-2.0
Claude To Imop7418/Claude-to-IM-skill2.9k—~3.4kAutomated safety check: NotesMIT
Sap Extension Creatorheshengtao/super-agent-party2.7k—~6kAutomated safety check: PassAGPL-3.0
Libtv Videonexu-io/nexu3.3k—~3.5kAutomated safety check: PassMIT

Similar skills

  • Agent Browser CLI

    vercel-labs/agent-browser

    Official

    Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling forms, clicking buttons, taking…

    44k GitHub starsUsed in 24 repos~864 tokens
    Productivity & AutomationAuto-check passed
  • Electron App Automation

    vercel-labs/agent-browser

    Official

    Automates Electron desktop apps such as VS Code, Slack or Discord by connecting agent-browser to their Chrome DevTools Protocol port.

    44k GitHub starsUsed in 5 repos~1.7k tokens
    Productivity & AutomationAuto-check passed
  • Claude To Im

    op7418/Claude-to-IM-skill

    Bridge THIS Claude Code or Codex session to Telegram, Discord, Feishu/Lark, QQ, or WeChat so the user can chat with Claude from their phone.

    2.9k GitHub stars~3.4k tokensUpdated 6 mo ago
    Productivity & AutomationAuto-check: notes
  • Sap Extension Creator

    heshengtao/super-agent-party

    Create Super Agent Party (SAP) extensions. An agent skill from heshengtao/super-agent-party.

    2.7k GitHub stars~6k tokensUpdated 1 mo ago
    Frontend & DesignAuto-check passed
  • Libtv Video

    nexu-io/nexu

    Seedance 2.0 video & image generation via LibTV Gateway - AI text-to-video, image-to-video, video continuation, style transfer, and text-to-image using Seedance 2.0 model.

    3.3k GitHub stars~3.5k tokensUpdated 5 mo ago
    Media & CreativeAuto-check passed
  • Send User Message

    TinyAGI/tinyagi

    Send a proactive message to a paired user via their channel (Discord, Telegram, or WhatsApp).

    3.6k GitHub stars~829 tokensUpdated 6 mo ago
    Productivity & AutomationAuto-check passed

More from Dpro-at/Tel-Agent

  • Contributing

    Dpro-at/Tel-Agent

    A skill your agent uses when contributing to Tel-Agent - picking an issue to work on, setting up the repository for the first time, starting or finishing a task, opening a pull request, or asking…

    1.1k GitHub stars~3.3k tokensUpdated yesterday
    Auto-check: notes

Works with

Questions about Channel Extension

What does Channel Extension do?

A skill your agent uses when adding a new messaging channel to Tel-Agent or changing an existing one — a transport under api/channels/, its official extension manifest, its settings card, tests…. Channel Extension is an agent skill from Dpro-at/Tel-Agent. Use when adding a new messaging channel to Tel-Agent or changing an existing one — a transport under api/channels/, its official extension manifest, its settings card, tests, health and docs.

When should I use Channel Extension?

Channel Extension fits situations like: adding a new messaging channel to Tel-Agent; changing an existing one — a transport under api/channels/; its official extension manifest; its settings card.

How do I install Channel Extension in Claude Code?

Run `npx skills add Dpro-at/Tel-Agent --skill channel-extension -a claude-code`. Or copy the skill folder (.claude/skills/channel-extension in Dpro-at/Tel-Agent) into .claude/skills/channel-extension in your project. Claude Code loads it when a task matches its description.

How do I install Channel Extension in Codex?

Run `npx skills add Dpro-at/Tel-Agent --skill channel-extension -a codex`. Or copy the skill folder (.claude/skills/channel-extension in Dpro-at/Tel-Agent) into .agents/skills/channel-extension in your project. Codex loads it when a task matches its description.

Can I use Channel Extension in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add Dpro-at/Tel-Agent --skill channel-extension -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/channel-extension, .gemini/skills/channel-extension, .github/skills/channel-extension and .opencode/skills/channel-extension in your project.

What does Channel Extension need to run?

Going by SKILL.md and its folder, Channel Extension needs the command-line tools its instructions call (ruff, pytest, mypy, npm and npx). Our summary lists: Python 3.

Does Channel Extension access the network?

SKILL.md names 1 domain. In commands or code: developers.line.biz; the agent is likely to contact it when it follows the instructions. This is read from the text; nothing was executed.

Is Channel Extension safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.

What licence does Channel Extension use?

Channel Extension is published under the AGPL-3.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Channel Extension use?

About 4.1k tokens (SKILL.md is roughly 17k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to Channel Extension?

Skills that share tags, products or a category with Channel Extension: Agent Browser CLI (vercel-labs/agent-browser, 44k stars), Electron App Automation (vercel-labs/agent-browser, 44k stars), Claude To Im (op7418/Claude-to-IM-skill, 2.9k stars) and Sap Extension Creator (heshengtao/super-agent-party, 2.7k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Channel Extension?

Dpro-at (a GitHub organization) maintains it in Dpro-at/Tel-Agent, which has 1,097 GitHub stars. The repository holds 2 skills in this directory. The repository was last updated on October 8, 2026.

Source: Dpro-at/Tel-Agent on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.