Initial commit: Discord-Claude Gateway with event-driven agent runtime

This commit is contained in:
2026-02-22 00:31:25 -05:00
commit 77d7c74909
58 changed files with 11772 additions and 0 deletions

View File

@@ -0,0 +1 @@
{"specId": "66d67457-3ea3-493c-9d0f-b868b51d309d", "workflowType": "requirements-first", "specType": "feature"}

View File

@@ -0,0 +1,908 @@
# Design Document: Discord-Claude Gateway
## Overview
The Discord-Claude Gateway is a TypeScript event-driven agent runtime platform that bridges Discord's messaging platform with the Claude Agent SDK. Inspired by the OpenClaw architecture, it goes beyond a simple chat bridge: it is a long-running process that accepts inputs from multiple sources (Discord messages, heartbeat timers, cron jobs, lifecycle hooks), routes them through a unified event queue, and dispatches them to an AI agent runtime for processing.
The agent's personality, identity, user context, long-term memory, tool configuration, and operating rules are all defined in local markdown files. The runtime reads these files fresh on each event processing cycle, assembles a dynamic system prompt, and passes it to the Agent SDK's `query()` function via the `systemPrompt` option. The agent can write back to `memory.md` using the Write tool, completing a read-process-write loop that persists state across sessions.
### Key Design Decisions
- **discord.js** for the Discord bot client — the most mature and widely-used Discord library for Node.js/TypeScript.
- **Unified event queue** — all inputs (messages, heartbeats, crons, hooks) enter a single in-memory FIFO queue, ensuring consistent ordering and preventing race conditions.
- **Markdown files as single source of truth** — all agent state and configuration lives in markdown files on disk. No database, no external state store. The runtime reconstructs full context by reading CONFIG_DIR on startup.
- **Fresh reads per event** — markdown config files are read from disk on each event processing cycle, so edits take effect immediately without restarts.
- **Per-channel session binding** — each Discord channel maps to at most one Agent SDK session, enabling conversational continuity.
- **Sequential event processing** — the event queue processes one event at a time to avoid concurrent writes to markdown files and ensure deterministic behavior.
- **Environment-variable-driven configuration** — all deployment settings are read from environment variables with sensible defaults.
- **node-cron** for cron scheduling — lightweight, well-maintained cron expression parser for Node.js.
- **Agent SDK `systemPrompt` option** — the assembled markdown content is injected via the `systemPrompt` option in the `query()` function call.
## Architecture
```mermaid
graph TD
A[Discord Users] -->|Messages / Slash Commands| B[DiscordBot]
B -->|message Event| C[EventQueue]
D[HeartbeatScheduler] -->|heartbeat Event| C
E[CronScheduler] -->|cron Event| C
F[HookManager] -->|hook Event| C
C -->|Dequeue FIFO| G[AgentRuntime]
G -->|Read configs| H[MarkdownConfigLoader]
H -->|soul.md, identity.md, etc.| I[CONFIG_DIR]
G -->|Assemble prompt| J[SystemPromptAssembler]
J -->|systemPrompt option| K[Agent SDK query]
K -->|Response Stream| L[ResponseFormatter]
L -->|Split & send| B
G -->|Agent writes memory.md| I
G -->|Signal complete| C
M[BootstrapManager] -->|Validate/create files| I
N[ConfigLoader] -->|Env vars| G
O[SessionManager] -->|Channel Bindings| G
P[GatewayCore] -->|Orchestrates| B
P -->|Orchestrates| C
P -->|Orchestrates| D
P -->|Orchestrates| E
P -->|Orchestrates| F
P -->|Orchestrates| M
P -->|Shutdown| Q[ShutdownHandler]
```
The system is composed of the following layers:
1. **Input Layer** — Multiple event sources feed into the unified queue:
- **DiscordBot**: Handles bot authentication, message/interaction reception, typing indicators, and message sending.
- **HeartbeatScheduler**: Manages recurring timers that fire heartbeat events at configured intervals.
- **CronScheduler**: Manages cron-expression-based scheduled events.
- **HookManager**: Fires lifecycle hook events (startup, agent_begin, agent_stop, shutdown).
2. **Event Queue** — A single in-memory FIFO queue that accepts all event types and dispatches them one at a time to the Agent Runtime.
3. **Agent Runtime** — The core processing engine that:
- Dequeues events from the EventQueue.
- Reads markdown config files via MarkdownConfigLoader.
- Assembles the system prompt via SystemPromptAssembler.
- Calls the Agent SDK `query()` with the assembled `systemPrompt` option.
- Routes responses back to the appropriate output channel.
- Signals the EventQueue when processing is complete.
4. **Configuration Layer**:
- **ConfigLoader**: Reads and validates environment variables at startup.
- **MarkdownConfigLoader**: Reads markdown files from CONFIG_DIR on each event cycle.
- **SystemPromptAssembler**: Concatenates markdown file contents with section headers into the system prompt.
- **BootstrapManager**: Validates and creates missing markdown files on first run.
5. **Session & Response Layer**:
- **SessionManager**: Maintains the mapping between Discord channel IDs and Agent SDK session IDs.
- **ResponseFormatter**: Splits long responses at safe boundaries (respecting code blocks and the 2000-char limit).
6. **Lifecycle Layer**:
- **GatewayCore**: The main orchestrator that wires all components and manages the startup/shutdown sequence.
- **ShutdownHandler**: Listens for SIGTERM/SIGINT, fires shutdown hook, drains the queue, and disconnects cleanly.
## Components and Interfaces
### ConfigLoader
Responsible for reading and validating environment variables at startup.
```typescript
interface GatewayConfig {
discordBotToken: string; // DISCORD_BOT_TOKEN (required)
anthropicApiKey: string; // ANTHROPIC_API_KEY (required)
allowedTools: string[]; // ALLOWED_TOOLS, default: ["Read","Write","Edit","Glob","Grep","WebSearch","WebFetch"]
permissionMode: string; // PERMISSION_MODE, default: "bypassPermissions"
queryTimeoutMs: number; // QUERY_TIMEOUT_MS, default: 120000
maxConcurrentQueries: number; // MAX_CONCURRENT_QUERIES, default: 5
configDir: string; // CONFIG_DIR, default: "./config"
maxQueueDepth: number; // MAX_QUEUE_DEPTH, default: 100
outputChannelId?: string; // OUTPUT_CHANNEL_ID, optional — default channel for heartbeat/cron output
}
function loadConfig(): GatewayConfig;
// Throws with descriptive message listing missing required vars if validation fails.
```
### DiscordBot
Wraps the discord.js `Client`, registers slash commands, and exposes event handlers.
```typescript
interface DiscordBot {
start(token: string): Promise<void>;
// Authenticates and waits for ready state. Logs username and guild count.
registerCommands(): Promise<void>;
// Registers /claude and /claude-reset slash commands.
sendMessage(channelId: string, content: string): Promise<void>;
// Sends a message to a channel. Logs errors if Discord API rejects.
sendTyping(channelId: string): Promise<void>;
// Sends a typing indicator to a channel.
destroy(): Promise<void>;
// Disconnects the bot from Discord.
onPrompt(handler: (prompt: Prompt) => void): void;
// Registers a callback for incoming prompts (from mentions or /claude).
onReset(handler: (channelId: string) => void): void;
// Registers a callback for /claude-reset commands.
}
interface Prompt {
text: string;
channelId: string;
userId: string;
guildId: string | null;
}
```
### EventQueue
Unified in-memory FIFO queue that accepts all event types and dispatches them sequentially to the AgentRuntime.
```typescript
interface Event {
id: number; // Monotonically increasing sequence number
type: EventType; // "message" | "heartbeat" | "cron" | "hook" | "webhook"
payload: EventPayload; // Type-specific payload
timestamp: Date; // Enqueue timestamp
source: string; // Source identifier (e.g., "discord", "heartbeat-scheduler", "cron-scheduler")
}
type EventType = "message" | "heartbeat" | "cron" | "hook" | "webhook";
interface MessagePayload {
prompt: Prompt; // The Discord prompt
}
interface HeartbeatPayload {
instruction: string; // The heartbeat check instruction
checkName: string; // Name of the heartbeat check
}
interface CronPayload {
instruction: string; // The cron job instruction
jobName: string; // Name of the cron job
}
interface HookPayload {
hookType: HookType; // "startup" | "agent_begin" | "agent_stop" | "shutdown"
instruction?: string; // Optional instruction prompt from agents.md
}
type HookType = "startup" | "agent_begin" | "agent_stop" | "shutdown";
type EventPayload = MessagePayload | HeartbeatPayload | CronPayload | HookPayload;
interface EventQueue {
enqueue(event: Omit<Event, "id" | "timestamp">): Event | null;
// Assigns sequence number and timestamp. Returns the event, or null if queue is full.
dequeue(): Event | undefined;
// Returns the next event in FIFO order, or undefined if empty.
size(): number;
// Returns current queue depth.
onEvent(handler: (event: Event) => Promise<void>): void;
// Registers the processing handler. The queue calls this for each event
// and waits for the promise to resolve before dispatching the next.
drain(): Promise<void>;
// Returns a promise that resolves when the queue is empty and no event is processing.
}
```
### AgentRuntime
Core processing engine that reads markdown configs, assembles the system prompt, and calls the Agent SDK.
```typescript
interface AgentRuntime {
processEvent(event: Event): Promise<EventResult>;
// Main entry point. Reads markdown configs, assembles system prompt,
// calls Agent SDK query(), and returns the result.
// For message events: uses the prompt text and resumes/creates sessions.
// For heartbeat/cron events: uses the instruction text as the prompt.
// For hook events: uses the hook instruction (if any) as the prompt.
}
interface EventResult {
responseText?: string; // The agent's response text (if any)
targetChannelId?: string; // Discord channel to send the response to
sessionId?: string; // Session ID (for message events)
}
```
### MarkdownConfigLoader
Reads markdown configuration files from CONFIG_DIR. Files are read fresh on each call (no caching) so that edits take effect immediately.
```typescript
interface MarkdownConfigs {
soul: string | null; // soul.md content, null if missing
identity: string | null; // identity.md content, null if missing
agents: string | null; // agents.md content, null if missing
user: string | null; // user.md content, null if missing
memory: string | null; // memory.md content, null if missing
tools: string | null; // tools.md content, null if missing
}
interface MarkdownConfigLoader {
loadAll(configDir: string): Promise<MarkdownConfigs>;
// Reads all markdown config files. Returns null for missing files.
// Logs warnings for missing soul.md, identity.md, agents.md, user.md, tools.md.
// Creates memory.md with "# Memory" header if missing.
loadFile(configDir: string, filename: string): Promise<string | null>;
// Reads a single markdown file. Returns null if missing.
}
```
### SystemPromptAssembler
Assembles the system prompt from markdown config file contents.
```typescript
interface SystemPromptAssembler {
assemble(configs: MarkdownConfigs): string;
// Concatenates markdown file contents in order:
// 1. Identity (## Identity)
// 2. Soul (## Personality)
// 3. Agents (## Operating Rules)
// 4. User (## User Context)
// 5. Memory (## Long-Term Memory)
// 6. Tools (## Tool Configuration)
//
// Each section is wrapped: "## [Section Name]\n\n[content]\n\n"
// Sections with null/empty content are omitted.
// A preamble is prepended instructing the agent it may update memory.md.
}
```
### HeartbeatScheduler
Manages recurring heartbeat timers based on heartbeat.md configuration.
```typescript
interface HeartbeatCheck {
name: string; // Check name/identifier
instruction: string; // Instruction prompt for the agent
intervalSeconds: number; // Interval between checks (minimum 60)
}
interface HeartbeatScheduler {
start(checks: HeartbeatCheck[], enqueue: (event: Omit<Event, "id" | "timestamp">) => Event | null): void;
// Starts a recurring timer for each check. On each tick, creates a heartbeat
// event and enqueues it. Rejects checks with interval < 60 seconds.
stop(): void;
// Stops all heartbeat timers.
parseConfig(content: string): HeartbeatCheck[];
// Parses heartbeat.md content into check definitions.
}
```
### CronScheduler
Manages cron-expression-based scheduled events parsed from agents.md.
```typescript
interface CronJob {
name: string; // Job name/identifier
expression: string; // Cron expression (e.g., "0 9 * * 1")
instruction: string; // Instruction prompt for the agent
}
interface CronScheduler {
start(jobs: CronJob[], enqueue: (event: Omit<Event, "id" | "timestamp">) => Event | null): void;
// Schedules each cron job. On each trigger, creates a cron event and enqueues it.
// Logs warning and skips jobs with invalid cron expressions.
stop(): void;
// Stops all cron jobs.
parseConfig(content: string): CronJob[];
// Parses the "Cron Jobs" section from agents.md into job definitions.
}
```
### HookManager
Fires lifecycle hook events at appropriate points in the Gateway lifecycle.
```typescript
interface HookConfig {
startup?: string; // Instruction prompt for startup hook
agent_begin?: string; // Instruction prompt for agent_begin hook
agent_stop?: string; // Instruction prompt for agent_stop hook
shutdown?: string; // Instruction prompt for shutdown hook
}
interface HookManager {
fire(hookType: HookType, enqueue: (event: Omit<Event, "id" | "timestamp">) => Event | null): void;
// Creates a hook event and enqueues it.
// agent_begin and agent_stop are processed inline (not re-enqueued).
fireInline(hookType: HookType, runtime: AgentRuntime): Promise<void>;
// For agent_begin/agent_stop: processes the hook inline without going through the queue.
parseConfig(content: string): HookConfig;
// Parses the "Hooks" section from agents.md into hook configuration.
}
```
### BootstrapManager
Handles first-run setup: validates markdown files exist, creates missing ones with defaults.
```typescript
interface BootConfig {
requiredFiles: string[]; // Files that must exist (default: ["soul.md", "identity.md"])
optionalFiles: string[]; // Files created with defaults if missing
defaults: Record<string, string>; // Default content for each file
}
interface BootstrapManager {
run(configDir: string): Promise<BootstrapResult>;
// 1. Reads boot.md for bootstrap parameters (or uses built-in defaults).
// 2. Verifies all required markdown files exist.
// 3. Creates missing optional files with default content.
// 4. Logs loaded files and any files created with defaults.
parseBootConfig(content: string | null): BootConfig;
// Parses boot.md content into bootstrap parameters.
// Returns built-in defaults if content is null.
}
interface BootstrapResult {
loadedFiles: string[]; // Files that existed and were loaded
createdFiles: string[]; // Files that were created with defaults
}
```
### SessionManager
Manages the mapping between Discord channels and Agent SDK sessions.
```typescript
interface SessionManager {
getSessionId(channelId: string): string | undefined;
setSessionId(channelId: string, sessionId: string): void;
removeSession(channelId: string): void;
clear(): void;
}
```
### ResponseFormatter
Splits long response text into Discord-safe chunks.
```typescript
function splitMessage(text: string, maxLength?: number): string[];
// Splits text into chunks of at most maxLength (default 2000) characters.
// Preserves code block formatting: if a split occurs inside a code block,
// the chunk is closed with ``` and the next chunk reopens with ```.
// Splits prefer line boundaries over mid-line breaks.
```
### GatewayCore
The main orchestrator that wires all components together and manages the full lifecycle.
```typescript
interface GatewayCore {
start(): Promise<void>;
// 1. Load config (ConfigLoader)
// 2. Run bootstrap (BootstrapManager)
// 3. Start Discord bot (DiscordBot)
// 4. Initialize EventQueue and AgentRuntime
// 5. Parse heartbeat.md → start HeartbeatScheduler
// 6. Parse agents.md → start CronScheduler, load HookConfig
// 7. Fire startup hook
// 8. Begin accepting events
shutdown(): Promise<void>;
// 1. Stop accepting new events from Discord
// 2. Stop HeartbeatScheduler and CronScheduler
// 3. Fire shutdown hook (enqueue and wait for processing)
// 4. Drain EventQueue
// 5. Disconnect DiscordBot
// 6. Exit with code 0
}
```
### Agent SDK Integration
The gateway calls the Agent SDK `query()` function with the assembled system prompt:
```typescript
import { query } from "@anthropic-ai/claude-agent-sdk";
// For a message event (new session):
const stream = query({
prompt: event.payload.prompt.text,
options: {
allowedTools: config.allowedTools,
permissionMode: config.permissionMode,
systemPrompt: assembledSystemPrompt, // Injected via systemPrompt option
}
});
// For a message event (resumed session):
const stream = query({
prompt: event.payload.prompt.text,
options: {
resume: sessionId,
allowedTools: config.allowedTools,
permissionMode: config.permissionMode,
systemPrompt: assembledSystemPrompt,
}
});
// For a heartbeat/cron event:
const stream = query({
prompt: event.payload.instruction,
options: {
allowedTools: config.allowedTools,
permissionMode: config.permissionMode,
systemPrompt: assembledSystemPrompt,
}
});
// Processing the stream:
for await (const message of stream) {
if (message.type === "system" && message.subtype === "init") {
sessionManager.setSessionId(channelId, message.session_id);
}
if ("result" in message) {
const chunks = splitMessage(message.result);
for (const chunk of chunks) {
await discordBot.sendMessage(targetChannelId, chunk);
}
}
}
```
## Data Models
### Event
```typescript
interface Event {
id: number; // Monotonically increasing sequence number
type: EventType; // "message" | "heartbeat" | "cron" | "hook" | "webhook"
payload: EventPayload; // Type-specific payload
timestamp: Date; // Enqueue timestamp
source: string; // Source identifier
}
```
### Channel Binding
```typescript
// In-memory Map<string, string>
// Key: Discord channel ID
// Value: Agent SDK session ID
type ChannelBindings = Map<string, string>;
```
### Prompt
```typescript
interface Prompt {
text: string; // The extracted prompt text
channelId: string; // Discord channel ID where the prompt originated
userId: string; // Discord user ID of the sender
guildId: string | null; // Discord guild ID (null for DMs)
}
```
### Markdown Configs
```typescript
interface MarkdownConfigs {
soul: string | null; // soul.md content
identity: string | null; // identity.md content
agents: string | null; // agents.md content
user: string | null; // user.md content
memory: string | null; // memory.md content
tools: string | null; // tools.md content
}
```
### Gateway State
```typescript
interface GatewayState {
config: GatewayConfig;
channelBindings: ChannelBindings;
activeQueryCount: number; // Number of currently executing Agent SDK queries
isShuttingDown: boolean; // True after receiving shutdown signal
eventQueue: EventQueue;
nextEventId: number; // Next sequence number for events
}
```
### Heartbeat and Cron Definitions
```typescript
interface HeartbeatCheck {
name: string;
instruction: string;
intervalSeconds: number; // Minimum 60
}
interface CronJob {
name: string;
expression: string; // Standard cron expression
instruction: string;
}
```
### Hook Configuration
```typescript
interface HookConfig {
startup?: string;
agent_begin?: string;
agent_stop?: string;
shutdown?: string;
}
```
### Bootstrap Configuration
```typescript
interface BootConfig {
requiredFiles: string[];
optionalFiles: string[];
defaults: Record<string, string>;
}
interface BootstrapResult {
loadedFiles: string[];
createdFiles: string[];
}
```
## Correctness Properties
*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
### Property 1: Config loading round-trip
*For any* set of valid environment variable values for DISCORD_BOT_TOKEN, ANTHROPIC_API_KEY, ALLOWED_TOOLS, PERMISSION_MODE, QUERY_TIMEOUT_MS, CONFIG_DIR, and MAX_QUEUE_DEPTH, calling `loadConfig()` should produce a `GatewayConfig` whose fields match the corresponding environment variable values, with ALLOWED_TOOLS correctly split from a comma-separated string into an array.
**Validates: Requirements 8.1, 2.3**
### Property 2: Missing required config reports all missing values
*For any* subset of the required environment variables (DISCORD_BOT_TOKEN, ANTHROPIC_API_KEY) that are unset, calling `loadConfig()` should throw an error whose message contains the names of all missing variables.
**Validates: Requirements 8.3, 1.2, 2.2**
### Property 3: Mention prompt extraction
*For any* Discord message containing a bot mention and arbitrary surrounding text, the prompt extraction function should return the message text with the mention removed and leading/trailing whitespace trimmed.
**Validates: Requirements 3.1**
### Property 4: Slash command prompt extraction
*For any* slash command interaction with a prompt option value, the prompt extraction function should return exactly the option value as the prompt text.
**Validates: Requirements 3.2**
### Property 5: Bot message filtering
*For any* Discord message where the author has the bot flag set, the gateway should not process it as a prompt. Conversely, for any message from a non-bot user that mentions the bot, the gateway should process it.
**Validates: Requirements 3.3**
### Property 6: Query arguments correctness
*For any* prompt text, gateway configuration, and assembled system prompt, the arguments passed to the Agent SDK `query()` function should include the prompt text, the configured allowed tools array, the configured permission mode, and the assembled system prompt via the `systemPrompt` option.
**Validates: Requirements 4.1, 4.4, 12.3**
### Property 7: Session resume on existing binding
*For any* channel that has a stored session ID in the channel bindings, when a new prompt is received for that channel, the `query()` call should include the `resume` option set to the stored session ID.
**Validates: Requirements 4.2, 6.2**
### Property 8: New session creation and storage
*For any* channel without an existing channel binding, when a prompt is processed and the Agent SDK returns an init message with a session_id, the session manager should store that session_id for the channel. A subsequent lookup for that channel should return the stored session_id.
**Validates: Requirements 4.3, 6.1**
### Property 9: Message splitting with code block preservation
*For any* string of arbitrary length, `splitMessage(text)` should produce chunks where: (a) every chunk is at most 2000 characters, (b) concatenating all chunks reproduces the original text (modulo inserted code block delimiters), and (c) if the original text contains a code block that spans a split boundary, each chunk is a valid markdown fragment with properly opened and closed code fences.
**Validates: Requirements 5.3, 5.4**
### Property 10: Reset removes channel binding
*For any* channel with a stored session ID, invoking the reset handler for that channel should result in the session manager returning `undefined` for that channel's session ID.
**Validates: Requirements 6.3**
### Property 11: Concurrent session isolation
*For any* two distinct channel IDs with different stored session IDs, operations on one channel (setting, getting, or removing its session) should not affect the session ID stored for the other channel.
**Validates: Requirements 6.4**
### Property 12: Error message formatting
*For any* error thrown by the Agent SDK (with a type/name and a message), the user-facing error message produced by the gateway should contain the error type/name but should not contain stack traces, API keys, or file paths from the server environment.
**Validates: Requirements 7.1**
### Property 13: Sequential per-channel queue ordering
*For any* sequence of tasks enqueued for the same channel, the tasks should execute in FIFO order, and no two tasks for the same channel should execute concurrently.
**Validates: Requirements 9.2**
### Property 14: Concurrency limit rejection
*For any* gateway state where the active query count is at or above the configured maximum, attempting to process a new prompt should be rejected with a "system busy" response rather than being forwarded to the Agent SDK.
**Validates: Requirements 9.3**
### Property 15: Event queue accepts all event types with monotonic IDs
*For any* sequence of events of mixed types (message, heartbeat, cron, hook, webhook), enqueuing them should succeed (when below max depth), and each event should be assigned a strictly increasing sequence number and a timestamp no earlier than the previous event's timestamp.
**Validates: Requirements 11.1, 11.2**
### Property 16: Event queue FIFO dispatch with sequential processing
*For any* sequence of events enqueued in the EventQueue, the processing handler should be called with events in the exact order they were enqueued, and the handler for event N+1 should not be called until the handler for event N has completed.
**Validates: Requirements 11.3, 11.4, 12.5**
### Property 17: Event queue depth rejection
*For any* EventQueue that has reached its configured maximum depth, attempting to enqueue a new event should return null (rejection) and the queue size should remain at the maximum.
**Validates: Requirements 11.5**
### Property 18: Non-message events use instruction as prompt
*For any* heartbeat or cron event with an instruction string, when the AgentRuntime processes it, the Agent SDK `query()` call should use the instruction string as the prompt text and include the assembled system prompt via the `systemPrompt` option.
**Validates: Requirements 12.4, 17.4, 18.4**
### Property 19: System prompt assembly with section headers and ordering
*For any* set of MarkdownConfigs where at least one field is non-null, the assembled system prompt should: (a) wrap each non-null config in a section with the format `## [Section Name]\n\n[content]`, (b) order sections as Identity, Personality, Operating Rules, User Context, Long-Term Memory, Tool Configuration, and (c) include a preamble instructing the agent it may update memory.md using the Write tool.
**Validates: Requirements 22.1, 22.2, 22.4, 12.2, 14.2, 15.2, 15.3, 16.2**
### Property 20: Missing or empty configs are omitted from system prompt
*For any* set of MarkdownConfigs where some fields are null or empty strings, the assembled system prompt should not contain section headers for those missing/empty configs, and the number of section headers in the output should equal the number of non-null, non-empty config fields.
**Validates: Requirements 22.3, 13.4, 14.3, 16.3**
### Property 21: System prompt assembly round-trip
*For any* valid set of MarkdownConfigs (with non-null, non-empty values), assembling the system prompt and then parsing the section headers from the output should produce the same set of section names as the non-null input config fields.
**Validates: Requirements 22.5**
### Property 22: Heartbeat config parsing
*For any* valid heartbeat.md content containing check definitions with names, instructions, and intervals, parsing the content should produce HeartbeatCheck objects whose fields match the defined values.
**Validates: Requirements 17.1**
### Property 23: Heartbeat minimum interval enforcement
*For any* heartbeat check definition with an interval less than 60 seconds, the HeartbeatScheduler should reject the definition and not start a timer for it.
**Validates: Requirements 17.6**
### Property 24: Cron job config parsing
*For any* valid agents.md content containing a "Cron Jobs" section with job definitions (name, cron expression, instruction), parsing the content should produce CronJob objects whose fields match the defined values.
**Validates: Requirements 18.1**
### Property 25: Invalid cron expression rejection
*For any* cron job definition with a syntactically invalid cron expression, the CronScheduler should skip scheduling for that job without affecting other valid jobs.
**Validates: Requirements 18.5**
### Property 26: Lifecycle hooks fire for every event
*For any* event processed by the AgentRuntime, the agent_begin hook should fire before the main processing and the agent_stop hook should fire after the main processing completes, both processed inline.
**Validates: Requirements 19.3, 19.4**
### Property 27: Bootstrap creates missing files with defaults
*For any* set of required files specified in BootConfig where some files are missing from CONFIG_DIR, the bootstrap process should create each missing file with its default content, and after bootstrap all required files should exist.
**Validates: Requirements 20.2, 20.3**
### Property 28: State reconstruction after restart
*For any* set of markdown configuration files in CONFIG_DIR, reading all configs and assembling the system prompt should produce the same result regardless of whether it's the first read or a subsequent read after a simulated restart (i.e., the markdown files are the complete source of truth with no in-memory state dependency).
**Validates: Requirements 21.3**
## Error Handling
### Startup Errors
- **Missing required config**: `loadConfig()` throws with a message listing all missing required environment variables. The process exits with code 1.
- **Invalid Discord token**: The discord.js client emits an error event. The gateway catches it, logs the error, and exits with code 1.
- **Network failures on startup**: If Discord or the API is unreachable, the gateway logs the connection error and exits with code 1.
- **Missing boot.md**: The BootstrapManager falls back to built-in defaults (require soul.md and identity.md, create missing optional files with empty headers).
- **Missing required markdown files**: The BootstrapManager creates them with default content and logs which files were created.
### Runtime Errors
- **Agent SDK query errors**: Caught per-event. For message events, the gateway formats a user-friendly message (error type only, no internals) and sends it to the Discord channel. For heartbeat/cron events, the error is logged.
- **Session corruption**: If the Agent SDK returns an error indicating the session is invalid, the gateway removes the channel binding and informs the user to retry.
- **Query timeout**: A `Promise.race` between the query stream processing and a timeout timer. On timeout, the gateway sends a timeout notification to the channel (for message events) and aborts the stream iteration.
- **Discord API send failures**: Caught and logged with channel ID and content length. The event processing continues.
- **Concurrency limit exceeded**: New prompts are immediately rejected with a "system busy" message. No query is started.
- **Event queue overflow**: When the queue reaches max depth, new events are rejected (enqueue returns null). For message events, a "system busy" message is sent. For heartbeat/cron events, the rejection is logged.
- **Markdown file read errors**: If a config file cannot be read (permissions, I/O error), the MarkdownConfigLoader logs the error and returns null for that file. The system prompt is assembled without the failed section.
- **Invalid heartbeat interval**: Heartbeat checks with interval < 60 seconds are rejected with a warning log. Other valid checks still start.
- **Invalid cron expression**: Invalid cron jobs are skipped with a warning log. Other valid jobs still schedule.
- **Memory.md write failures**: If the agent's Write tool call to memory.md fails, the error is logged. The event still completes, but the memory update is lost.
### Shutdown Errors
- **In-flight event timeout during shutdown**: The gateway waits up to the configured timeout for the current event to complete. If it doesn't complete, it is abandoned after the timeout.
- **Shutdown hook processing failure**: If the shutdown hook event fails, the error is logged and shutdown continues.
- **Discord disconnect failure**: Logged but does not prevent process exit.
## Testing Strategy
### Property-Based Testing
Use **fast-check** as the property-based testing library for TypeScript.
Each correctness property from the design document maps to a single property-based test. Tests should be configured with a minimum of 100 iterations per property.
Each test must be tagged with a comment in the format:
`// Feature: discord-claude-gateway, Property {number}: {property_text}`
Key property tests organized by component:
**Config & Startup (Properties 1, 2)**:
- Generate random env var combinations and verify config loading behavior.
- Generate subsets of missing required vars and verify error messages.
**Prompt Handling (Properties 3, 4, 5)**:
- Generate random message content with mentions and verify extraction.
- Generate random slash command option values and verify extraction.
- Generate messages with random bot/non-bot authors and verify filtering.
**Agent SDK Integration (Properties 6, 7, 8, 18)**:
- Generate random prompts, configs, and system prompts; verify query arguments.
- Generate random channel/session pairs; verify resume behavior.
- Generate random heartbeat/cron instructions; verify they're used as prompts.
**Response Formatting (Property 9)**:
- Generate strings of varying lengths with and without code blocks; verify chunk sizes and formatting.
**Session Management (Properties 10, 11)**:
- Generate random channel IDs and session IDs; verify CRUD operations and isolation.
**Error Handling (Property 12)**:
- Generate random error objects; verify formatted message excludes sensitive data.
**Concurrency (Properties 13, 14)**:
- Enqueue tasks with observable side effects; verify execution order.
- Generate random active query counts; verify rejection behavior.
**Event Queue (Properties 15, 16, 17)**:
- Generate mixed-type event sequences; verify monotonic IDs and FIFO dispatch.
- Generate queue-at-capacity scenarios; verify rejection.
**System Prompt Assembly (Properties 19, 20, 21)**:
- Generate random MarkdownConfigs with various null/non-null combinations; verify section headers, ordering, omission of empty sections, and round-trip parsing.
**Config Parsing (Properties 22, 23, 24, 25)**:
- Generate random heartbeat.md content; verify parsing and interval enforcement.
- Generate random agents.md cron sections; verify parsing and invalid expression rejection.
**Lifecycle & Bootstrap (Properties 26, 27, 28)**:
- Generate event sequences; verify agent_begin/agent_stop hooks fire for each.
- Generate file sets with missing files; verify bootstrap creates them.
- Generate markdown file sets; verify state reconstruction produces consistent results.
### Unit Testing
Unit tests complement property tests by covering specific examples, edge cases, and integration points:
- **Config defaults** (Req 8.2): Verify specific default values when optional env vars are unset.
- **Startup logging** (Req 1.3): Verify log output contains bot username and guild count.
- **Typing indicator** (Req 3.4, 5.2): Verify typing indicator is sent when processing starts and maintained during streaming.
- **Timeout handling** (Req 7.2): Verify timeout notification is sent after the configured period.
- **Discord API error logging** (Req 7.3): Verify log contains channel ID and content length.
- **Session corruption recovery** (Req 7.4): Verify binding removal and user notification on session error.
- **Shutdown sequence** (Req 10.1, 10.2, 10.3): Verify the gateway stops accepting prompts, waits for in-flight queries, and disconnects.
- **Result forwarding** (Req 5.1): Verify result messages are sent to the correct channel.
- **Markdown file reading** (Req 13.1-13.3, 14.1, 15.1, 16.1): Verify each config file is read from the correct path.
- **Hot-reload behavior** (Req 13.5, 14.4, 16.4): Verify modified files are picked up on next event cycle.
- **Memory.md auto-creation** (Req 15.5): Verify memory.md is created with "# Memory" header when missing.
- **Heartbeat timer startup** (Req 17.2, 17.3): Verify timers start and fire events.
- **Missing heartbeat.md** (Req 17.5): Verify gateway operates without heartbeat events.
- **Cron job scheduling** (Req 18.2, 18.3): Verify cron jobs schedule and fire events.
- **Hook types** (Req 19.1): Verify all four hook types are supported.
- **Startup hook** (Req 19.2): Verify startup hook fires after initialization.
- **Shutdown hook** (Req 19.5): Verify shutdown hook fires and is processed before exit.
- **Hook config parsing** (Req 19.6): Verify hooks section is parsed from agents.md.
- **Boot.md reading** (Req 20.1): Verify boot config is read.
- **Bootstrap logging** (Req 20.4): Verify log lists loaded and created files.
- **Missing boot.md defaults** (Req 20.5): Verify built-in defaults are used.
- **Memory write ordering** (Req 21.2): Verify memory changes are written before event completion signal.
### Test Organization
```
tests/
unit/
config-loader.test.ts
discord-bot.test.ts
session-manager.test.ts
response-formatter.test.ts
gateway-core.test.ts
event-queue.test.ts
agent-runtime.test.ts
markdown-config-loader.test.ts
system-prompt-assembler.test.ts
heartbeat-scheduler.test.ts
cron-scheduler.test.ts
hook-manager.test.ts
bootstrap-manager.test.ts
property/
config.property.test.ts
prompt-extraction.property.test.ts
message-splitting.property.test.ts
session-manager.property.test.ts
error-formatting.property.test.ts
channel-queue.property.test.ts
concurrency.property.test.ts
event-queue.property.test.ts
system-prompt.property.test.ts
heartbeat-config.property.test.ts
cron-config.property.test.ts
lifecycle-hooks.property.test.ts
bootstrap.property.test.ts
state-reconstruction.property.test.ts
```
### Testing Tools
- **vitest** as the test runner
- **fast-check** for property-based testing
- Discord.js client and Agent SDK `query()` should be mocked in unit tests using vitest mocks
- File system operations should be mocked using `memfs` or vitest's `vi.mock` for markdown config tests
- **node-cron** should be mocked for cron scheduler tests to avoid real timer dependencies

