<!-- skill: Discord Bot | version: 1.1.0 | description: Build Discord bots with discord.py — slash commands first, registered properly every boot | category: AI & Automation -->

## Instructions
- SLASH COMMANDS FIRST. Every user-facing command is a slash command (`@app_commands.command` inside a `commands.Cog`). Prefix commands (`!`) are admin/debug only.
- Register commands PROPERLY ON EVERY BOOT: call `await bot.tree.sync()` inside `setup_hook` (runs before login completes), never bare in `on_ready` (it fires on every reconnect and double-syncs).
- Dev loop: sync to ONE test guild for instant updates (`bot.tree.copy_global_to(guild=...)` + `await bot.tree.sync(guild=...)`); production: one global `await bot.tree.sync()`. Use an env flag (`DEV_GUILD_ID`) to switch.
- Keep the token in `DISCORD_TOKEN` env var, never in code. Enable MESSAGE CONTENT INTENT in the portal only if a prefix/debug command needs it.
- One cog per file under `cogs/`, loaded in `setup_hook` with `await bot.load_extension(...)`. Cogs hold state; `bot.py` stays thin.
- Always respond within 3s (`interaction.response.send_message`) or `await interaction.response.defer()` + `followup.send` for slow work. Errors: `on_app_command_error` cog listener, ephemeral replies, full traceback in logs.

## Examples
- `/ping` → "Pong! 42ms" (ephemeral optional flag).
- `/poll "Best day?" options:"Mon,Tue,Wed"` → embed + buttons; button callback tallies votes.
- `/weather city:<str>` with `app_commands.describe` hints and autocomplete.

## Workflows
1. Scaffold: `bot.py`, `cogs/`, `.env` (`DISCORD_TOKEN`, optional `DEV_GUILD_ID`), `requirements.txt` (`discord.py`).
2. Portal: application → Bot → token → Privileged Intents as needed → OAuth2 URL generator (scopes `bot` + `applications.commands`) → invite to test server.
3. Write cogs with slash commands; boot with `python bot.py`; confirm "Synced N command(s)" in logs AND that commands appear in Discord (global sync can take up to 1h to propagate — use the dev guild for instant).
4. Test each command, check error handler path (bad input → ephemeral error, no crash).
5. Deploy 24/7 (`systemd`/`tmux` on the Pi); rotate token if ever leaked.

## Templates
```python
# bot.py — slash-first bot with correct every-boot registration
import os, discord
from discord.ext import commands

TOKEN = os.getenv("DISCORD_TOKEN")
DEV_GUILD = os.getenv("DEV_GUILD_ID")  # set for instant dev sync, unset for prod

class Bot(commands.Bot):
    async def setup_hook(self):
        # Load cogs BEFORE syncing so their commands exist
        for cog in ("general", "polls"):
            try:
                await self.load_extension(f"cogs.{cog}")
            except Exception as e:
                print(f"cog {cog} failed: {e}")
        # Register slash commands every boot, exactly once
        if DEV_GUILD:
            guild = discord.Object(id=int(DEV_GUILD))
            self.tree.copy_global_to(guild=guild)
            synced = await self.tree.sync(guild=guild)
            print(f"Synthetic dev sync: {len(synced)} command(s) to guild {DEV_GUILD}")
        else:
            synced = await self.tree.sync()
            print(f"Synced {len(synced)} command(s) globally (up to 1h to appear)")

intents = discord.Intents.default()
bot = Bot(command_prefix="!", intents=intents)

@bot.event
async def on_ready():
    print(f"Logged in as {bot.user} — commands registered")

bot.run(TOKEN)
```
```python
# cogs/general.py — slash command cog pattern (copy per feature)
import discord
from discord import app_commands
from discord.ext import commands

class General(commands.Cog):
    def __init__(self, bot: commands.Bot):
        self.bot = bot

    @app_commands.command(name="ping", description="Check the bot is alive")
    @app_commands.describe(ephemeral="Reply only you can see")
    async def ping(self, interaction: discord.Interaction, ephemeral: bool = False):
        await interaction.response.send_message(
            f"Pong! {round(self.bot.latency * 1000)}ms", ephemeral=ephemeral
        )

    @app_commands.command(name="echo", description="Repeat text back")
    @app_commands.describe(text="What to say")
    async def echo(self, interaction: discord.Interaction, text: str):
        await interaction.response.send_message(text[:1500])

async def setup(bot: commands.Bot):
    await bot.add_cog(General(bot))
```

## Tools
- SARAH `shell_run` with `python bot.py`; the "Synced N command(s)" log line proves registration — if a command is missing in Discord, the sync log tells you why.
- SARAH `file_write`: one cog per file, keep cogs under ~150 lines.

## Advanced
- Scale: shard with `AutoShardedBot` past ~2500 guilds; per-guild settings in SQLite (`aiosqlite`), cogs read config on command invoke.
- Rate limits: global 50 req/s — use `commands.cooldown(1, 5, BucketType.user)` on spammy commands, bulk deletes with `channel.purge(limit=...)` (14-day limit on bulk).
- Permissions: `@app_commands.checks.has_permissions(manage_messages=True)` + `@app_commands.guild_only()`; test as a no-perms alt account.
- Interactive flows: `discord.ui.Modal` for forms, `View` with timeout + `on_timeout` disabling buttons, paginators for long lists.
- Background work: `@tasks.loop(minutes=10)` for reminders/status rotation; start in `setup_hook`, cancel in `cog_unload`.
- Troubleshooting: commands missing → check sync log + dev-guild propagation; `403 Forbidden` → missing bot role/permission or intent off in portal; random disconnects → heartbeat blocked (host network), add auto-restart.

## Documentation
- app_commands: https://discordpy.readthedocs.io/en/stable/interactions/api.html
- Command syncing: https://discordpy.readthedocs.io/en/stable/ext/commands/cogs.html
- Developer portal: https://discord.com/developers/applications
