# Implementation Plan: Discord-Claude Gateway ## Overview Incrementally build the Discord-Claude Gateway in TypeScript, starting with configuration and pure utility modules, then layering on the event queue, markdown config loading, system prompt assembly, session management, scheduling, hooks, the Discord bot, Agent SDK integration via AgentRuntime, bootstrap, and finally the orchestrating GatewayCore. Each step builds on the previous, and testing tasks validate all 28 correctness properties from the design document. ## Tasks - [x] 1. Initialize project and install dependencies - Initialize a TypeScript Node.js project with `tsconfig.json` - Install runtime dependencies: `discord.js`, `@anthropic-ai/claude-agent-sdk`, `node-cron` - Install dev dependencies: `vitest`, `fast-check`, `typescript`, `tsx`, `@types/node` - Configure vitest in `vitest.config.ts` - Create `src/` directory structure with placeholder `index.ts` - _Requirements: 8.1_ - [x] 2. Implement ConfigLoader - [x] 2.1 Create `src/config.ts` with `loadConfig()` function and `GatewayConfig` interface - Read DISCORD_BOT_TOKEN, ANTHROPIC_API_KEY, ALLOWED_TOOLS, PERMISSION_MODE, QUERY_TIMEOUT_MS, MAX_CONCURRENT_QUERIES, CONFIG_DIR, MAX_QUEUE_DEPTH, OUTPUT_CHANNEL_ID from environment variables - Apply defaults: ALLOWED_TOOLS → `["Read","Write","Edit","Glob","Grep","WebSearch","WebFetch"]`, PERMISSION_MODE → `"bypassPermissions"`, QUERY_TIMEOUT_MS → `120000`, MAX_CONCURRENT_QUERIES → `5`, CONFIG_DIR → `"./config"`, MAX_QUEUE_DEPTH → `100` - Throw descriptive error listing all missing required variables if DISCORD_BOT_TOKEN or ANTHROPIC_API_KEY are absent - _Requirements: 8.1, 8.2, 8.3, 1.2, 2.1, 2.2, 2.3_ - [ ]* 2.2 Write property test: Config loading round-trip (Property 1) - **Property 1: Config loading round-trip** - Generate random valid env var values for all config fields, call `loadConfig()`, verify fields match inputs and ALLOWED_TOOLS is correctly split from comma-separated string - **Validates: Requirements 8.1, 2.3** - [ ]* 2.3 Write property test: Missing required config reports all missing values (Property 2) - **Property 2: Missing required config reports all missing values** - Generate subsets of required vars (DISCORD_BOT_TOKEN, ANTHROPIC_API_KEY) that are unset, verify error message contains all missing variable names - **Validates: Requirements 8.3, 1.2, 2.2** - [x] 3. Implement ResponseFormatter - [x] 3.1 Create `src/response-formatter.ts` with `splitMessage()` function - Split text into chunks of at most 2000 characters - Prefer splitting at line boundaries - Track open code blocks: if a split occurs inside a code block, close with ``` and reopen in the next chunk - Preserve markdown formatting across splits - _Requirements: 5.3, 5.4_ - [ ]* 3.2 Write property test: Message splitting with code block preservation (Property 9) - **Property 9: Message splitting with code block preservation** - Generate strings of varying lengths with and without code blocks; verify: (a) every chunk ≤ 2000 chars, (b) concatenation reproduces original text modulo inserted delimiters, (c) code fences are properly opened/closed in each chunk - **Validates: Requirements 5.3, 5.4** - [x] 4. Implement SessionManager - [x] 4.1 Create `src/session-manager.ts` with SessionManager class - Implement `getSessionId(channelId)`, `setSessionId(channelId, sessionId)`, `removeSession(channelId)`, `clear()` - Use an in-memory `Map` for channel bindings - _Requirements: 6.1, 6.2, 6.3, 6.4_ - [ ]* 4.2 Write property test: New session creation and storage (Property 8) - **Property 8: New session creation and storage** - Generate random channel/session IDs, store and retrieve, verify round-trip - **Validates: Requirements 4.3, 6.1** - [ ]* 4.3 Write property test: Reset removes channel binding (Property 10) - **Property 10: Reset removes channel binding** - Store a session, remove it, verify `getSessionId` returns `undefined` - **Validates: Requirements 6.3** - [ ]* 4.4 Write property test: Concurrent session isolation (Property 11) - **Property 11: Concurrent session isolation** - Generate two distinct channel IDs with different session IDs, verify operations on one don't affect the other - **Validates: Requirements 6.4** - [x] 5. Implement ErrorFormatter - [x] 5.1 Create `src/error-formatter.ts` with `formatErrorForUser()` function - Include error type/name in user-facing message - Exclude stack traces, API keys, and file paths - _Requirements: 7.1_ - [ ]* 5.2 Write property test: Error message formatting (Property 12) - **Property 12: Error message formatting** - Generate random error objects with types, messages, stacks, and embedded sensitive data; verify output contains error type but not stack traces, API keys, or file paths - **Validates: Requirements 7.1** - [x] 6. Implement EventQueue - [x] 6.1 Create `src/event-queue.ts` with EventQueue class and Event/EventType/EventPayload types - Implement `enqueue(event)` — assign monotonically increasing sequence number and timestamp, return the event or null if queue is at max depth - Implement `dequeue()` — return next event in FIFO order or undefined if empty - Implement `size()` — return current queue depth - Implement `onEvent(handler)` — register processing handler; queue calls handler for each event and waits for promise to resolve before dispatching next - Implement `drain()` — return promise that resolves when queue is empty and no event is processing - Accept configurable max depth from GatewayConfig - _Requirements: 11.1, 11.2, 11.3, 11.4, 11.5_ - [ ]* 6.2 Write property test: Event queue accepts all event types with monotonic IDs (Property 15) - **Property 15: Event queue accepts all event types with monotonic IDs** - Generate sequences of mixed-type events (message, heartbeat, cron, hook, webhook), enqueue them, verify each gets a strictly increasing sequence number and timestamp ≥ previous - **Validates: Requirements 11.1, 11.2** - [ ]* 6.3 Write property test: Event queue FIFO dispatch with sequential processing (Property 16) - **Property 16: Event queue FIFO dispatch with sequential processing** - Enqueue a sequence of events, verify the processing handler is called in exact enqueue order and handler N+1 is not called until handler N completes - **Validates: Requirements 11.3, 11.4, 12.5** - [ ]* 6.4 Write property test: Event queue depth rejection (Property 17) - **Property 17: Event queue depth rejection** - Fill the EventQueue to max depth, attempt to enqueue another event, verify it returns null and queue size remains at max - **Validates: Requirements 11.5** - [x] 7. Checkpoint - Ensure all tests pass - Ensure all tests pass, ask the user if questions arise. - [x] 8. Implement MarkdownConfigLoader - [x] 8.1 Create `src/markdown-config-loader.ts` with MarkdownConfigLoader class and MarkdownConfigs interface - Implement `loadAll(configDir)` — read soul.md, identity.md, agents.md, user.md, memory.md, tools.md from configDir; return null for missing files; log warnings for missing files; create memory.md with "# Memory" header if missing - Implement `loadFile(configDir, filename)` — read a single markdown file, return null if missing - Files are read fresh on each call (no caching) so edits take effect immediately - _Requirements: 13.1, 13.2, 13.3, 13.4, 13.5, 14.1, 14.3, 14.4, 15.1, 15.5, 16.1, 16.3, 16.4_ - [ ]* 8.2 Write unit tests for MarkdownConfigLoader - Test each config file is read from the correct path in CONFIG_DIR - Test missing files return null and log warnings - Test memory.md is created with "# Memory" header when missing - Test modified files are picked up on subsequent calls (hot-reload behavior) - _Requirements: 13.1, 13.2, 13.3, 13.4, 13.5, 14.1, 15.1, 15.5_ - [x] 9. Implement SystemPromptAssembler - [x] 9.1 Create `src/system-prompt-assembler.ts` with SystemPromptAssembler class - Implement `assemble(configs)` — concatenate markdown file contents in order: Identity (## Identity), Soul (## Personality), Agents (## Operating Rules), User (## User Context), Memory (## Long-Term Memory), Tools (## Tool Configuration) - Wrap each non-null/non-empty config in `## [Section Name]\n\n[content]\n\n` - Omit sections for null or empty configs - Prepend a preamble instructing the agent it may update memory.md using the Write tool - _Requirements: 22.1, 22.2, 22.3, 22.4, 12.2, 14.2, 15.2, 15.3, 16.2_ - [ ]* 9.2 Write property test: System prompt assembly with section headers and ordering (Property 19) - **Property 19: System prompt assembly with section headers and ordering** - Generate random MarkdownConfigs with at least one non-null field; verify: (a) each non-null config is wrapped in a section header, (b) sections are ordered Identity, Personality, Operating Rules, User Context, Long-Term Memory, Tool Configuration, (c) preamble about memory.md is present - **Validates: Requirements 22.1, 22.2, 22.4, 12.2, 14.2, 15.2, 15.3, 16.2** - [ ]* 9.3 Write property test: Missing or empty configs are omitted from system prompt (Property 20) - **Property 20: Missing or empty configs are omitted from system prompt** - Generate MarkdownConfigs with some null/empty fields; verify the assembled prompt contains no section headers for those fields and the count of section headers equals the count of non-null, non-empty fields - **Validates: Requirements 22.3, 13.4, 14.3, 16.3** - [ ]* 9.4 Write property test: System prompt assembly round-trip (Property 21) - **Property 21: System prompt assembly round-trip** - Generate valid MarkdownConfigs with non-null, non-empty values; assemble the system prompt then parse section headers from the output; verify the set of section names matches the input config fields - **Validates: Requirements 22.5** - [x] 10. Implement BootstrapManager - [x] 10.1 Create `src/bootstrap-manager.ts` with BootstrapManager class, BootConfig and BootstrapResult interfaces - Implement `run(configDir)` — read boot.md for bootstrap parameters (or use built-in defaults), verify all required markdown files exist, create missing optional files with default content, log loaded and created files - Implement `parseBootConfig(content)` — parse boot.md content into BootConfig; return built-in defaults (require soul.md and identity.md) if content is null - Built-in defaults: requiredFiles = ["soul.md", "identity.md"], optionalFiles = ["agents.md", "user.md", "memory.md", "tools.md", "heartbeat.md"] - _Requirements: 20.1, 20.2, 20.3, 20.4, 20.5_ - [ ]* 10.2 Write property test: Bootstrap creates missing files with defaults (Property 27) - **Property 27: Bootstrap creates missing files with defaults** - Generate sets of required files where some are missing from CONFIG_DIR; run bootstrap; verify each missing file is created with default content and all required files exist after bootstrap - **Validates: Requirements 20.2, 20.3** - [ ]* 10.3 Write unit tests for BootstrapManager - Test boot.md is read for bootstrap parameters - Test built-in defaults are used when boot.md is missing - Test log output lists loaded and created files - _Requirements: 20.1, 20.4, 20.5_ - [x] 11. Implement HeartbeatScheduler - [x] 11.1 Create `src/heartbeat-scheduler.ts` with HeartbeatScheduler class and HeartbeatCheck interface - Implement `parseConfig(content)` — parse heartbeat.md content into HeartbeatCheck objects with name, instruction, and intervalSeconds - Implement `start(checks, enqueue)` — start a recurring timer (setInterval) for each valid check; on each tick create a heartbeat event and enqueue it; reject checks with interval < 60 seconds with a warning log - Implement `stop()` — clear all timers - _Requirements: 17.1, 17.2, 17.3, 17.5, 17.6_ - [ ]* 11.2 Write property test: Heartbeat config parsing (Property 22) - **Property 22: Heartbeat config parsing** - Generate valid heartbeat.md content with check definitions; parse and verify HeartbeatCheck objects match defined values - **Validates: Requirements 17.1** - [ ]* 11.3 Write property test: Heartbeat minimum interval enforcement (Property 23) - **Property 23: Heartbeat minimum interval enforcement** - Generate heartbeat check definitions with intervals < 60 seconds; verify the scheduler rejects them and does not start timers - **Validates: Requirements 17.6** - [x] 12. Implement CronScheduler - [x] 12.1 Create `src/cron-scheduler.ts` with CronScheduler class and CronJob interface - Implement `parseConfig(content)` — parse the "Cron Jobs" section from agents.md content into CronJob objects with name, expression, and instruction - Implement `start(jobs, enqueue)` — schedule each cron job using node-cron; on each trigger create a cron event and enqueue it; log warning and skip jobs with invalid cron expressions - Implement `stop()` — stop all cron jobs - _Requirements: 18.1, 18.2, 18.3, 18.5_ - [ ]* 12.2 Write property test: Cron job config parsing (Property 24) - **Property 24: Cron job config parsing** - Generate valid agents.md content with a "Cron Jobs" section; parse and verify CronJob objects match defined values - **Validates: Requirements 18.1** - [ ]* 12.3 Write property test: Invalid cron expression rejection (Property 25) - **Property 25: Invalid cron expression rejection** - Generate cron job definitions with syntactically invalid cron expressions; verify the scheduler skips them without affecting other valid jobs - **Validates: Requirements 18.5** - [x] 13. Implement HookManager - [x] 13.1 Create `src/hook-manager.ts` with HookManager class, HookConfig and HookType types - Implement `parseConfig(content)` — parse the "Hooks" section from agents.md content into HookConfig mapping hook types to optional instruction prompts - Implement `fire(hookType, enqueue)` — create a hook event and enqueue it (for startup and shutdown hooks) - Implement `fireInline(hookType, runtime)` — process agent_begin/agent_stop hooks inline without going through the queue - Support hook types: startup, agent_begin, agent_stop, shutdown - _Requirements: 19.1, 19.2, 19.3, 19.4, 19.5, 19.6_ - [ ]* 13.2 Write property test: Lifecycle hooks fire for every event (Property 26) - **Property 26: Lifecycle hooks fire for every event** - Generate event sequences processed by AgentRuntime; verify agent_begin fires before main processing and agent_stop fires after, both inline - **Validates: Requirements 19.3, 19.4** - [ ]* 13.3 Write unit tests for HookManager - Test all four hook types are supported - Test startup hook fires after initialization - Test shutdown hook fires and is processed before exit - Test hook config is parsed from agents.md "Hooks" section - _Requirements: 19.1, 19.2, 19.5, 19.6_ - [x] 14. Checkpoint - Ensure all tests pass - Ensure all tests pass, ask the user if questions arise. - [x] 15. Implement DiscordBot - [x] 15.1 Create `src/discord-bot.ts` with DiscordBot class - Wrap discord.js `Client` with GatewayIntentBits for Guilds, GuildMessages, MessageContent - Implement `start(token)` — authenticate and wait for ready, log bot username and guild count - Implement `registerCommands()` — register `/claude` (with prompt string option) and `/claude-reset` slash commands - Implement `sendMessage(channelId, content)` — send message, log errors on failure with channel ID and content length - Implement `sendTyping(channelId)` — send typing indicator - Implement `destroy()` — disconnect the bot - Implement `onPrompt(handler)` and `onReset(handler)` callbacks - Filter out bot messages to prevent feedback loops - _Requirements: 1.1, 1.3, 3.1, 3.2, 3.3, 3.4, 7.3_ - [ ]* 15.2 Write property test: Mention prompt extraction (Property 3) - **Property 3: Mention prompt extraction** - Generate messages with bot mention and surrounding text, verify extraction removes mention and trims whitespace - **Validates: Requirements 3.1** - [ ]* 15.3 Write property test: Slash command prompt extraction (Property 4) - **Property 4: Slash command prompt extraction** - Generate slash command interactions with prompt option values, verify exact extraction - **Validates: Requirements 3.2** - [ ]* 15.4 Write property test: Bot message filtering (Property 5) - **Property 5: Bot message filtering** - Generate messages with bot/non-bot authors, verify filtering behavior - **Validates: Requirements 3.3** - [x] 16. Implement ChannelQueue - [x] 16.1 Create `src/channel-queue.ts` with ChannelQueue class - Implement `enqueue(channelId, task)` — execute immediately if idle, otherwise queue behind current task - Implement `drainAll()` — resolve when all queued tasks across all channels complete - Ensure sequential per-channel execution with concurrent cross-channel execution - _Requirements: 9.1, 9.2_ - [ ]* 16.2 Write property test: Sequential per-channel queue ordering (Property 13) - **Property 13: Sequential per-channel queue ordering** - Enqueue tasks with observable side effects for the same channel, verify FIFO execution and no concurrent execution within a channel - **Validates: Requirements 9.2** - [x] 17. Implement AgentRuntime - [x] 17.1 Create `src/agent-runtime.ts` with AgentRuntime class and EventResult interface - Implement `processEvent(event)` — main entry point that: - Reads all markdown configs via MarkdownConfigLoader - Assembles system prompt via SystemPromptAssembler - For message events: uses prompt text, resumes/creates sessions via SessionManager, calls Agent SDK `query()` with systemPrompt option - For heartbeat/cron events: uses instruction text as prompt, calls Agent SDK `query()` with systemPrompt option - For hook events: uses hook instruction (if any) as prompt - Iterates Response_Stream: stores session_id from init messages, collects result text - Returns EventResult with responseText, targetChannelId, and sessionId - Integrate HookManager for agent_begin/agent_stop inline hooks - Implement timeout via `Promise.race` with configurable QUERY_TIMEOUT_MS - On Agent SDK error: format user-friendly error, remove binding if session corrupted - _Requirements: 12.1, 12.2, 12.3, 12.4, 12.5, 4.1, 4.2, 4.3, 4.4, 5.1, 7.1, 7.2, 7.4, 21.1, 21.2_ - [ ]* 17.2 Write property test: Query arguments correctness (Property 6) - **Property 6: Query arguments correctness** - Generate random prompt text, gateway config, and assembled system prompt; verify `query()` is called with correct prompt, allowed tools, permission mode, and systemPrompt option - **Validates: Requirements 4.1, 4.4, 12.3** - [ ]* 17.3 Write property test: Session resume on existing binding (Property 7) - **Property 7: Session resume on existing binding** - Set up a channel with a stored session ID, process a message event, verify `query()` includes the `resume` option with the stored session ID - **Validates: Requirements 4.2, 6.2** - [ ]* 17.4 Write property test: Non-message events use instruction as prompt (Property 18) - **Property 18: Non-message events use instruction as prompt** - Generate heartbeat and cron events with instruction strings; verify `query()` uses the instruction as prompt text and includes the assembled systemPrompt option - **Validates: Requirements 12.4, 17.4, 18.4** - [ ]* 17.5 Write property test: State reconstruction after restart (Property 28) - **Property 28: State reconstruction after restart** - Generate sets of markdown config files in CONFIG_DIR; read all configs and assemble system prompt twice (simulating restart); verify both reads produce identical results - **Validates: Requirements 21.3** - [ ]* 17.6 Write unit tests for AgentRuntime - Test timeout notification after configured period - Test session corruption triggers binding removal and user notification - Test result messages are returned in EventResult - Test memory changes are written to disk before signaling event completion - _Requirements: 7.2, 7.4, 5.1, 21.2_ - [x] 18. Checkpoint - Ensure all tests pass - Ensure all tests pass, ask the user if questions arise. - [x] 19. Implement GatewayCore - [x] 19.1 Create `src/gateway-core.ts` with GatewayCore class - Implement `start()`: 1. Load config via ConfigLoader 2. Run bootstrap via BootstrapManager 3. Start Discord bot via DiscordBot, log username and guild count 4. Initialize EventQueue with max depth from config 5. Initialize AgentRuntime with all dependencies 6. Parse heartbeat.md → start HeartbeatScheduler 7. Parse agents.md → start CronScheduler, load HookConfig 8. Register EventQueue processing handler that calls AgentRuntime.processEvent(), routes responses via DiscordBot.sendMessage() with ResponseFormatter splitting, and manages typing indicators 9. Wire DiscordBot.onPrompt() to create message events and enqueue them; check isShuttingDown and concurrency limit before enqueuing 10. Wire DiscordBot.onReset() to remove channel binding via SessionManager 11. Fire startup hook via HookManager - Implement `shutdown()`: 1. Set isShuttingDown flag, stop accepting new events from Discord 2. Stop HeartbeatScheduler and CronScheduler 3. Fire shutdown hook via HookManager (enqueue and wait for processing) 4. Drain EventQueue 5. Disconnect DiscordBot 6. Exit with code 0 - _Requirements: 1.1, 1.3, 2.1, 3.4, 5.1, 5.2, 9.3, 10.1, 10.2, 10.3, 17.2, 17.3, 17.4, 18.2, 18.3, 18.4, 19.2, 19.5_ - [ ]* 19.2 Write property test: Concurrency limit rejection (Property 14) - **Property 14: Concurrency limit rejection** - Generate states where active query count ≥ configured max; verify new prompts are rejected with a "system busy" response - **Validates: Requirements 9.3** - [ ]* 19.3 Write unit tests for GatewayCore - Test startup sequence: config loaded, bootstrap run, Discord bot started, event queue initialized, schedulers started, startup hook fired - Test shutdown sequence: stops accepting prompts, stops schedulers, fires shutdown hook, drains queue, disconnects bot - Test typing indicator is sent on prompt reception and maintained during streaming - Test heartbeat timer events are enqueued when timers fire - Test cron job events are enqueued when jobs trigger - _Requirements: 1.1, 1.3, 3.4, 5.2, 10.1, 10.2, 10.3, 17.2, 17.3, 18.2, 18.3, 19.2, 19.5_ - [x] 20. Implement shutdown handler and entry point - [x] 20.1 Create `src/shutdown-handler.ts` - Listen for SIGTERM and SIGINT signals - Call `gateway.shutdown()` on signal - Wait for in-flight queries to complete or timeout, then exit with code 0 - _Requirements: 10.1, 10.2, 10.3_ - [x] 20.2 Create `src/index.ts` entry point - Instantiate GatewayCore and call `start()` - Register shutdown handler - Handle startup errors (log and exit with code 1) - _Requirements: 1.1, 1.2, 2.1, 2.2_ - [x] 21. Final checkpoint - Ensure all tests pass - Ensure all tests pass, ask the user if questions arise. ## Notes - Tasks marked with `*` are optional and can be skipped for faster MVP - Each task references specific requirements for traceability - Checkpoints ensure incremental validation - Property tests validate all 28 universal correctness properties from the design document - Unit tests validate specific examples and edge cases - Discord.js client and Agent SDK `query()` should be mocked in tests using vitest mocks - File system operations should be mocked using vitest's `vi.mock` for markdown config tests - node-cron should be mocked for cron scheduler tests to avoid real timer dependencies - fast-check property tests should use a minimum of 100 iterations per property