# Build and prove a CopilotKit Channels agent

Inspect the target project now. Build or reuse the smallest headless agent
service that fits the agreed job. Use the public CopilotKit CLI to create and
reconcile its managed Channel, then stay with the work until a real Slack or
Microsoft Teams message gets a useful agent reply.

The setup path and message path are different:

```text
Setup:   CopilotKit CLI → declared config → managed Channel → provider connection
Message: Slack or Teams → CopilotKit Intelligence → long-running runtime
         → AG-UI agent → CopilotKit Intelligence → Slack or Teams
```

The message path is **two legs with two different mechanisms**, and getting this
wrong is the most expensive mistake available here, because a misconfigured app
installs cleanly and answers nothing.

| Leg                         | Mechanism                                                                         | What authenticates it                                                                                             |
| --------------------------- | --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| Slack → Intelligence        | Slack posts events over **HTTPS** to an Intelligence-hosted request URL           | the app's **signing secret**, held by Intelligence                                                                |
| Teams → Intelligence        | Teams posts a **Bot Framework** activity over HTTPS to an Intelligence-hosted URL | a Microsoft-signed **bearer JWT**, verified against the **Entra application identity** and routed by its audience |
| Intelligence → your runtime | your runtime dials **out** over a websocket                                       | the project API key                                                                                               |

The inbound leg is **not** one mechanism with two skins. Reaching for a signing
secret on Teams, or a bearer JWT on Slack, produces an app that installs cleanly
and authenticates nothing.

Two consequences worth stating to the user before they ask:

- **No tunnel and no public URL of your own is needed** — but not because of Slack
  Socket Mode. It is because Intelligence owns the public URL, and the second leg
  is outbound from their machine.
- **Socket Mode stays off, and there is no `xapp-` app-level token anywhere in this
  workflow.** A managed Slack app needs `socket_mode_enabled: false` and a request
  URL. Created with Socket Mode on and no request URL, no event ever arrives.

## What the managed path does not deliver on managed Slack

These are Slack event-plumbing limits. Say them up front rather than letting the
user debug them, and never invent a request URL to fill a gap. **Do not carry this
list over to a Teams run** — these event names are Slack's, and this guide does not
state the equivalent Teams limits, so do not assume parity in either direction:

- **No `slash_commands`.** Slash commands never arrive.
- **Interactivity is on and `block_actions` is handled**, so HITL buttons and
  selects **do** fire.
- **`view_submission` is not handled**, so modals **do not**.

An app registering a command or modal handler compiles, starts, reports `online`,
and never fires them. If the target project has such handlers, name them as dead on
this path before you build on them.

## Done means

1. The agreed first message gets a model-backed answer through the local agent.
2. **Custom build:** the project has tried `@copilotkit/runtime@latest` and
   `@copilotkit/channels@latest` together. The resolved pair type-checks, and the
   user approved any fallback after seeing the exact conflict. **Fast start:** the
   clone's own resolved pair is recorded, and its `check-types` and `test` results
   are reported as observed. Do not upgrade a working clone to satisfy this gate,
   and do not assume the clone is green — a fresh clone can fail its own checks
   because of upstream drift, and this gate is met by reporting that accurately,
   not by producing a pass.
3. The CLI reports a completed Channel reconcile and a clean status without
   exposing a secret to the agent.
4. The long-running runtime reports both its overall state and the named Channel
   as `online`.
5. The Channel registers at least one component defined with
   `defineChannelComponent`, and the acceptance reply in gate 6
   renders it rather than plain text.
   The one documented exception is Phase 2's approved fallback: a pair that pins
   `@copilotkit/channels` below 0.7 cannot satisfy this gate, and the user's
   recorded choice stands in its place.
6. A real provider mention gets a useful reply, a follow-up in the subscribed
   conversation gets a reply, and an unrelated conversation stays silent.

**Gate 4 is where agents declare victory early.** Awaiting readiness resolves on
`setup_required` as well — a documented valid-but-degraded state, not a failure. A
runtime with **no provider connection at all** starts cleanly, prints its listening
line, serves HTTP 200 on its info endpoint, and answers nothing. That endpoint
reports license and runtime information, **not** channel state, so a 200 there is
not evidence of anything provider-related. Read the named Channel's status; do not
infer it from a resolved promise or a healthy port.

## Boundaries

1. Use `copilotkit channels add` and `copilotkit channels status` to provision the
   managed Channel. Do not substitute a private CLI build or the Intelligence web
   UI for them. `channels setup` is **not** a provisioning command — it installs
   the `channels-setup` skill that sent you here, so it is very likely how this
   run began; re-running it provisions nothing and gets you no further.
