{"id":"migrate-slack-agents","name":"migrate-slack-agents","summary":"従来のシングルボットSlackインストールを、エージェントの識別、ワークスペース、メモリ、配線動作を保持しつつ、既存のエージェントグループごとに1つのプロビジョニングされたSlackアプリに移行するか、オペレーターがクラシックに残るかどうかを記録し、クラシックは引き続きサポートされます。","body":"# Migrate classic Slack agents\n\nTurn every agent group wired through the classic `instance='slack'` adapter into\na named `slack-<slug>` bot without replacing the agent group. This is an\noperator-guided, resumable data migration. It never creates an agent group and\nnever edits an agent workspace.\n\nMigration is optional. Classic single-bot Slack remains fully supported; this\nskill first offers the choice, and staying on classic is a valid outcome that\nalso satisfies the update requirement.\n\nHard invariants:\n\n- Never call `create_agent` or `ncl groups create`.\n- Never change an `agent_groups.id` or write under `groups/<folder>/`.\n- Never merge the `channels` branch; fetch and copy skill-owned files only.\n- Never print token values. Show key names and masked presence only.\n- Keep classic rows, credentials, and the shared Slack app available for\n  rollback until the operator explicitly approves cutover.\n\n## Phase 1: Detect classic state\n\nRun from the NanoClaw project root. Read the central DB only through the\nsanctioned wrapper.\n\nClassic state requires all four signals:\n\n1. `src/channels/index.ts` contains the Slack barrel import\n   `import './slack.js';`.\n2. `.env` has a non-empty unsuffixed `SLACK_BOT_TOKEN` and either a non-empty\n   `SLACK_APP_TOKEN` or `SLACK_SIGNING_SECRET`. Check presence without echoing\n   values.\n3. This query returns at least one row:\n\n   ```bash\n   pnpm exec tsx scripts/q.ts data/v2.db \"SELECT mg.id, mga.id, mga.agent_group_id FROM messaging_groups mg JOIN messaging_group_agents mga ON mga.messaging_group_id=mg.id WHERE mg.channel_type='slack' AND mg.instance='slack' ORDER BY mg.id, mga.agent_group_id\"\n   ```\n\n4. At least one wired group does not yet have complete named-instance coverage:\n   a stable slug, both `SLACK_BOT_TOKEN_<SUFFIX>` and\n   `SLACK_APP_TOKEN_<SUFFIX>`, that slug in `SLACK_INSTANCES`, and the expected\n   `slack-<slug>` messaging-group/wiring rows.\n\nIf the classic conjunction is absent and there is no partial state, stop with:\n\n> Nothing to migrate: this install does not have the classic shared-bot Slack state.\n\nThis is a successful no-op. If only some signals exist, make no changes; report\nthe inconsistent or partial state instead of guessing. If every wired group\nalready has complete named coverage, report that the migration is already\ncomplete and proceed only to the update-requirement acknowledgement in Phase 9.\n\n## Phase 1b: Offer the choice\n\nClassic state confirmed does not mean migration is required. Present the\ndecision to the operator before touching anything, in words like these:\n\n> Your classic Slack setup keeps working as-is — nothing forces this\n> migration. The new Slack experience adds Slack agent spawning (create new\n> agents straight from Slack, each with its own provisioned bot and avatar)\n> plus UX improvements — per-agent identities, DM onboarding, multi-agent\n> rooms. Say the word and we'll run the upgrade now — or run\n> `/migrate-slack-agents` later manually.\n\nIf the operator chooses to **stay on classic**: make no changes, acknowledge\nthe update requirement now using the Phase 9 ack command (the requirement\nrecords a decision, not only a completed migration), state that classic Slack\ncontinues working unchanged, and stop. Re-running this skill later re-offers\nthe migration.\n\nIf the operator chooses to **migrate**, continue to Phase 2.\n\n## Phase 2: Inventory and propose the map\n\nBefore any mutation, capture every classic surface and its complete behavior:\n\n```bash\npnpm exec tsx scripts/q.ts data/v2.db \"SELECT ag.id, ag.name, ag.folder, mg.id, mg.platform_id, mg.name, mg.is_group, mg.unknown_sender_policy, mga.id, mga.engage_mode, mga.engage_pattern, mga.sender_scope, mga.ignored_message_policy, mga.session_mode, mga.threads, mga.priority FROM messaging_groups mg JOIN messaging_group_agents mga ON mga.messaging_group_id=mg.id JOIN agent_groups ag ON ag.id=mga.agent_group_id WHERE mg.channel_type='slack' AND mg.instance='slack' ORDER BY ag.id, mg.id, mga.priority DESC\"\npnpm exec tsx scripts/q.ts data/v2.db \"SELECT ad.agent_group_id, ad.local_name, ad.target_type, ad.target_id FROM agent_destinations ad JOIN messaging_groups mg ON ad.target_type='channel' AND ad.target_id=mg.id WHERE mg.channel_type='slack' AND mg.instance='slack' ORDER BY ad.agent_group_id, ad.local_name\"\n```\n\nClassify each surface as DM, channel, or MPIM. A `D…` conversation is a DM;\nuse Slack `conversations.info` with the classic bot token to distinguish an\nMPIM from a channel when the stored id is ambiguous, returning only type/id\nmetadata and never the token.\n\nChoose one stable, unique slug per agent group using the flow's normalization:\nlowercase, replace non-alphanumerics with `-`, trim `-`, and add a numeric\nsuffix if an env key, `SLACK_INSTANCES` entry, or named DB instance is already\nclaimed by another group. Once any migration state exists, never change that\ngroup's slug. Include the group commonly called “master”; it also gets its own\nprovisioned app.\n\nSlugs are de-duplicated, but the Slack-visible bot display name comes from the\nagent group's name — and Slack allows two apps with identical display names in\none workspace, leaving humans a mention picker with twins told apart only by\navatar. If any two migrating groups share a display name, flag it in the\ndry-run and have the operator differentiate the names before provisioning;\nrenaming at this point is free, while renaming after provisioning requires a\nmanifest update.\n\nPresent a dry-run table with:\n\n- agent group id, name, folder, and chosen slug;\n- every old messaging-group id, type, platform id, and destination name;\n- the full old wiring row and unknown-sender policy;\n- the proposed `slack-<slug>` surface and whether it is new, partial, or done.\n\nFor a channel, map the old row to a sibling with the same `platform_id` and\n`instance='slack-<slug>'`. DMs and MPIMs need new conversation ids as described\nin Phase 6. Ask the operator to confirm the entire map before continuing.\n\n## Phase 3: Install the current Slack agents payloads\n\nFirst run `/update-skills` for the installed Slack channel only and require its\nstructured result to report `success: true` and `refreshed`. That refreshes the\nbarrel-registered `/add-slack` payload; it deliberately does not install\ncompanion skills, credentials, wirings, or restarts.\n\nResolve the remote that points at `nanocoai/nanoclaw` the same fork-aware way\n`/update-skills` does; do not assume it is `origin`. Fetch, but never merge:\n\n```bash\nsource setup/lib/channels-remote.sh\nchannels_remote=\"$(resolve_channels_remote)\"\ngit fetch \"$channels_remote\" channels\n```\n\nFrom `$channels_remote/channels`, materialize every file under\n`.claude/skills/slack-a2a-rooms/` and then every file under\n`.claude/skills/slack-agent-flow/` with `git ls-tree` + `git show`. Read each\nnew `SKILL.md` completely and apply its own Apply steps, in that order. The\norder is load-bearing and mirrors `setup/channels/companions.ts`. The standard\ndriver may apply each document:\n\n```bash\npnpm exec tsx setup/lib/skill-driver.ts .claude/skills/slack-a2a-rooms\npnpm exec tsx setup/lib/skill-driver.ts .claude/skills/slack-agent-flow\n```\n\nDo not continue unless both report fully applied and their own build/tests\npass. Source contracts: `.claude/skills/add-slack/SKILL.md`,\n`setup/channels/companions.ts`, and the two fetched companion `SKILL.md` files.\n\n## Phase 4: Get provisioning authority\n\nPause and ask the operator to choose and complete one authority path:\n\n1. **Managed broker** — enroll/validate the registry install token, then have\n   the operator connect the intended Slack workspace through the broker OAuth\n   flow. The flow accepts `NANOCLAW_INSTALL_TOKEN` or the enrolled account.\n2. **Direct Slack** — the operator supplies a valid `SLACK_MANAGER_TOKEN` in\n   `.env` for `apps.manifest.create` + `apps.managedInstall`.\n\nNever select a path, workspace, or authority on the operator's behalf. Confirm\nthe intended workspace matches the classic bot's `auth.test` team before any\napp is created. See `src/provisioning/slack-app.ts` and the fetched flow's\n`provision.ts` for the two transport contracts.\n\n## Phase 5: Provision every existing group\n\nChoose an existing classic Slack-wired group with a Slack approver as the\nstable source group. It supplies the operator identity and authenticates the\norigin `slack` instance; it is not cloned. For every inventoried group,\nincluding the source/master group, run the installed finish primitive with\nthe recorded slug and defer room creation:\n\n```bash\npnpm exec tsx scripts/slack-agent-flow-finish.ts \\\n  --group <existing-agent-group-id> \\\n  --name <stable-slug> \\\n  --source-group <source-agent-group-id> \\\n  --origin-instance slack \\\n  --room none\n```\n\nDo not pass `--restart` yet. Run all groups first. The script reuses a complete\nsuffixed token pair, creates the operator DM and its wiring idempotently, and\ndoes not create an agent group. On a partial pair, finish the existing Slack\napp installation and retry the same slug; never choose a new slug or create a\nsecond app. Ensure every completed slug is present exactly once in\n`SLACK_INSTANCES`, including reuse cases. The exact CLI contract is documented\nat the top of `scripts/slack-agent-flow-finish.ts`.\n\n## Phase 6: Recreate surfaces and clone behavior\n\nFor each old wiring:\n\n- **Channel:** the operator or an authorized Slack API caller must invite the\n  new bot to the existing channel. Use the same channel `platform_id`.\n- **DM:** use the operator DM created by the finish script. Every other user\n  must open a new DM with the new bot; use that new DM id.\n- **MPIM:** recreate the membership with the new bot. Slack forks it to a new\n  conversation id; use the new id.\n\nCreate each sibling through `ncl messaging-groups create`, copying `name`,\n`is_group`, and `unknown_sender_policy`. Then use `ncl wirings create` with\n`--channel-type slack`, the resolved `--platform-id`,\n`--instance slack-<slug>`, and the existing `--agent-group-id`. Pass the\nrecorded `--engage-mode`, `--engage-pattern`, `--sender-scope`,\n`--ignored-message-policy`, `--session-mode`, `--priority`, and thread policy;\nomit `--threads` when the old value is NULL, otherwise pass `true` or `false`.\n\n```bash\nncl messaging-groups create --channel-type slack --platform-id <new-or-reused-platform-id> --instance slack-<slug> --name \"<recorded-name>\" --is-group <0-or-1> --unknown-sender-policy <recorded-unknown-sender-policy>\nncl wirings create --channel-type slack --platform-id <new-or-reused-platform-id> --instance slack-<slug> --agent-group-id <existing-agent-group-id> --engage-mode <recorded-engage-mode> --sender-scope <recorded-sender-scope> --ignored-message-policy <recorded-ignored-policy> --session-mode <recorded-session-mode> --priority <recorded-priority>\n```\n\nIf a legacy pattern row has NULL `engage_pattern`, pass `.` to preserve its\nmatch-all runtime behavior. Do not accept adapter defaults in place of any\nrecorded behavior field. These create operations are idempotent on their\nnatural keys/pairs and create the companion destination row.\n\nShow these operator-visible warnings verbatim:\n\n- DM `platform_id`s are bot-specific (old DMs can't be copied; only the operator DM is auto-resolved — other DMs need re-opening per user).\n- `user_dms` cache is not instance-aware.\n- MPIM membership changes fork conversations (new IDs).\n- Existing sessions stay on the old rows (session history does not transfer — new instances start fresh sessions).\n- Destination names collide → suffixed (reconcile deliberately, preserve the original local name where the old wiring is retired).\n\nKeep suffixed destination names during the rollback window. If the operator\nlater retires an old wiring, reconcile with `ncl destinations remove/add`\ninstead of raw SQL, and transfer its original local name only after the old\ntarget no longer needs it. `src/db/messaging-groups.ts` and\n`docs/db-central.md` define the destination side effect and session behavior.\n\n## Phase 7: Verify before cutover\n\nRestart once after all apps and rows exist:\n\n```bash\nbash setup/lib/restart.sh\n```\n\nDo not proceed until all checks pass:\n\n- every `slack-<slug>` instance authenticates after restart; confirm with\n  instance-specific startup logs and Slack `auth.test` without printing tokens;\n- the operator can DM every agent and receive the response from the new bot;\n- each new bot is invited to every mapped channel, and an explicit mention of\n  that bot reaches only the expected existing agent group;\n- controlled channel tests show no duplicate responders from the classic and\n  named bots;\n- the baseline and current `agent_groups` ids/folders are identical, every\n  original `groups/<folder>/` remains in place, and no persona, workspace, or\n  memory file was rewritten;\n- every mapped sibling row and wiring matches the recorded behavior fields.\n\nIf a test fails, keep the classic app active, repair the partial named state,\nand resume from the ledger. Do not acknowledge the update requirement.\n\n## Phase 8: Explicit cutover gate\n\nShow the verification result and ask the operator for explicit cutover\nconfirmation. Only after approval, remove the classic bot from migrated\nchannels and/or disable the unsuffixed Slack credentials so the shared adapter\ncannot answer alongside the named bots. Re-run the channel tests and require\nno duplicate responders.\n\nNever delete or archive a Slack app automatically. Until this gate, keep the\nold rows, apps, and tokens intact. Detached old messaging-group rows preserve\ntheir wirings, sessions, and destinations, providing the rollback path: restore\nthe unsuffixed credentials or re-invite the classic bot, restart, and disable\nthe named instances if necessary.\n\n## Phase 9: Resume and acknowledge the update\n\nThe workflow is safe to re-run. Treat a group as complete only when its stable\nslug has both suffixed tokens, a `SLACK_INSTANCES` entry, and matching DB\ncoverage for every mapped surface. Skip complete steps. Resume partial rows or\ncredentials with the same slug; the finish primitive reuses tokens and its\nSlack/DB legs are get-before-create, so retries do not duplicate apps or rows.\n\nAcknowledge the `[BREAKING]` requirement in exactly two cases: the operator\nchose to stay on classic (Phase 1b), or Phases 7 and 8 passed. Use the\ntransaction id and requirement id supplied by `/update-nanoclaw`:\n\n```bash\npnpm exec tsx \"$stageRoot/scripts/update-nanoclaw.ts\" ack \\\n  --project-root \"$PWD\" --id \"$id\" \\\n  --requirement \"$requirement_id\" --status succeeded\n```\n\nThen return control to `/update-nanoclaw` to finish and health-check. A failed\nor half-migrated state remains pending; never acknowledge it merely to let the\nupdate finish — either complete verification and cutover, or roll back to\nclassic and record the stay decision instead.","author":"@nanocoai","ownerProfile":null,"authorContacts":null,"sourceUrl":"https://github.com/nanocoai/nanoclaw/tree/main/.claude/skills/migrate-slack-agents","license":"MIT","category":"writing","lang":"en","tokens":3451,"stars":0,"calls30d":2,"claimed":false,"visibility":"public","origin":"crawler","version":"0.1.0","createdAt":"2026-08-22","updatedAt":"2026-08-22","files":[],"requires":{"mcp":[],"tools":[]},"safety":{"flags":[],"scannedAt":"2026-08-22","hasScripts":false,"networkEndpoints":[]}}