View File

@@ -0,0 +1,284 @@
# Requirements Document
## Introduction
The Discord-Claude Gateway is an agent runtime platform inspired by OpenClaw that connects a Discord bot to Claude via the Claude Agent SDK. At its core, it is a long-running process that accepts messages from Discord, routes them through a unified event queue, and dispatches them to an AI agent runtime for processing. The agent's personality, identity, user context, long-term memory, tool configuration, and operating rules are all defined in local markdown files that the runtime reads on each wake-up. Beyond user messages, the platform supports five input types: Messages, Heartbeats (timer-based proactive checks), Cron Jobs (scheduled events), Hooks (internal state change triggers), and Webhooks (external system events). All inputs enter a unified event queue, are processed by the agent runtime, and state is persisted back to markdown files, completing the event loop.
## Glossary
- **Gateway**: The long-running middleware process that accepts connections from Discord, manages the event queue, and routes events to the Agent_Runtime.
- **Agent_Runtime**: The core processing engine that dequeues events, assembles context from markdown configuration files, executes Agent SDK queries, performs actions using tools, and persists state changes.
- **Discord_Bot**: The Discord.js bot client that listens for messages and slash commands in Discord channels and guilds.
- **Agent_SDK**: The Claude Agent SDK (`@anthropic-ai/claude-agent-sdk`) TypeScript library that provides the `query()` function with a `systemPrompt` option for sending prompts to Claude and receiving streamed responses with built-in tool execution.
- **Event_Queue**: The unified in-memory queue that receives all input events (messages, heartbeats, crons, hooks, webhooks) and dispatches them to the Agent_Runtime for sequential processing.
- **Event**: A discrete unit of work entering the Event_Queue, carrying a type (message, heartbeat, cron, hook, webhook), payload, timestamp, and source metadata.
- **Session**: A stateful conversation context maintained by the Agent SDK, identified by a session ID, that preserves conversation history across multiple exchanges.
- **Prompt**: A text message submitted by a Discord user intended to be forwarded to Claude via the Agent SDK.
- **Response_Stream**: The async iterable of messages returned by the Agent SDK's `query()` function, containing assistant text, tool use events, and result messages.
- **Channel_Binding**: The association between a Discord channel and an active Agent SDK session, enabling conversational continuity.
- **Allowed_Tools**: The configurable list of Agent SDK built-in tools (Read, Write, Edit, Bash, Glob, Grep, WebSearch, WebFetch) that Claude is permitted to use when processing prompts.
- **Permission_Mode**: The Agent SDK permission configuration that controls tool execution approval behavior (bypassPermissions, acceptEdits, plan, default).
- **Soul_Config**: The markdown file (`soul.md`) that defines the agent's personality, tone, values, and behavior defaults.
- **Identity_Config**: The markdown file (`identity.md`) that defines the agent's name, role, and specialization.
- **Agents_Config**: The markdown file (`agents.md`) that defines operating rules, workflows, and safety/permission boundaries.
- **User_Config**: The markdown file (`user.md`) that stores information about the user: identity, preferences, and life/work context.
- **Memory_Store**: The markdown file (`memory.md`) that stores long-term durable facts, IDs, lessons learned, and other persistent knowledge across sessions.
- **Tools_Config**: The markdown file (`tools.md`) that documents API configurations, tool usage limits, and gotchas.
- **Heartbeat_Config**: The markdown file (`heartbeat.md`) that defines proactive check instructions, intervals, and prompts for timer-based events.
- **Boot_Config**: The markdown file (`boot.md`) that defines bootstrap configuration and initial setup parameters.
- **Bootstrap_Process**: The initial setup sequence that loads Boot_Config, validates markdown files, and prepares the Agent_Runtime for operation.
- **Heartbeat**: A timer-based event that fires at configurable intervals, triggering the agent to perform proactive checks (e.g., check email, review calendar).
- **Cron_Job**: A scheduled event with exact timing (cron expression) and custom instructions that enters the Event_Queue at the specified time.
- **Hook**: An internal state change trigger (e.g., startup, agent_begin, agent_stop) that fires when specific lifecycle events occur within the Gateway.
- **System_Prompt**: The assembled prompt text constructed by combining Soul_Config, Identity_Config, Agents_Config, User_Config, Memory_Store, and Tools_Config content, passed to the Agent SDK `query()` function via the `systemPrompt` option.
## Requirements
### Requirement 1: Discord Bot Initialization
**User Story:** As an operator, I want the Gateway to connect to Discord on startup, so that it can begin receiving user messages.
#### Acceptance Criteria
1. WHEN the Gateway starts, THE Discord_Bot SHALL authenticate with Discord using a configured bot token and transition to a ready state.
2. IF the bot token is missing or invalid, THEN THE Gateway SHALL log a descriptive error message and terminate with a non-zero exit code.
3. WHEN the Discord_Bot reaches the ready state, THE Gateway SHALL log the bot's username and the number of guilds it has joined.
### Requirement 2: Agent SDK Initialization
**User Story:** As an operator, I want the Gateway to validate Claude Agent SDK credentials on startup, so that I know the Claude integration is functional before accepting user prompts.
#### Acceptance Criteria
1. WHEN the Gateway starts, THE Gateway SHALL verify that the ANTHROPIC_API_KEY environment variable is set.
2. IF the ANTHROPIC_API_KEY environment variable is missing, THEN THE Gateway SHALL log a descriptive error message and terminate with a non-zero exit code.
3. THE Gateway SHALL load Allowed_Tools and Permission_Mode from a configuration source (environment variables or config file).
### Requirement 3: Prompt Reception via Discord
**User Story:** As a Discord user, I want to send prompts to Claude by mentioning the bot or using a slash command, so that I can interact with Claude from within Discord.
#### Acceptance Criteria
1. WHEN a user sends a message that mentions the Discord_Bot, THE Gateway SHALL extract the text content (excluding the mention) and treat it as a Prompt.
2. WHEN a user invokes the `/claude` slash command with a prompt argument, THE Gateway SHALL extract the prompt argument and treat it as a Prompt.
3. THE Gateway SHALL ignore messages sent by other bots to prevent feedback loops.
4. WHEN a Prompt is received, THE Gateway SHALL send a typing indicator in the originating Discord channel while processing.
### Requirement 4: Prompt Forwarding to Claude Agent SDK
**User Story:** As a Discord user, I want my prompts forwarded to Claude Code, so that I receive intelligent responses powered by Claude's agent capabilities.
#### Acceptance Criteria
1. WHEN a valid Prompt is received, THE Gateway SHALL call the Agent SDK `query()` function with the Prompt text, the configured Allowed_Tools, and the configured Permission_Mode.
2. WHILE a Channel_Binding exists for the originating Discord channel, THE Gateway SHALL resume the existing Session by passing the session ID to the `query()` function.
3. WHEN a new Prompt is received in a channel without a Channel_Binding, THE Gateway SHALL create a new Session and store the Channel_Binding.
4. THE Gateway SHALL pass the assembled System_Prompt to the Agent SDK `query()` function via the `systemPrompt` option to inject personality, identity, user context, and memory.
### Requirement 5: Response Streaming and Delivery
**User Story:** As a Discord user, I want to see Claude's responses in my Discord channel, so that I can read and act on the information Claude provides.
#### Acceptance Criteria
1. WHEN the Agent SDK emits a result message in the Response_Stream, THE Gateway SHALL send the result text as a message in the originating Discord channel.
2. WHILE the Response_Stream is active, THE Gateway SHALL maintain the typing indicator in the Discord channel.
3. IF the response text exceeds 2000 characters (Discord's message limit), THEN THE Gateway SHALL split the response into multiple sequential messages, each within the character limit, preserving code block formatting across splits.
4. WHEN the Response_Stream contains markdown code blocks, THE Gateway SHALL preserve the markdown formatting in the Discord message.
### Requirement 6: Session Management
**User Story:** As a Discord user, I want my conversation context preserved across messages in the same channel, so that I can have multi-turn conversations with Claude.
#### Acceptance Criteria
1. WHEN the Agent SDK returns an init message with a session_id, THE Gateway SHALL store the session_id in the Channel_Binding for the originating Discord channel.
2. WHILE a Channel_Binding exists, THE Gateway SHALL use the stored session_id to resume the Session on subsequent prompts from the same channel.
3. WHEN a user invokes the `/claude-reset` slash command, THE Gateway SHALL remove the Channel_Binding for that channel, causing the next prompt to start a new Session.
4. THE Gateway SHALL support concurrent Sessions across multiple Discord channels without interference.
### Requirement 7: Error Handling
**User Story:** As a Discord user, I want to see clear error messages when something goes wrong, so that I understand why my prompt was not processed.
#### Acceptance Criteria
1. IF the Agent SDK `query()` call throws an error, THEN THE Gateway SHALL send a user-friendly error message to the originating Discord channel that includes the error type without exposing internal details.
2. IF the Agent SDK does not produce a result within a configurable timeout period, THEN THE Gateway SHALL send a timeout notification to the Discord channel and cancel the pending query.
3. IF the Discord API rejects a message send operation, THEN THE Gateway SHALL log the error with the channel ID and message content length.
4. IF a Session becomes invalid or corrupted, THEN THE Gateway SHALL remove the Channel_Binding and inform the user to retry, which will start a new Session.
### Requirement 8: Configuration
**User Story:** As an operator, I want to configure the Gateway's behavior through environment variables, so that I can customize it for different deployment environments.
#### Acceptance Criteria
1. THE Gateway SHALL read the following configuration from environment variables: DISCORD_BOT_TOKEN, ANTHROPIC_API_KEY, ALLOWED_TOOLS (comma-separated list), PERMISSION_MODE, QUERY_TIMEOUT_MS, and CONFIG_DIR (path to the markdown configuration directory).
2. THE Gateway SHALL apply default values for optional configuration: ALLOWED_TOOLS defaults to "Read,Glob,Grep,WebSearch,WebFetch", PERMISSION_MODE defaults to "bypassPermissions", QUERY_TIMEOUT_MS defaults to "120000", and CONFIG_DIR defaults to "./config".
3. IF a required configuration value (DISCORD_BOT_TOKEN, ANTHROPIC_API_KEY) is missing, THEN THE Gateway SHALL terminate with a descriptive error message listing the missing values.
### Requirement 9: Concurrency and Rate Limiting
**User Story:** As an operator, I want the Gateway to handle multiple simultaneous requests safely, so that it remains stable under load.
#### Acceptance Criteria
1. THE Gateway SHALL process prompts from different Discord channels concurrently without blocking.
2. WHILE a prompt is being processed for a given channel, THE Gateway SHALL queue subsequent prompts from the same channel and process them sequentially to maintain conversation coherence.
3. IF the number of concurrent active queries exceeds a configurable maximum (default: 5), THEN THE Gateway SHALL respond to new prompts with a message indicating the system is busy and to retry later.
### Requirement 10: Graceful Shutdown
**User Story:** As an operator, I want the Gateway to shut down cleanly, so that in-flight requests complete and resources are released.
#### Acceptance Criteria
1. WHEN the Gateway receives a SIGTERM or SIGINT signal, THE Gateway SHALL stop accepting new prompts from Discord.
2. WHEN the Gateway receives a shutdown signal, THE Gateway SHALL wait for all in-flight Agent SDK queries to complete or timeout before terminating.
3. WHEN all in-flight queries have resolved, THE Gateway SHALL disconnect the Discord_Bot and terminate with exit code 0.
### Requirement 11: Event Queue
**User Story:** As an operator, I want all inputs to flow through a unified event queue, so that the system processes events in a consistent, ordered manner regardless of their source.
#### Acceptance Criteria
1. THE Event_Queue SHALL accept events of type: message, heartbeat, cron, hook, and webhook.
2. WHEN an Event is enqueued, THE Event_Queue SHALL assign a monotonically increasing sequence number and record the enqueue timestamp.
3. THE Event_Queue SHALL dispatch events to the Agent_Runtime in first-in-first-out order.
4. WHILE the Agent_Runtime is processing an Event, THE Event_Queue SHALL hold subsequent events until the current event completes processing.
5. IF the Event_Queue exceeds a configurable maximum depth (default: 100), THEN THE Event_Queue SHALL reject new events and log a warning with the current queue depth.
### Requirement 12: Agent Runtime
**User Story:** As an operator, I want a core agent runtime that processes events from the queue, so that all inputs are handled by the AI agent with full context.
#### Acceptance Criteria
1. WHEN the Agent_Runtime dequeues an Event, THE Agent_Runtime SHALL read all markdown configuration files (Soul_Config, Identity_Config, Agents_Config, User_Config, Memory_Store, Tools_Config) from the CONFIG_DIR.
2. WHEN the Agent_Runtime dequeues an Event, THE Agent_Runtime SHALL assemble the System_Prompt by concatenating the content of Soul_Config, Identity_Config, Agents_Config, User_Config, Memory_Store, and Tools_Config in that order, separated by section headers.
3. WHEN the Agent_Runtime processes a message Event, THE Agent_Runtime SHALL call the Agent SDK `query()` function with the assembled System_Prompt via the `systemPrompt` option and the message payload as the prompt.
4. WHEN the Agent_Runtime processes a heartbeat or cron Event, THE Agent_Runtime SHALL call the Agent SDK `query()` function with the assembled System_Prompt and the event's instruction text as the prompt.
5. WHEN the Agent_Runtime finishes processing an Event, THE Agent_Runtime SHALL signal the Event_Queue to dispatch the next event.
### Requirement 13: Markdown-Based Personality and Identity
**User Story:** As an operator, I want to define the agent's personality and identity in markdown files, so that I can customize the agent's behavior without code changes.
#### Acceptance Criteria
1. THE Agent_Runtime SHALL read Soul_Config from `{CONFIG_DIR}/soul.md` to obtain personality, tone, values, and behavior defaults.
2. THE Agent_Runtime SHALL read Identity_Config from `{CONFIG_DIR}/identity.md` to obtain the agent's name, role, and specialization.
3. THE Agent_Runtime SHALL read Agents_Config from `{CONFIG_DIR}/agents.md` to obtain operating rules, workflows, and safety/permission boundaries.
4. IF Soul_Config, Identity_Config, or Agents_Config is missing, THEN THE Agent_Runtime SHALL log a warning and use an empty string for the missing section in the System_Prompt.
5. WHEN any personality or identity markdown file is modified on disk, THE Agent_Runtime SHALL read the updated content on the next Event processing cycle without requiring a restart.
### Requirement 14: Markdown-Based User Context
**User Story:** As a user, I want the agent to know about my identity, preferences, and context, so that responses are personalized and relevant.
#### Acceptance Criteria
1. THE Agent_Runtime SHALL read User_Config from `{CONFIG_DIR}/user.md` to obtain user identity, preferences, and life/work context.
2. WHEN User_Config content is included in the System_Prompt, THE Agent_Runtime SHALL place it in a clearly labeled section so the agent can distinguish user context from other configuration.
3. IF User_Config is missing, THEN THE Agent_Runtime SHALL log a warning and omit the user context section from the System_Prompt.
4. WHEN User_Config is modified on disk, THE Agent_Runtime SHALL read the updated content on the next Event processing cycle without requiring a restart.
### Requirement 15: Markdown-Based Long-Term Memory
**User Story:** As a user, I want the agent to remember durable facts, lessons learned, and important information across sessions, so that I do not have to repeat context.
#### Acceptance Criteria
1. THE Agent_Runtime SHALL read Memory_Store from `{CONFIG_DIR}/memory.md` to obtain long-term durable facts, IDs, and lessons learned.
2. WHEN Memory_Store content is included in the System_Prompt, THE Agent_Runtime SHALL place it in a clearly labeled section titled "Long-Term Memory".
3. THE Agent_Runtime SHALL instruct the agent (via the System_Prompt) that it may append new facts to Memory_Store by writing to `{CONFIG_DIR}/memory.md` using the Write tool.
4. WHEN the agent writes to Memory_Store during event processing, THE Agent_Runtime SHALL read the updated Memory_Store content on the next Event processing cycle.
5. IF Memory_Store is missing, THEN THE Agent_Runtime SHALL create an empty `{CONFIG_DIR}/memory.md` file with a "# Memory" header.
### Requirement 16: Markdown-Based Tool Configuration
**User Story:** As an operator, I want to document API configurations, tool usage limits, and gotchas in a markdown file, so that the agent has reference material for using external tools correctly.
#### Acceptance Criteria
1. THE Agent_Runtime SHALL read Tools_Config from `{CONFIG_DIR}/tools.md` to obtain API documentation, tool configurations, usage limits, and known gotchas.
2. WHEN Tools_Config content is included in the System_Prompt, THE Agent_Runtime SHALL place it in a clearly labeled section titled "Tool Configuration".
3. IF Tools_Config is missing, THEN THE Agent_Runtime SHALL log a warning and omit the tool configuration section from the System_Prompt.
4. WHEN Tools_Config is modified on disk, THE Agent_Runtime SHALL read the updated content on the next Event processing cycle without requiring a restart.
### Requirement 17: Heartbeat System
**User Story:** As a user, I want the agent to proactively perform checks at regular intervals, so that it can monitor things like email, calendar, or other services without me asking.
#### Acceptance Criteria
1. THE Gateway SHALL read Heartbeat_Config from `{CONFIG_DIR}/heartbeat.md` to obtain a list of proactive check definitions, each with an instruction prompt and an interval in seconds.
2. WHEN the Gateway starts and Heartbeat_Config contains valid check definitions, THE Gateway SHALL start a recurring timer for each defined check at its configured interval.
3. WHEN a heartbeat timer fires, THE Gateway SHALL create a heartbeat Event with the check's instruction prompt as the payload and enqueue it in the Event_Queue.
4. WHEN the Agent_Runtime processes a heartbeat Event, THE Agent_Runtime SHALL use the heartbeat instruction as the prompt and deliver any response to the configured output channel.
5. IF Heartbeat_Config is missing, THEN THE Gateway SHALL log an informational message and operate without heartbeat events.
6. IF a heartbeat check definition has an interval less than 60 seconds, THEN THE Gateway SHALL reject the definition and log a warning indicating the minimum interval is 60 seconds.
### Requirement 18: Cron Job System
**User Story:** As an operator, I want to schedule events at exact times using cron expressions, so that the agent can perform tasks on a precise schedule.
#### Acceptance Criteria
1. THE Gateway SHALL read cron job definitions from `{CONFIG_DIR}/agents.md` under a "Cron Jobs" section, each with a cron expression and an instruction prompt.
2. WHEN the Gateway starts and valid cron job definitions exist, THE Gateway SHALL schedule each cron job according to its cron expression.
3. WHEN a cron job fires at its scheduled time, THE Gateway SHALL create a cron Event with the job's instruction prompt as the payload and enqueue it in the Event_Queue.
4. WHEN the Agent_Runtime processes a cron Event, THE Agent_Runtime SHALL use the cron instruction as the prompt and deliver any response to the configured output channel.
5. IF a cron expression is syntactically invalid, THEN THE Gateway SHALL log a warning identifying the invalid cron job and skip scheduling for that job.
### Requirement 19: Hook System
**User Story:** As an operator, I want internal state changes to trigger agent actions, so that the agent can respond to lifecycle events like startup, shutdown, or session changes.
#### Acceptance Criteria
1. THE Gateway SHALL support the following Hook types: `startup`, `agent_begin`, `agent_stop`, and `shutdown`.
2. WHEN the Gateway completes initialization (Discord_Bot ready and Agent_Runtime ready), THE Gateway SHALL fire a `startup` Hook event and enqueue it in the Event_Queue.
3. WHEN the Agent_Runtime begins processing any Event, THE Agent_Runtime SHALL fire an `agent_begin` Hook event (the agent_begin hook is processed inline, not re-enqueued).
4. WHEN the Agent_Runtime finishes processing any Event, THE Agent_Runtime SHALL fire an `agent_stop` Hook event (the agent_stop hook is processed inline, not re-enqueued).
5. WHEN the Gateway begins its shutdown sequence, THE Gateway SHALL fire a `shutdown` Hook event and enqueue it in the Event_Queue, waiting for it to be processed before completing shutdown.
6. THE Gateway SHALL read Hook instruction prompts from `{CONFIG_DIR}/agents.md` under a "Hooks" section, mapping each Hook type to an optional instruction prompt.
### Requirement 20: Bootstrap and Boot System
**User Story:** As an operator, I want the agent to perform an initial setup sequence on first run, so that all markdown configuration files are validated and the runtime is properly initialized.
#### Acceptance Criteria
1. THE Gateway SHALL read Boot_Config from `{CONFIG_DIR}/boot.md` to obtain bootstrap parameters including required markdown files, default content templates, and initialization instructions.
2. WHEN the Bootstrap_Process runs, THE Gateway SHALL verify that all markdown configuration files referenced in Boot_Config exist in CONFIG_DIR.
3. IF a required markdown configuration file is missing during bootstrap, THEN THE Gateway SHALL create the file with default content as specified in Boot_Config.
4. WHEN the Bootstrap_Process completes successfully, THE Gateway SHALL log the list of configuration files loaded and any files that were created with defaults.
5. IF Boot_Config is missing, THEN THE Gateway SHALL use built-in defaults: require only `soul.md` and `identity.md`, and create any missing optional files with empty section headers.
### Requirement 21: State Persistence
**User Story:** As a user, I want the agent's state to persist across restarts, so that memory, preferences, and context survive Gateway restarts.
#### Acceptance Criteria
1. THE Agent_Runtime SHALL persist all state exclusively in markdown files within CONFIG_DIR.
2. WHEN the agent modifies Memory_Store during event processing, THE Agent_Runtime SHALL ensure the changes are written to disk before signaling event completion.
3. WHEN the Gateway restarts, THE Agent_Runtime SHALL reconstruct its full context by reading all markdown configuration files from CONFIG_DIR, requiring no additional state recovery mechanism.
4. THE Gateway SHALL treat the CONFIG_DIR markdown files as the single source of truth for all agent state and configuration.
### Requirement 22: System Prompt Assembly
**User Story:** As an operator, I want the system prompt to be dynamically assembled from all markdown configuration files, so that the agent always operates with the latest context.
#### Acceptance Criteria
1. WHEN assembling the System_Prompt, THE Agent_Runtime SHALL read each markdown file and wrap its content in a labeled section using the format: `## [Section Name]\n\n[file content]`.
2. THE Agent_Runtime SHALL assemble sections in the following order: Identity_Config, Soul_Config, Agents_Config, User_Config, Memory_Store, Tools_Config.
3. THE Agent_Runtime SHALL omit sections for markdown files that are missing or empty, rather than including empty sections.
4. THE Agent_Runtime SHALL include a preamble in the System_Prompt instructing the agent that it may update `memory.md` to persist new long-term facts using the Write tool.
5. FOR ALL valid markdown configuration file sets, assembling the System_Prompt then parsing the section headers SHALL produce the same set of section names as the input files (round-trip property).

View File

@@ -0,0 +1,365 @@
# 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<string, string>` 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