2. Never run `npx copilotkit@latest login` for the user. Check authentication,
   give the user that command when needed, and wait for them to finish it.
3. **Drive the provider sites yourself. That is the default.** Check for a browser
   or computer-use tool before Phase 1 and say which you found — do not guess.
   - **You have one**: take the single authorization below, then work the whole
     sequence without stopping again except for the exceptions under it.
   - **You do not**: say so, then **ask the user to install it before you start.**
     Treat that as the default path, not an optional upgrade. Work out which
     harness you are running in and name the one route that applies to it rather
     than reciting all of them — Claude Code and Codex each ship their own
     browser or computer-use support and enable it differently, most other
     harnesses take a general browser-use MCP server such as Playwright MCP, and
     the same agent run as a desktop app usually has more browser access than it
     does in a bare terminal. **If you are not sure what your harness supports,
     look it up before you guess.** Tell them what it buys: driving reduces this
     workflow to typing a few secrets, while the fallback is roughly fifteen
     manual steps. Only if they decline, switch to guided mode: one website action
     at a time, and do not click, type, copy, paste, inspect, or take screenshots
     in those sites.
4. Never ask for, read, transcribe, screenshot, or repeat provider tokens,
   signing secrets, model keys, API keys, or other secret values. This holds when
   you are driving, where a secret may be on screen in front of you: navigate to
   the value and let the user copy it. The user stores provider secrets under the
   CLI-emitted variable names in an ignored `.env` or secret manager.
5. Do not add a direct Slack or Teams adapter or an unrelated frontend.
   Intelligence owns provider transport and stored provider credentials.

Proceed with in-scope local reads, edits, installs, and tests. Ask once before a
hosted project write or another external write. Preserve unrelated local work.

Ask for provider-site authorization **once**, naming the whole sequence: create
the Slack or Teams app from the CLI-emitted manifest, install it into the
workspace or tenant they name, create the Channel, attach the adapter, and issue
a project key. One yes covers all of it.

Then go. **Do not re-ask per step, per page, or per click.** A run that pauses at
every control is slower than the manual path it replaced, which is the failure
this default exists to fix. Read each page before you act on it and never guess at
a control — but reading is not a reason to stop and check in.

Stop only for these, and never batch them away:

- A secret or password the user must type: provider tokens, signing secrets, API
  keys, model keys, and any login or MFA prompt.
- Something you cannot do: Teams tenant admin consent and the Teams package
  upload.
- Anything destructive, or any change outside the sequence they authorized.

If a browser or computer-use tool fails mid-run — a login wall, MFA, an expired
session, or bot detection — say so, degrade to guided mode, and continue from the
last proved state. Never attempt a CAPTCHA or bot check, and never sign in to a
provider account on the user's behalf.

If you end up in guided mode, say once how the user could switch you to driving
next time, using the routes in boundary 3. Say it once, then keep going — do not
stall waiting for an answer.

## Ask first: fast start or custom build

Two paths lead to a working Channel, and the wrong one wastes an hour. Settle
this before any other question, because it decides which of the questions below
still apply.

Ask it in your own words, not these:

> Do you want a working Slack bot as fast as possible, or are you building
> something specific around your own agent?

**Prefer the fast start unless something rules it out.** Take the custom path
only when one of these holds:

- The target directory already holds an app the user wants to keep.
- The user named an agent, framework, or codebase to build this around.
- The user wants Microsoft Teams. OpenTag's quick start is Slack.
- The user cannot run Python 3.12 and [`uv`](https://docs.astral.sh/uv/), which
  OpenTag's bundled agent needs, and does not want to repoint it at their own.

Say which path you picked and why in one line, then go. Do not present this as
a menu and wait.

### Fast start: clone OpenTag

[OpenTag](https://github.com/CopilotKit/OpenTag) is the complete, MIT-licensed
Channels application: a managed Channel, a Node runtime, a Python LangGraph
agent, native Slack UI, human approval before writes, and a live Slack harness.
It exists to be taken.

Cloning it replaces Phase 2 and most of Phase 4 with **verify and run**.
OpenTag's own README says the same thing about this guide, and it is right:

> Point your agent at the existing code to verify and run, not to rewrite.

Phase 1's inspect-and-authenticate steps, Phase 3, and Phase 5 are identical on
both paths — the CLI gate, the managed Channel, and the acceptance checks are the
same work either way. Phase 1's last step, Phase 2, and Phase 4 each carry their
own fast-start section, and the fast path also waives the framework and model
question below, reports "Done means" gate 2 instead of performing it, and
constrains which first message Phase 5 can accept. Work a phase's fast-start
items wherever it has them instead of the custom-build items beside them.

The framework and model question below does not apply on this path. OpenTag
ships a Python LangGraph agent and reaches it over `AGENT_URL`, so any
AG-UI-compatible agent can replace it later without touching the Channel. Say
that once instead of asking.

### Custom build

Everything else. Work Phases 1 through 5 in order as written.

## Use current public contracts

Read sources in this order:

1. The target project, its instruction files, package manifest, installed types,
   environment example, scripts, tests, and similar local code.
2. The exact public CLI `--help` output and the JSON envelope from the command
   being run.
3. The public
   [minimal Channel example](https://github.com/CopilotKit/channels-sdk/tree/main/examples/minimal-channel)
   for trigger subscriptions and listener lifecycle only.
4. Current public
   [Channels documentation](https://docs.copilotkit.ai/channels) and
   [AG-UI documentation](https://docs.ag-ui.com/).

**Custom build only.** CopilotKit integration starters are full agent-to-web
showcases. For a new app, use the CLI scaffold to get the supported hosted
project and environment wiring, then keep only the agent, runtime, scripts, and
configuration needed by the agreed headless service. Remove the unrelated web
frontend, Threads UI, A2UI, MCP Apps, and browser demo features.
**This does not mean removing generative UI — it means moving it.** In-channel
rendering is the point of Channels, and Phase 4 requires at least one Channel
component. Strip the browser surface, keep the rendering. Follow installed types
when an example or document disagrees with them.

**On the fast path this reduction does not apply at all.** There is no CLI
scaffold to reduce, and OpenTag already is the shape it aims at. OpenTag's `app/`
directory is the Channel surface, not a web frontend: `app/components/`,
`app/modals/`, and `app/render/browser.ts` are how it renders natively in Slack,
so they read like the names above and are the opposite of them. Take nothing out
of a clone you came here to verify.

## Ask only for missing inputs

Ask for no more than these choices, and do not re-ask what the conversation or
target files already answer:

1. Target directory, app name, and whether the app already exists.
2. The agent's real job, one first message, and what a useful answer must contain.
3. Slack or Microsoft Teams. Recommend Slack when the user has no preference.
4. An existing or preferred AG-UI framework and model. Preserve an existing
   framework.
5. **The bot's display name.** The Channel code derives from it, and that code is
   what `createChannel({ name })` declares. The Slack handle derives from the
   display name too — not from the Channel code, which is only a fallback for when
   the label cannot become a safe Slack identity — so do not construct the invite
   command yourself. The CLI emits the invite command once the adapter is
   attached; present that verbatim. Bot names are workspace-wide, so a collision
   blocks the install. Suggest one, but do not settle it yourself —
   it is the bot's identity in their workspace.
6. **Which workspace or Team** the app installs into, and **which channel to test
   in.** Never assume the workspace their browser session happens to be signed
   into. The test channel has to be somewhere they can post and somewhere a bot
   reply is welcome, because the Phase 5 trigger checks run there.

**Driving does not mean deciding.** Items 5 and 6 are inputs you cannot invent, and
they are separate from the one-time provider-site authorization: collapsing that
authorization does not license skipping these. Ask for everything missing in one
exchange, state the answers back, and then proceed without coming back. If the user
defers a choice, say what you are defaulting to rather than silently picking.

Default to a local, long-running Node.js host. If the chosen agent framework runs
in another process or language, keep that service and connect it through its
documented AG-UI endpoint.

At the end of each major phase, show only:

```text
Passed: <facts proved in this phase>
Blocked: <one exact blocker, or none>
Next: <one action that can be done now>
```

## Phase 1: inspect, authenticate, and prepare the project

1. Inspect the target directory, Git state, package manager, Node version,
   instruction files, ignore rules, existing agent code, and runtime code.
2. Run the public CLI gate and record the version that answered:

   ```sh
   npx --yes copilotkit@latest version
   npx --yes copilotkit@latest channels --help
   ```

   If `channels` is missing, stop with the observed version. Do not use a private
   checkout, invent a tag, or fall back to the web UI. Replace `<cli-version>`
   below with the observed version for the rest of this run.

3. Check CopilotKit authentication with:

   ```sh
   npx copilotkit@latest whoami
   ```

   Do not rely on the exit code alone. Authentication passes only when the output
   shows a signed-in account and organization. Do not repeat its personal details
   in chat or reports.

4. If the session is missing, expired, or invalid, tell the user exactly:

   > Run `npx copilotkit@latest login` in your terminal. Complete sign-in, then
   > tell me when it finishes.

   Stop and wait. After the user confirms, run `whoami` again and continue only
   when it proves authentication. If a later command asks for sign-in, return to
   this step.

5. **Fast start:** clone OpenTag into the target directory. It must be empty or
   not yet exist — never clone over an app the user already has.

   ```sh
   git clone https://github.com/CopilotKit/OpenTag.git <target-dir>
   ```

   The clone carries OpenTag's Git history. Ask whether the user wants their own
   repository, and only if they say yes remove `.git` and run `git init` in its
   place. Some people clone deliberately to track upstream; do not decide that
   for them.

   OpenTag's prerequisites are **Node.js 22+**, pnpm, Python 3.12, and
   [`uv`](https://docs.astral.sh/uv/). Check the Node major before you install;
   an older one fails partway through and reads as a broken clone.

   **Install now; start nothing yet.** Installing and running are separate
   steps here, and doing them in one breath is the most common way this path
   fails. `pnpm install` installs the Node dependencies and stops there. There
   is no separate `uv` step under `agent/`: the locked Python environment and
   Playwright's Chromium both come from the `predev` hook that runs before
   `pnpm dev`, which syncs with `uv sync --project agent --locked` **from the
   repository root**.

   Copy `.env.example` to `.env`, confirm `.env` is ignored, and keep
   `AGENT_URL` exactly as it ships: it is a genuine working default and is
   required with no fallback. **`INTELLIGENCE_CHANNEL_NAME` is not a default to
   keep.** It ships `open-tag`, the name OpenTag's own production deployment
   claims, and its `.env.example` annotates that line
   "Forks: use your own name." Managed delivery is claim-based — two runtimes
   declaring the same Channel name in one Intelligence project race per
   delivery, and the loser silently receives nothing, so the tell is a Slack
   reply this terminal knows nothing about. Phase 2 replaces the value with the
   code derived from the display name the user chose.

   `INTELLIGENCE_API_KEY` and `OPENAI_API_KEY` are the two that need real
   values, by name — and neither is unset after the copy. `.env.example` ships
   them as the placeholders `cpk-...` and `sk-...`, so a plain `cp` leaves both
   **set to a fake value**, which is why "the variable is set" proves nothing
   about either one. `project select` below replaces the first; the user
   replaces the second. The check that discriminates is that the shipped
   placeholder is gone, so compare the two files rather than hardcoding what
   ships: for each of those two names, the line in `.env` must differ from the
   line `.env.example` carries. Do not test for the literal `cpk-...` — if
   upstream ever changes its placeholder text, a literal check stops matching
   and reports success on a value that is still fake. Comparing `.env` against
   the published `.env.example` is not reading a secret, and the boundary is
   unchanged: never read, transcribe, screenshot, or repeat a real key. That is not the whole inventory either: Phase 3 adds the provider
   variables the CLI names in `requiredEnvVars`, so do not report the secret
   list as closed.

   Then run `npx --yes copilotkit@<cli-version> project select` **from the
   target directory**, showing the user the list before they pick. Run it after
   the `.env` copy, not before. This is where `INTELLIGENCE_API_KEY` comes from:
   on success the CLI mints a project-scoped runtime key for the chosen project
   and writes that variable into the `.env` of the directory it ran in — the
   "issue a project key" step the one provider-site authorization already
   covers. Confirm only that the variable no longer holds the shipped `cpk-...`
   placeholder; never read or repeat the value the CLI wrote. Provisioning is
   separate from the selection and can fail without failing the command, so if
   it reports that it could not provision a key, or the line still reads
   `cpk-...` afterwards, ask the user to issue a project-scoped key for that
   project themselves and set `INTELLIGENCE_API_KEY` in the ignored `.env`. Do
   not leave the placeholder in place and continue: nothing later in this
   workflow produces a real key, and a runtime that starts with a fake one
   starts cleanly, serves HTTP 200 on its info endpoint, and answers nothing.
   `project select` writes into `.env`, which is read once at startup, so if a
   runtime is already running from an earlier attempt, restart it and prove
   `online` again rather than assuming it picked the key up.

   **Only now start it.** `pnpm dev` runs the runtime and the Python agent
   together, and both of the paragraphs above are prerequisites rather than
   follow-up steps: the runtime's `app/env.ts` throws on an absent `AGENT_URL`
   or `INTELLIGENCE_API_KEY`, and `dev` is a `concurrently --kill-others` script,
   so that throw takes the Python agent down with it. A start attempted before
   `.env` is real therefore dies as a pair, several minutes into the run, and
   reads as a broken clone or a bad Node major when it is neither. Expect that
   first `pnpm dev` to sit on the Chromium download for a while; it is not a
   hang. It is a long-running foreground process, so give it its own terminal or
   start it in the background and continue the workflow alongside it — do not
   wait for it to exit. To start the agent alone later, OpenTag's own script is
   `pnpm dev:agent`.

   **Custom build:** for a new app, confirm the hosted write, then run:

   ```sh
   npx --yes copilotkit@<cli-version> create --name <app-name> --framework <framework>
   ```

   Omit `--framework` when the user has not chosen one and use only an identifier
   the live CLI offers. For an existing app, preserve its layout and run
   `npx --yes copilotkit@<cli-version> project select` only when it has no selected
   hosted project. Show the user the projects that command lists and let them pick
   one; do not ask whether they want a new project without their existing ones on
   screen. If `create` offers a Channel, choose **Not now** so Phase 3 can
   use the JSON flow. Reduce a new scaffold to the agreed headless service.
   Confirm `.env` is ignored and required variables are present by name only. On
   an existing-app path, stop if the CLI did not supply every hosted URL the
   runtime needs; do not guess them.

## Phase 2: build the real agent

### Fast start: verify what OpenTag already ships

On this path you are not building an agent. OpenTag has one. Do this instead of
this phase's **Custom build** items 1 to 5, then go to Phase 3 — the items
directly below are this fast start's own, and you work all five. Skipping
**Custom build** item 1 skips the `@latest` upgrade with it: OpenTag ships its own
`@copilotkit/runtime` and
`@copilotkit/channels` pair, so record the resolved versions and report them
against "Done means" gate 2 instead of upgrading a working clone. Whether that
pair passes the clone's own checks is something you observe in item 1 below, not
something to assume here.

1. Run the repository's own `check-types` and `test` commands and record both
   results. **Do not assume the clone is green.** If a fresh clone fails its own
   checks before you have changed anything, report it as upstream drift in the
   clone, not the user's environment — a dependency bump that outran its own test
   doubles looks exactly like this, and it has happened. Name the failing file and
   the error, then ask the user whether to fix the clone in place, continue on a
   known-good older revision, or switch to the custom build. Do not silently edit
   someone else's repository, and do not report this phase as passed while a check
   is red.
2. Start the Python agent with OpenTag's own script — `pnpm dev` for both
   services, or `pnpm dev:agent` for the agent alone — and prove it answers on
   `AGENT_URL` (`http://localhost:8123/` by default). If
   `OPENAI_API_KEY` is unset, name only the variable and the ignored file, and
   continue independent work.
3. **Customize lightly, and stop there.** Change only the persona in
   `agent/prompts/system.py` so the bot matches the job the user described.
   Leave the Channel lifecycle, the tools, the components, and the runtime
   alone. Every other seam is named in OpenTag's "Make it yours" table and can
   wait until the bot is live and answering.
4. **Set the Channel name in all three places.** OpenTag ships `open-tag`, and
   that is a value to replace rather than keep: it is the name OpenTag's own
   production deployment claims, and a second runtime declaring it in the same
   Intelligence project loses deliveries to a silent race — no error, no log
   line, just a Slack reply your terminal knows nothing about.
   Whatever display name the user chose, the code you derive from it has to
   be identical in `INTELLIGENCE_CHANNEL_NAME`, in the `--name` you pass in
   Phase 3, and, once Phase 3 creates it, in `.copilotkit/channels.json` —
   character for character. Change one and not the
   others and you get a runtime that starts, reports its port,
   and is connected to nothing.
   This one lives in `.env`, which is read once at startup, so a `pnpm dev`
   already running when you rename it keeps declaring `open-tag` and keeps
   losing that race. Restart the process after the rename and prove `online`
   again.
5. **The component gate is already met.** OpenTag registers its components in
   `app/channel.tsx`, and `app/tools/render-chart.tsx` is a
   `defineChannelComponent`.
   Confirm it is still in the `components` array rather than adding
   another, and skip Phase 4's component step.
   Phase 5 still has to watch it render, so pick an agreed first
   message that reaches it — sending a `.csv` and asking for a chart is the one
   OpenTag was built around.

### Custom build

1. Use the target's package manager to try
   `@copilotkit/runtime@latest` and `@copilotkit/channels@latest` in one install
   command. Upgrade both together, inspect their resolved types, and type-check.
   If the pair cannot install or compile, restore both prior versions, report the
   exact conflict, and stop before Channel setup. Continue on an older compatible
   pair only after the user approves that fallback. Never upgrade only one
   package.

   `defineChannelComponent` is **0.7+**. If the only pair that installs and
   type-checks pins `@copilotkit/channels` below 0.7, say the Channel component
   gate cannot be met on that pair and let the user choose between the older
   pair and the component. Do not drop the gate silently.

   Say what that choice actually costs, because it is the newer API against the
   older pair and not in-channel UI against none: plain component functions
   registered on the Channel still render natively below 0.7, so a user who keeps
   the older pair keeps native rendering and loses only `defineChannelComponent`,
   which is what this gate asks for.

2. Build or reuse one AG-UI-compatible agent for the agreed job. Use a cloneable
   factory or the installed equivalent so conversations do not share mutable
   agent state.
3. Replace demo behavior with clear instructions for the agreed job. Add only
   the first real tool or data source it needs, and never invent tool results.
4. Add a focused smoke test for the agreed first message. It must exercise the
   model-backed agent, not a canned transport response.
5. Run the smoke test. If a model key is missing, name only its variable and the
   ignored file where the user should set it; continue independent work.

## Phase 3: create and reconcile the managed Channel

Use the display name and provider the user chose, derive the code from that name,
and confirm the hosted write is covered by their authorization. Do not invent a
name here. Then run:

```sh
npx --yes copilotkit@<cli-version> channels add \
  --name <channel-code> \
  --display-name "<display-name>" \
  --adapter <slack-or-teams> \
  --json
```

Save the full JSON envelope. The command may declare the Channel in
`.copilotkit/channels.json`, create it on the server, write provider artifacts,
attach the provider, or return `blocked`. A blocked result is a normal pause and
may exit zero.

For each envelope:

1. `completed`: preserve its diagnostics and continue to Phase 4.
2. `blocked`: read `nextAction.summary`, `nextAction.instructions`,
   `nextAction.caveats`, `nextAction.url`, `nextAction.linkFile`,
   `nextAction.requiredEnvVars`, `nextAction.artifacts`, and
   `nextAction.resumeCommand`.
3. `failed`: report the error code and message, diagnose it, and do not claim
   progress past that state.
4. Follow the emitted next action instead of replacing it with remembered
   provider steps.
5. Keep `.copilotkit/channels.json` tracked. Keep `.env` and
   `.copilotkit/artifacts/` ignored.

### Slack handoff — guided fallback

The user performs every Slack website action and handles every secret value.
Give one action at a time, then wait. Do not open Slack or ask the user to paste a
secret into chat.

1. Give the user the emitted link or `linkFile`. They open it, review the
   prefilled manifest, create the app, and choose their workspace.
2. They open **OAuth & Permissions** and choose
   **Reinstall to Workspace** → **Allow**.
3. Only after reinstall, they copy the **Bot User OAuth Token** from
   **OAuth & Permissions** and ignore the token in the app-creation modal.
4. They copy the **Signing Secret** from
   **Basic Information** → **App Credentials**.
5. They put both values in ignored `.env` under the exact names in
   `requiredEnvVars`, then report only that the variables are set.

Trust the user's confirmation and do not inspect `.env` contents. Run the emitted
`resumeCommand`; the CLI will report any missing variable. Replace only its
leading `copilotkit` executable with
`npx --yes copilotkit@<cli-version>`. Keep every emitted argument unchanged.
Repeat until the envelope says `completed`. If it reports
`slack_token_scopes_incomplete`, return the reinstall step to the user and wait
for the reissued bot token.

### Slack handoff — driving it yourself

This is the default path. You already have authorization for the whole sequence,
so run steps 1 to 3 straight through without checking in. The user's only job is
step 4.

1. Open the emitted link or `linkFile`, confirm the prefilled manifest's app name,
   scopes, and request URL match the emitted values, and create the app. Report
   what you created; do not ask first. Create a **dedicated** app — never reuse,
   reinstall, or modify one that is already installed and in use.
2. Choose the workspace the user named. If **Next** appears not to advance, scroll
   inside the modal — a required workspace field sits below the fold and blocks it.
3. Open **OAuth & Permissions** and choose **Reinstall to Workspace** → **Allow**.
   Slack applies the manifest's real scopes only on this reinstall, so it is not
   optional. **Installing is the gated step**, not creating: by default only
   Workspace Owners review app requests, though they may appoint app managers too.
   If the install needs approval, say so and keep going on everything that does not
   depend on it rather than idling until it clears.
4. Do not read either credential. Navigate to the **Bot User OAuth Token** on
   **OAuth & Permissions**, then to the **Signing Secret** on
   **Basic Information** → **App Credentials**, and at each one stop and ask the
   user to copy it into ignored `.env` under the exact name in `requiredEnvVars`.
   Ignore the token shown in the app-creation modal.
5. Resume exactly as in the guided path once the user reports the variables are
   set.

The bot token is reissued on reinstall, so treat a post-reinstall auth failure as
a stale stored **bot token** before you suspect a missing scope — `channels rotate`
exists for exactly this. By contrast, reinstall does not reissue the signing
secret; it is a separate credential copied from **Basic Information**, so rotating
it is not a fix for a post-reinstall failure. If any step
needs a credential, a payment method, or an account you cannot legitimately act
in, stop and hand that step to the user.

### Teams

Follow the same mode split and only the CLI-emitted Azure and Teams steps. Two
Teams gates stay user-owned in both modes, because no browser control can work
around either: granting tenant admin consent, which a non-admin cannot do, and
uploading the app package through **Apps → Manage your apps → Upload an app**. If
the adapter reports a setup failure while API permissions are empty and **Grant
admin consent** is greyed out, stop for the user's Teams admin instead of trying
another route. After attachment, give the user the CLI-emitted Slack invite
command or Teams package action. When the CLI emits
`setup_credentials_removable`, ask the user to remove those setup-only variables.

## Phase 4: connect the Channel runtime

### Fast start: verify OpenTag's existing runtime wiring

OpenTag already ships this phase, so verify it instead of building it, then go
to Phase 5. Read all of its runtime wiring before you decide anything is
missing, because it is split across four files: `server.ts` holds only the
lifecycle — readiness, HTTP listen and close, and `SIGINT`/`SIGTERM` — while the
`CopilotRuntime` and `CopilotKitIntelligence` construction lives in
`app/runtime-host.ts` and is composed in `app/index.ts`. The Channel surface —
triggers, tools, and components — lives in `app/channel.tsx`. Confirm those four
files satisfy the runtime-wiring items below rather than writing those items
yourself. An agent that reads `server.ts` alone finds no runtime construction,
concludes it is missing, and builds a second one: the working-runtime rewrite
this path exists to avoid.

Leave `INTELLIGENCE_CHANNEL_NAME` where it is. It is OpenTag's existing wiring
and the value you already aligned in Phase 2, not a second source of truth. The
item below about not adding one is about a name you introduce; it is no reason
to strip the name the shipped runtime already reads.

**Name OpenTag's commands and its modal handler as dead before you build on
them.** `app/channel.tsx` passes `commands: appCommands` — `/agent`, `/triage`,
`/preview`, and `/file-issue` from `app/commands/index.ts` — and registers
`channel.onModalSubmit(FILE_ISSUE_CALLBACK, fileIssueSubmit)` from
`app/modals/file-issue.tsx`. On the managed path none of them ever fire, for the
reasons under "What the managed path does not deliver": the code compiles,
starts, reports `online`, and never invokes them. They are kept for a
non-managed transport, not broken code, so leave them in place — but tell the
user that this clone's `/triage` and `/file-issue` do nothing here. OpenTag's
README advertises app-owned commands as a shipped feature, so the user will
otherwise expect them to work and find out from a colleague trying them.

**OpenTag awaits readiness but never inspects status.** All of it is
`controls.ready({ timeoutMs })` in `server.ts`; nothing under `app/` or in
`server.ts` reads a Channel's status outside the tests. That leaves unproved
exactly the half "Done means" calls load-bearing, because `ready()` resolves on
`setup_required` too. Do not add status inspection to a runtime you came here to
verify — observe it from outside instead. Phase 5's
`channels status --json` gives the hosted view, and starting the process with
the log level raised prints the Channel lifecycle breadcrumbs, including the
transition to `online`. The knob is `LOG_LEVEL`, and OpenTag documents it in
both places: `.env.example` carries a commented `export LOG_LEVEL=debug` line,
and its README raises it for one run with `LOG_LEVEL=debug pnpm runtime`. The
same prefix works on `pnpm dev`. Report the named Channel's status from what you
actually saw, and never infer it from a `ready()` that returned.

Two items below are operational facts rather than build steps, and this path
does not escape them. Raise the log level before you conclude anything from a
quiet log, and restart the process after any edit, because the runtime does not
hot-reload and a stale process is indistinguishable from a change that did not
work.

**The environment is the case that catches people here.** OpenTag's
`dev:runtime` is `tsx watch server.ts`, so a code edit does restart the process
on its own — but `.env` is loaded once by `dotenv/config` at startup and is
watched by nothing. This path mutates `.env` twice after the stack may already
be up: `project select` writes `INTELLIGENCE_API_KEY`, and Phase 2's item 4
renames `INTELLIGENCE_CHANNEL_NAME`. After either one, restart the process
yourself and prove `online` again. A runtime still holding `open-tag` is in the
claim race above, and that failure has no error and no log line — only a Slack
reply this terminal never sees.

### Custom build

1. Resolve the exact Channel name from `.copilotkit/channels.json`. Do not add a
   second hard-coded or environment-only source of truth.
2. Follow the installed `createChannel` and agent types. Use the public minimal
   example's trigger rule: `onMention` subscribes the thread before running the
   agent; `onMessage` runs it only when the thread is already subscribed.
3. Register handlers before activation. Pass the current inbound text and content
   parts in the shape the installed SDK accepts. Catch agent failures, log a
   secret-free error, and attempt one short visible failure reply.
4. **Register at least one Channel component.** Native in-channel UI is the
   reason to be on this platform at all; a bot that only posts text has used
   none of it. Define it with `defineChannelComponent` and pass it to
   `createChannel`:

   ```tsx
   import { defineChannelComponent, Message, Header, Context } from "@copilotkit/channels"
   import { z } from "zod"

   const IssueCard = defineChannelComponent({
     name: "issue_card",
     description: "Render an issue as a card.",
     parameters: z.object({
       id: z.string().describe("The issue's ticket number, e.g. ENG-123."),
       title: z.string(),
     }),
     render({ id, title }) {
       return (
         <Message>
           <Header>{title}</Header>
           <Context>{id}</Context>
         </Message>
       )
     },
   })
   ```

   ```ts
   createChannel({ name, agent, components: [IssueCard] })
   ```

   Registering it makes it a tool the agent calls to render UI itself. The
   schema is that tool's input schema, so `description` and the per-field
   `.describe()` text are what the model reads to decide when to use it — write
   them for a reader who cannot see the code. `render` also takes a second
   argument, `{ platform, signal }`, for platform-specific output or an
   in-flight abort; this example has no use for either and omits the
   parameter rather than declare it unused.

   **Choose one the agreed first message would naturally produce**, not a
   decorative extra. If the job is triage, render the issue; if it is a report,
   render the table. A component the agent never has a reason to call proves
   nothing in Phase 5.

   The JSX needs both `"jsx": "react-jsx"` and
   `"jsxImportSource": "@copilotkit/channels"` in `tsconfig.json`. TypeScript
   only honors `jsxImportSource` when `jsx` is `react-jsx`; under
   `"jsx": "react"` it is silently ignored and the tree compiles against
   React and fails anyway. Point `jsxImportSource` at `@copilotkit/channels`
   itself, not at `@copilotkit/channels-ui`, which is only
   a transitive dependency and does not resolve under pnpm.

5. Build one `CopilotRuntime` with `CopilotKitIntelligence` and the Channel. Keep
   provider transport out of this process. Read hosted values from the selected
   project's environment contract and do not guess missing URLs.
6. Use the installed Node lifecycle API in one long-running process. Handle
   `SIGINT` and `SIGTERM`, await Channel readiness, inspect Channel status, and
   continue only when the overall state and named Channel are both `online`.
7. **Start it with logs turned up.** The runtime's logger defaults to `error`
   while every Channel lifecycle breadcrumb is emitted at `warn`, so at default
   verbosity a Channel that never connects looks identical to one that did. Raise
   the level before you conclude anything from a quiet log.
8. The runtime does not hot-reload. After any edit to handler, agent, or Channel
   code, **or any change to the environment it read at startup** — a rewritten
   `.env`, a new API key, a renamed Channel —
   restart the process and prove `online` again before retesting. A stale
   process answering with the old behaviour is indistinguishable from a change
   that did not work, and it will send you debugging code that is already correct.
   The environment half is the easier one to miss, because nothing in the code
   changed and no watcher fires.

## Phase 5: validate and report

1. Run the target's focused format, lint, type-check, test, and build commands.
   Record the exact commands and results; do not claim a check that did not run.
2. Prove the agreed first message gets a model-backed answer through the local
   agent path.
3. Run the command below and resolve every declaration, source, server, adapter,
   environment, and lifecycle diagnostic:

   ```sh
   npx --yes copilotkit@<cli-version> channels status --json
   ```

4. Start the long-running process and prove both the overall and named SDK status
   are `online`.
5. From a human account, send a real mention and receive a useful reply that
   **renders the registered Channel component** — a native card, table, or chart
   in the thread, not a paragraph describing one. Send an
   unmentioned follow-up in that subscribed conversation and receive a reply.
   Send an unmentioned message in a fresh conversation and verify silence. These
   three checks stay user-owned in both modes — do not send the test messages
   yourself, even when you are driving the browser. A reply to your own message
   proves nothing about a real user's mention.

If a gate fails, name it, give one user action, and wait.

When every gate passes, report:

1. Project directory and files changed.
2. Agent framework, model, job, and first message.
3. Non-secret Channel code, provider, and completed CLI reconcile result.
4. Exact run and validation commands with results and checks not run.
5. Evidence for local agent output, clean CLI status, online SDK status, the
   rendered Channel component, and all three provider trigger checks.

Start now: inspect the target, ask only for missing inputs, run the public CLI
gate, and check `whoami` before any hosted project command.
