# AGENTS.md — Discord Bot > **Parent**: `../../AGENTS.md` — env vars (`DISCORD_TOKEN`, `DISCORD_GUILD_ID`), deployment, Payload config. ## Overview Standalone long-running process (`bun run bot`). Imports `@payload-config` directly, shares PostgreSQL with web app. Under active development. ## Structure ``` bot/ index.ts # Entry point config.ts # Env validation (fail-fast on missing required vars) commands/ index.ts # Command registry (global vs guild scope) ping.ts # /ping (global, DM-usable) signup.ts # /signup (DM-only, creates Payload user with temp password) link.ts # /link (global, links discordId to existing user) announce.ts # /announce (guild-only, staff only; message omitted = compose modal with channel picker, handler in this file) remindEvaluations.ts # /remind-evaluations (guild-only, staff only, manual evaluation-reminder trigger) events/ interactionCreate.ts # Routes ptf-att: RSVP button interactions transferInteractions.ts # Handles transfer decision buttons (approve/deny/appeal) services/ index.ts # Service registry missionEmbeds.ts # Attendance embed lifecycle + reconcile loop (poll tick) notificationBridge.ts # Poll user-notifications → Discord DMs evaluationReminders.ts # Post-mission evaluation reminder DMs (one-shot marker on Missions) imminentReminders.ts # Pre-op reminder: thread on the roll-call message + attendee pings (one-shot marker on Missions) fridayOpsNotice.ts # Weekly notice: pings Community Zeus in #side-ops when the upcoming Friday has no op signup.ts # Signup service logic transferRequests.ts # Polls assignment transfer requests awaiting leader decision transferDelivery.ts # Transfer leader-request / rejection / info delivery DMs lib/ roles.ts # isStaff check resolve.ts # discordId ↔ Payload user lookups transferMessaging.ts # Transfer embed builders + delivery keys ``` ## Command registration scope - **Global** (DM-usable): `signup`, `link`, `ping` - **Guild-only**: `announce`, `remind-evaluations` (guild-scoped commands never appear in DMs) ## Feature flow: signup/link `/signup` is DM-only. Creates Payload user with `username` = `discordUsername` = caller's Discord username. Ephemeral reply carries temp password (guaranteed delivery path); `interaction.user.send()` is best-effort persistent copy. `/link` works in servers and DMs — matches `discordUsername` → sets `discordId`. ## Feature flow: attendance Bot posts RSVP embeds (Yes/Tentative/No) for future, Ready/Scheduled, visibility:"unit" missions into ops channel. Stores `discordMessageId` + `discordAttendanceHash` on mission. Reconciles hash changes every poll tick (web ↔ Discord two-way sync). The embed title links to the mission page, a mission summary sits under the title (truncated to 300 characters with an ellipsis), and an "Open the op" field carries the URL; `attendanceHash` includes an embed schema version so layout-only changes re-render live roll-calls once (idempotent). Web UI: `MissionAttendance` component. ## Feature flow: notifications `notificationBridge` polls `user-notifications` (cursor = last seen id; cap 5 DMs/tick). DMs opted-in users (`preferences.discord.enabled`, not in `mutedTypes`). ## Feature flow: evaluation reminders Once a mission is evaluable (`Completed`, or `Scheduled`/`Active` past its start), sends a one-shot DM per linked user asking them to rate their leadership (leaders also get a "rate your subordinates" section). Recipient plan: `computeEvaluationReminderPlan` in `src/lib/evaluations/reminders.ts`; state marker `evaluationRemindersSentAt` on Missions (withheld if the 40-DM/tick cap is hit — retries next tick). DMs ignore `preferences.discord.enabled` (defaults false, not yet exposed in web UI). Staff can re-send manually via `/remind-evaluations`. ## Feature flow: imminent op reminder 15 minutes before a posted op starts (`visibility: "unit"`, Ready/Scheduled, live roll-call message), `imminentReminders` opens a thread on the roll-call message ("Op starting soon: ") and pings every yes-RSVP via `<@discordId>` (pings ignore `preferences.discord.enabled`, same exception as evaluation reminders). One-shot via the `imminentReminderSentAt` marker on Missions; the cancel flow clears it so a rescheduled op re-reminds. Grace bound: a missed reminder still fires within 15 minutes after the start, never later. Query: `src/lib/intelligence/imminentReminders.ts` (window constants + `getImminentReminderMissions`). ## Feature flow: Friday open-slot notice At 8 PM Eastern on Wednesday (`America/New_York`, evaluated via `isNoticeDue` in `src/lib/intelligence/fridayOpsNotice.ts`; catch-up until ET midnight, a fully missed Wednesday skips to next week), if no non-cancelled mission starts on that week's upcoming Friday, `fridayOpsNotice` posts once in #side-ops pinging the Community Zeus role: the slot is open, hosting is welcome, and if nobody picks it up the slot stays empty or Jason hosts it himself. When a non-superuser creator later claims the slot (`getFridayClaimant`, earliest-created mission wins; superuser/staff-created missions never claim), the notice is edited to append "This slot was claimed by \. Thank you for hosting for our community!"; if that op is later cancelled or deleted, the claim line is stripped and the notice reverts to the open-slot state (a between-polls claim swap re-credits the new claimant). Role: `DISCORD_COMMUNITY_ZEUS_ROLE_ID` or resolved by name in the guild; channel: `DISCORD_SIDE_OPS_CHANNEL_ID`. Dedup without DB state: the message embeds a stable per-Friday `` token and the service scans recent channel messages for it (claim idempotency via a marker in the message content). Drafts (Concept) count as the slot being taken. ## Feature flow: assignment transfers `transferRequests` polls `assignment-transfers` for requests awaiting leader decision; leaders approve/deny via buttons handled in `transferInteractions.ts`; DMs are built by `lib/transferMessaging.ts` and sent through `transferDelivery.ts` (leader request, requester rejection notice, decision info). Web side: `src/lib/transfers/` + `/transfers` page. ## Where to look | Task | Path | |------|------| | Add new slash command | `bot/commands/.ts` + register in `bot/commands/index.ts` | | Add button interaction | `bot/events/interactionCreate.ts` (RSVP) or `transferInteractions.ts` (transfers) | | Modify embed lifecycle | `bot/services/missionEmbeds.ts` | | Change DM bridging | `bot/services/notificationBridge.ts` | | Evaluation reminder logic | `bot/services/evaluationReminders.ts` + `src/lib/evaluations/reminders.ts` | | Imminent-op reminder logic | `bot/services/imminentReminders.ts` + `src/lib/intelligence/imminentReminders.ts` | | Friday open-slot notice | `bot/services/fridayOpsNotice.ts` + `src/lib/intelligence/fridayOpsNotice.ts` | | Transfer workflow | `bot/services/transferRequests.ts` + `lib/transferMessaging.ts` | | User lookup patterns | `bot/lib/resolve.ts` | ## Anti-patterns - **NEVER** run bot and web app with separate database connections without connection pooling — they share PostgreSQL - **NEVER** register guild-only commands if the command needs to work in DMs (signup, link, ping must be global) - **NEVER** assume DM delivery succeeded — `/signup` uses ephemeral reply as primary delivery path - **NEVER** modify `discordId` directly in Payload admin — use the `/link` command or the resolve helper