.agents/skills/migrate-to-channels/SKILL.md
Migrate existing multi-agent Discord/Slack setup to the new agent/server/category/channel context architecture. Restructures group workspaces into isolated per-channel notebooks with shared category and agent identity layers.
npx skillsauth add omniaura/omniclaw migrate-to-channelsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill migrates your existing OmniClaw setup from the flat groups/ workspace model to the new layered context architecture:
groups/
agents/{agentId}/CLAUDE.md ← agent identity (shared across channels)
servers/{server}/{category}/ ← category team workspace
servers/{server}/{category}/{channel}/ ← isolated channel notebook
channels/{name}/ ← non-server channels (WhatsApp, Telegram)
channel_subscriptions, agents, and their current workspace mappingschannel_folder, category_folder, agent_context_folder in the DB for each subscription/agentis_primary correctly on each channel's subscriptionsIn older OmniClaw setups, each channel got its own agent entry (e.g., landing-astro-discord, spec-discord, backend-discord) even though they all share the same persona. These are not separate agents — they're just channels.
Before migrating, merge them: re-point their subscriptions to the canonical agent and delete the stubs.
-- Example: landing-astro-discord was just another PeytonOmni channel
-- Move its subs to clayton-discord (if not already there)
INSERT OR IGNORE INTO channel_subscriptions (channel_jid, agent_id, trigger_pattern, requires_trigger, priority, is_primary, discord_bot_id, discord_guild_id, created_at)
SELECT channel_jid, 'clayton-discord', trigger_pattern, requires_trigger, priority, is_primary, discord_bot_id, discord_guild_id, created_at
FROM channel_subscriptions WHERE agent_id = 'landing-astro-discord';
DELETE FROM channel_subscriptions WHERE agent_id = 'landing-astro-discord';
DELETE FROM agents WHERE id = 'landing-astro-discord';
DELETE FROM registered_groups WHERE folder = 'landing-astro-discord';
agent_context_folder — or identity breaksWithout agent_context_folder, the fallback identity injection fires using agent.name from the DB (e.g., "Landing Astro", "Ditto Discord") — not the persona name. The agent will say "I am Landing Astro" instead of "I am PeytonOmni".
Set this immediately after adding the agent identity file:
UPDATE agents SET agent_context_folder = 'agents/peytonomi'
WHERE id IN ('clayton-discord', 'landing-astro-discord');
OmniClaw uses two completely different identifiers for Discord bots:
| Identifier | Where it lives | Example | Used for |
| ------------------------ | ---------------------------------------- | --------------------- | ----------------- |
| Internal bot key | DISCORD_BOT_IDS in .env | PRIMARY, OCPEYTON | OmniClaw routing |
| Discord snowflake ID | Discord Developer Portal → App → General | 1476396931709276191 | Discord's own API |
The channel_subscriptions.discord_bot_id column stores the internal bot key — the human-readable alias you defined in DISCORD_BOT_IDS. It must never contain a numeric Discord snowflake ID. If you accidentally write a numeric ID there, OmniClaw's routing will silently fall back to DISCORD_DEFAULT_BOT_ID for every message and scheduled task.
To verify:
# These should match keys in DISCORD_BOT_IDS, not numeric IDs
sqlite3 store/messages.db "SELECT DISTINCT discord_bot_id FROM channel_subscriptions WHERE discord_bot_id IS NOT NULL"
If you see numeric IDs, fix them:
sqlite3 store/messages.db "UPDATE channel_subscriptions SET discord_bot_id = 'OCPEYTON' WHERE discord_bot_id = '1476396931709276191'"
Separate issue: When agent_context_folder is NULL, the injected identity block contains the bot key string (e.g., OCPEYTON, PRIMARY) not the persona name. This causes the agent to report conflicting identity info. The fix is always to set agent_context_folder.
is_primary controls more than routing — it controls channel name resolutionis_primary determines:
In a multi-agent channel (e.g., PeytonOmni + OCPeyton both in #spec), set is_primary = 1 only on the human-persona agent (e.g., clayton-discord). The tool agent (ocpeyton-discord) should have is_primary = 0 so it only responds to explicit @OCPeyton mentions and never fires via fallback.
If is_primary is wrong, you'll see another agent's name appear as a channel name in the multi-channel list (e.g., "OCPeyton" showing up as a channel name in PeytonOmni's channel list).
-- Set primary correctly for each channel
UPDATE channel_subscriptions SET is_primary = 1
WHERE agent_id = 'clayton-discord'; -- persona agent owns the channel
UPDATE channel_subscriptions SET is_primary = 0
WHERE agent_id = 'ocpeyton-discord'; -- tool agent never owns
sqlite3 store/messages.db "
SELECT id, name, agent_context_folder FROM agents ORDER BY id;
SELECT '---';
SELECT channel_jid, agent_id, trigger_pattern, is_primary, channel_folder FROM channel_subscriptions ORDER BY channel_jid, agent_id;
"
Look for:
agent_context_folder = NULL (identity will be wrong)is_primary isn't set correctlyIdentify agents that are actually just channels for an existing persona. Re-point their subscriptions and delete the agent entry (see gotcha #1 above).
For each distinct persona, create groups/agents/{id}/CLAUDE.md:
mkdir -p groups/agents/peytonomi
cat > groups/agents/peytonomi/CLAUDE.md << 'EOF'
# PeytonOmni Identity
You are **PeytonOmni** (@PeytonOmni), ...
EOF
Then set agent_context_folder in the DB immediately (gotcha #2).
mkdir -p groups/servers/{server}/{category}/{channel}
Create a CLAUDE.md in each category folder documenting the project context shared across channels.
For each old flat group folder (e.g., spec-discord/, agentflow-discord/), copy its contents into the new channel workspace. Use cp — never mv at this stage, so the original is preserved as a fallback.
# Copy all files except logs/ from old folder to new channel dir
find groups/spec-discord -maxdepth 1 -not -name 'logs' -not -path 'groups/spec-discord' \
-exec cp -r {} groups/servers/omni-aura/ditto-assistant/spec/ \;
Repeat for each old folder → new path mapping. After copying, verify the new dirs have the expected files.
If a channel has content in two legacy locations (e.g., both ocpeyton-discord/ and landing-astro-discord/ map to the same new channel), merge them manually — copy one first, then copy the other, skipping any collisions.
After all content is verified in the new locations, use AskUserQuestion to ask the user:
All legacy folder content has been copied to the new channel structure. Want to delete the old folders?
- Yes, delete them — removes the duplicates, keeps
groups/clean- No, keep them — I'll show you where everything lives so you can clean up manually later
If yes: first check whether any containers are actively running — deleting a folder while a container has it virtiofs-mounted can break the mount mid-run (the host rename makes the path disappear inside the container).
# Check for running omniclaw containers
container list 2>/dev/null | grep omniclaw || echo "No containers running"
If containers are running, use AskUserQuestion to ask:
Some agent containers are currently running. Deleting their workspace folders now could break an in-progress response. What would you like to do?
- Wait and check again — I'll re-check in a moment
- Stop the service now — safest option, agents will pick up again on next restart
If wait: re-run the container list check and loop until clear, then delete without stopping the service:
rm -rf groups/spec-discord groups/agentflow-discord groups/clayton-discord # etc.
If stop the service: unload, delete, reload:
launchctl unload ~/Library/LaunchAgents/com.omniclaw.plist
rm -rf groups/spec-discord groups/agentflow-discord groups/clayton-discord # etc.
launchctl load ~/Library/LaunchAgents/com.omniclaw.plist
If no containers running: delete directly:
rm -rf groups/spec-discord groups/agentflow-discord groups/clayton-discord # etc.
If no: print a summary table of old → new paths so the user knows where to look:
Legacy folder → New location
groups/spec-discord/ → groups/servers/omni-aura/ditto-assistant/spec/
groups/agentflow-discord/ → groups/servers/omni-aura/omniaura/agentflow/
groups/clayton-discord/ → groups/servers/omni-aura/omniaura/agent-debug/
...
Some old folders may have no active DB entry (no registered_groups row, no channel_subscriptions). These are stale experiments. Show the user a list and ask if they want them deleted too. Safe to delete if they're just a CLAUDE.md — worth reviewing first if they have real content.
# Find folders with no DB entry
for f in groups/*/; do
name=$(basename "$f")
count=$(sqlite3 store/messages.db "SELECT COUNT(*) FROM registered_groups WHERE folder='$name'")
[ "$count" = "0" ] && echo "Orphaned: $f ($(ls $f | grep -v logs | wc -l | tr -d ' ') files)"
done
UPDATE channel_subscriptions
SET channel_folder = 'servers/omni-aura/ditto-assistant/spec',
category_folder = 'servers/omni-aura/ditto-assistant'
WHERE channel_jid = 'dc:...' AND agent_id = 'clayton-discord';
is_primary correctly-- Persona agent owns all channels
UPDATE channel_subscriptions SET is_primary = 1 WHERE agent_id = 'clayton-discord';
-- Tool agents never own
UPDATE channel_subscriptions SET is_primary = 0 WHERE agent_id = 'ocpeyton-discord';
Discord agents should not have host filesystem mounts. They should clone repos into their own workspace rather than reading from the host. Legacy additionalMounts in registered_groups.container_config give Discord agents access to the host filesystem, which can cause confusion (agents following stale docs to wrong paths) and is unnecessary security surface.
Remove container_config from all Discord registered_groups:
-- NULL out container_config for all Discord channels (dc: prefix)
UPDATE registered_groups
SET container_config = NULL
WHERE jid LIKE 'dc:%';
If any channel genuinely needs custom mounts (e.g., a local dev agent), restore only that channel's config explicitly:
UPDATE registered_groups
SET container_config = '{"additionalMounts":[...]}'
WHERE folder = 'local-dev-agent';
Set nonMainReadOnly: true in the mount allowlist:
Edit ~/.config/omniclaw/mount-allowlist.json and set:
{
"nonMainReadOnly": true
}
This is belt-and-suspenders: even if container_config is accidentally set again, the allowlist prevents any non-main agent from getting write access to extra mounts. Discord agents that need to read/write code should clone the repo into /workspace/group/ or /workspace/extra/ and work from there.
launchctl unload ~/Library/LaunchAgents/com.omniclaw.plist
launchctl load ~/Library/LaunchAgents/com.omniclaw.plist
Ask each agent: "what are your debug info / loaded contexts?" — verify:
/workspace/agent/CLAUDE.md appears in loaded contextsThe heartbeat feature has been removed. Use scheduled tasks (create_task) instead — they're more flexible and don't silently re-create themselves on restart.
If any agents or groups had heartbeat configured, it will be automatically NULLed out on first startup (the migration runs in createSchema()). But you can also clean it up manually:
UPDATE registered_groups SET heartbeat = NULL WHERE heartbeat IS NOT NULL;
UPDATE agents SET heartbeat = NULL WHERE heartbeat IS NOT NULL;
DELETE FROM scheduled_tasks WHERE id LIKE 'heartbeat-%';
Any ## Heartbeat sections in existing CLAUDE.md files are now inert (the system no longer reads them). You can leave them as documentation or migrate the content to a regular scheduled task.
agent_context_folder → no /workspace/agent/ mount, identity via agentName fallback (works but name may be wrong — see gotcha #2)channel_folder → workspace falls back to agent folder (unchanged behavior)category_folder → no /workspace/category/ mount (unchanged behavior)tools
Manage stacked pull requests using Graphite CLI. Create, submit, and restack PR chains.
tools
Full GitHub operations via `gh` CLI — pull requests, issues, code review, CI/CD, search, and GraphQL API. Use for any GitHub interaction beyond basic git.
development
Browse the web for any task — research topics, read articles, interact with web apps, fill forms, take screenshots, extract data, and test web pages. Use whenever a browser would be useful, not just when the user explicitly asks.
testing
X (Twitter) integration for OmniClaw. Post tweets, like, reply, retweet, and quote. Use for setup, testing, or troubleshooting X functionality. Triggers on "setup x", "x integration", "twitter", "post tweet", "tweet".