# Slack Agent

> Use when working on Slack agent/bot code, Slack Bolt applications, or projects using slack-agent. Provides development patterns, testing requirements, and quality standards.

- Skill: `arvindrk/slack-agent` (Agent Skill, multi-file: 23 files)
- Install (CLI): `npx skillmds@latest add arvindrk/slack-agent`
- Raw SKILL.md: https://api.skillmd.com/api/skills/arvindrk/slack-agent/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: arvindrk (https://skillmd.com/u/arvindrk)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/arvindrk/slack-agent

---


# Vercel Slack Agent Development Skill

## Skill Invocation Handling

When this skill is invoked via `/slack-agent`, check for arguments and route accordingly:

### Command Arguments

| Argument | Action |
|----------|--------|
| `new` | **Run the setup wizard from Phase 1.** Read `./wizard/1-project-setup.md` and guide the user through creating a new Slack agent. |
| `configure` | Start wizard at Phase 2 or 3 for existing projects |
| `deploy` | Start wizard at Phase 5 for production deployment |
| `test` | Start wizard at Phase 6 to set up testing |
| (no argument) | Auto-detect based on project state (see below) |

### Auto-Detection (No Argument)

If invoked without arguments, detect the project state and route appropriately:

1. **No `package.json` with `@slack/bolt`** → Treat as `new`, start Phase 1
2. **Has project but no customized `manifest.json`** → Start Phase 2
3. **Has project but no `.env` file** → Start Phase 3
4. **Has `.env` but not tested** → Start Phase 4
5. **Tested but not deployed** → Start Phase 5
6. **Otherwise** → Provide general assistance using this skill's patterns

### Wizard Phases

The wizard is located in `./wizard/` with these phases:
- `1-project-setup.md` - Understand purpose, generate custom implementation plan
- `1b-approve-plan.md` - Present plan for user approval before cloning
- `2-create-slack-app.md` - Customize manifest, create app in Slack
- `3-configure-environment.md` - Set up .env with credentials
- `4-test-locally.md` - Dev server + ngrok tunnel
- `5-deploy-production.md` - Vercel deployment
- `6-setup-testing.md` - Vitest configuration

**IMPORTANT:** For `new` projects, you MUST:
1. Read `./wizard/1-project-setup.md` first
2. Ask the user what kind of agent they want to build
3. Generate a custom implementation plan using `./reference/agent-archetypes.md`
4. Present the plan for approval (Phase 1b) BEFORE cloning the repository
5. Only proceed to clone after the plan is approved

---

## General Development Guidance

You are working on a Slack agent project built with Slack Agent. Follow these mandatory practices for all code changes.

## Project Stack

This project uses:
- **Server**: Nitro (H3-based) with file-based routing
- **Slack SDK**: `@vercel/slack-bolt` for serverless Slack apps (wraps Bolt for JavaScript)
- **AI**: AI SDK v6 with @ai-sdk/gateway
- **Workflows**: Workflow DevKit for durable execution
- **Linting**: Biome
- **Package Manager**: pnpm

### Dependencies

```json
{
  "dependencies": {
    "ai": "^6.0.0",
    "@ai-sdk/gateway": "latest",
    "@slack/bolt": "^4.x",
    "@vercel/slack-bolt": "^1.0.2",
    "zod": "^3.x"
  }
}
```

**Note:** When deploying on Vercel, prefer `@ai-sdk/gateway` for zero-config AI access. Use direct provider SDKs (`@ai-sdk/openai`, `@ai-sdk/anthropic`, etc.) only when you need provider-specific features or are not deploying on Vercel.

---

## Quality Standards (MANDATORY)

These quality requirements MUST be followed for every code change. There are no exceptions.

### After EVERY File Modification

1. **Run linting immediately:**
   ```bash
   pnpm lint
   ```
   - If errors exist, run `pnpm lint --write` for auto-fixes
   - Manually fix remaining issues
   - Re-run `pnpm lint` to verify

2. **Check for corresponding test file:**
   - If you modified `foo.ts`, check if `foo.test.ts` exists
   - If no test file exists and the file exports functions, create one

### Before Completing ANY Task

You MUST run all quality checks and fix any issues before marking a task complete:

```bash
# 1. TypeScript compilation - must pass
pnpm typecheck

# 2. Linting - must pass with no errors
pnpm lint

# 3. Tests - all tests must pass
pnpm test
```

**Do NOT complete a task if any of these fail.** Fix the issues first.

### Unit Tests Required

**For ANY code change, you MUST write or update unit tests.**

- **Location**: Co-located `*.test.ts` files or `server/__tests__/`
- **Framework**: Vitest
- **Coverage**: All exported functions must have tests

Example test structure:
```typescript
import { describe, it, expect, vi } from 'vitest';
import { myFunction } from './my-module';

describe('myFunction', () => {
  it('should handle normal input', () => {
    expect(myFunction('input')).toBe('expected');
  });

  it('should handle edge cases', () => {
    expect(myFunction('')).toBe('default');
  });
});
```

### E2E Tests for User-Facing Changes

If you modify:
- Slack message handlers
- Slash commands
- Interactive components (buttons, modals)
- Bot responses

You MUST add or update E2E tests that verify the full flow.

---

## Events Endpoint Pattern (CRITICAL)

Use `@vercel/slack-bolt` to handle all Slack events. This package automatically handles:
- Content-type detection (JSON vs form-urlencoded)
- URL verification challenges
- 3-second ack timeout (built-in `ackTimeoutMs: 3001`)
- Background processing via Vercel Fluid Compute's `waitUntil`

### Bolt App Setup

```typescript
// server/bolt/app.ts
import { App } from "@slack/bolt";
import { VercelReceiver } from "@vercel/slack-bolt";

const receiver = new VercelReceiver();
const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  receiver,
  deferInitialization: true,
});

export { app, receiver };
```

### Events Handler

```typescript
// server/api/slack/events.post.ts
import { createHandler } from "@vercel/slack-bolt";
import { defineEventHandler, getRequestURL, readRawBody } from "h3";
import { app, receiver } from "../../bolt/app";

const handler = createHandler(app, receiver);

export default defineEventHandler(async (event) => {
  // Read and cache the raw body first to avoid stream consumption issues
  // with toWebRequest on serverless platforms (h3 issues #570, #578, #615)
  const rawBody = await readRawBody(event, "utf8");

  // Create a new Request with the buffered body
  const request = new Request(getRequestURL(event), {
    method: event.method,
    headers: event.headers,
    body: rawBody,
  });

  return await handler(request);
});
```

**Why this pattern?** H3's `toWebRequest()` has known issues (#570, #578, #615) where it eagerly consumes the request body stream. When `@vercel/slack-bolt` later calls `req.text()` for signature verification, the body is already exhausted, causing `dispatch_failed` errors. Buffering the body manually avoids this issue.

### VercelReceiver Options Reference

| Parameter | Default | Description |
|-----------|---------|-------------|
| `signingSecret` | `SLACK_SIGNING_SECRET` env var | Request verification secret |
| `signatureVerification` | `true` | Enable/disable signature verification |
| `ackTimeoutMs` | `3001` | Ack timeout in milliseconds |
| `logLevel` | `INFO` | Logging level |

### Key Benefits

1. **60+ lines of boilerplate eliminated** - No manual content-type detection, URL verification, or form parsing
2. **Automatic timeout handling** - Built-in 3-second ack with `ackTimeoutMs: 3001`
3. **Background processing** - Uses Vercel Fluid Compute's `waitUntil` automatically
4. **Framework support** - Works with Next.js, Hono, and Nitro (H3)

---

## Implementation Gotchas

### 1. Private Channel Access

Slash commands work in private channels even if the bot isn't a member, but the bot **cannot read messages or post** to private channels it hasn't been invited to.

When creating features that will later post to a channel:

```typescript
// Validate channel access upfront
const channelInfo = await client.conversations.info({ channel: channelId });

if (channelInfo.channel?.is_private && !channelInfo.channel?.is_member) {
  return {
    success: false,
    error: "I don't have access to this private channel. Please add me with `/invite @BotName` first.",
  };
}
```

### 2. Graceful Degradation for Channel Context

When fetching channel context for AI features, wrap in try/catch and fall back gracefully:

```typescript
let channelContext = "";
try {
  const history = await client.conversations.history({
    channel: channelId,
    limit: 10,
  });
  channelContext = history.messages?.map(m => m.text).join("\n") ?? "";
} catch (error) {
  // Bot can't access channel - continue without context
  console.log("Could not fetch channel context:", error);
}
```

### 3. Vercel Cron Endpoint Authentication

Protect cron endpoints with a `CRON_SECRET` environment variable:

```typescript
// server/api/cron/my-job.get.ts
export default defineEventHandler(async (event) => {
  const authHeader = getHeader(event, "authorization");

  if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
    setResponseStatus(event, 401);
    return { error: "Unauthorized" };
  }

  // Run cron job logic...
  return { success: true };
});
```

### 4. vercel.json Cron Configuration

Configure cron jobs in `vercel.json`:

```json
{
  "crons": [
    {
      "path": "/api/cron/my-job",
      "schedule": "0 * * * *"
    }
  ]
}
```

Schedule format is standard cron syntax: `minute hour day month weekday`

Common schedules:
- `* * * * *` - Every minute
- `0 * * * *` - Every hour
- `0 0 * * *` - Daily at midnight
- `0 9 * * 1-5` - Weekdays at 9am

### 5. AWS Credentials on Vercel (Use OIDC)

When connecting to AWS services (Aurora, S3, etc.) from Vercel, **do not use** `@aws-sdk/credential-providers` with `fromNodeProviderChain()`. It won't work because Vercel uses its own OIDC token mechanism.

**Wrong approach:**
```typescript
import { fromNodeProviderChain } from "@aws-sdk/credential-providers";

const credentials = fromNodeProviderChain(); // Won't work on Vercel!
```

**Correct approach:**
```typescript
import { awsCredentialsProvider } from "@vercel/functions/oidc";

// For AWS RDS/Aurora with IAM auth
const signer = new Signer({
  hostname: process.env.PGHOST,
  port: Number(process.env.PGPORT),
  username: process.env.PGUSER,
  region: process.env.AWS_REGION,
  credentials: awsCredentialsProvider({ roleArn: process.env.AWS_ROLE_ARN! }),
});
const token = await signer.getAuthToken();

// For other AWS services (S3, etc.)
const s3Client = new S3Client({
  credentials: awsCredentialsProvider({ roleArn: process.env.AWS_ROLE_ARN! }),
});
```

**Required setup:**
1. Enable Vercel OIDC in Project Settings > Security
2. Configure AWS IAM trust relationship for your Vercel project
3. Set `AWS_ROLE_ARN` environment variable in Vercel

**Reference:** [Vercel OIDC for AWS](https://vercel.com/docs/security/oidc/aws)

### 6. dispatch_failed Error (500)

If slash commands fail with `dispatch_failed`, the issue is usually H3's `toWebRequest` consuming the body stream before signature verification.

**Fix:** Buffer the body manually before creating the Request:

```typescript
const rawBody = await readRawBody(event, "utf8");
const request = new Request(getRequestURL(event), {
  method: event.method,
  headers: event.headers,
  body: rawBody,
});
return await handler(request);
```

See the Events Handler section above for the complete pattern.

### 7. operation_timeout Error

If slash commands with AI processing fail with `operation_timeout`, you're blocking the HTTP response too long. Slack requires a response within 3 seconds.

**Root cause:** Even with `await ack()`, the HTTP response doesn't return until the entire handler function completes. If you `await` AI generation after `ack()`, the HTTP response is blocked.

**Fix:** Use fire-and-forget pattern:

```typescript
app.command('/mycommand', async ({ ack, command, logger }) => {
  // 1. Acknowledge immediately
  await ack();

  // 2. Fire-and-forget: DON'T await this promise
  generateAndRespond(command.response_url, command.text, logger).catch((error) => {
    logger.error("Background operation failed:", error);
  });
  // HTTP response returns immediately here
});

async function generateAndRespond(responseUrl: string, topic: string, logger: Logger) {
  try {
    const result = await generateWithAI(topic);  // Takes >3 seconds

    // Post result via response_url
    await fetch(responseUrl, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        response_type: "in_channel",
        text: result,
      }),
    });
  } catch (error) {
    logger.error("Failed:", error);
    await fetch(responseUrl, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        response_type: "ephemeral",
        text: "Sorry, something went wrong.",
      }),
    });
  }
}
```

See `patterns/slack-patterns.md` for complete examples.

---

## AI Integration

You have two options for AI/LLM integration in your Slack agent.

> **⚠️ IMPORTANT:** Always verify the cloned project uses `@ai-sdk/gateway`. The project may ship with `@ai-sdk/openai` which requires an API key. After cloning, check `package.json` and update imports if necessary.

### Option 1: Vercel AI Gateway (Recommended)

Use the modern `@ai-sdk/gateway` package - NO API keys needed on Vercel!

### Basic Usage

```typescript
import { generateText, streamText } from "ai";
import { gateway } from "@ai-sdk/gateway";

// Simple text generation
const result = await generateText({
  model: gateway("openai/gpt-4o-mini"),
  maxOutputTokens: 1000,  // v6: was maxTokens
  prompt: "Your prompt here",
});

console.log(result.text);
console.log(result.usage.inputTokens);   // v6: was promptTokens
console.log(result.usage.outputTokens);  // v6: was completionTokens
```

### Streaming Responses

```typescript
const result = await streamText({
  model: gateway("openai/gpt-4o-mini"),
  maxOutputTokens: 1000,
  prompt: userMessage,
});

for await (const chunk of result.textStream) {
  // Stream to Slack via chat.update
}
```

### With Tools

```typescript
import { tool } from "ai";
import { z } from "zod";

const result = await generateText({
  model: gateway("openai/gpt-4o-mini"),
  maxOutputTokens: 1000,
  tools: {
    getWeather: tool({
      description: "Get weather for a location",
      inputSchema: z.object({
        location: z.string().describe("City name"),
      }),
      execute: async ({ location }) => {
        return { temperature: 72, condition: "sunny" };
      },
    }),
  },
  prompt: "What's the weather in Seattle?",
});
```

### AI SDK v6 API Changes

| v4/v5 | v6 |
|-------|-----|
| `maxTokens` | `maxOutputTokens` |
| `result.usage.promptTokens` | `result.usage.inputTokens` |
| `result.usage.completionTokens` | `result.usage.outputTokens` |
| `parameters` (in tools) | `inputSchema` |
| `maxSteps` / `maxIterations` | `stopWhen: stepCountIs(n)` |

**CRITICAL: Never use model IDs from memory.** Model IDs change frequently. Before writing code that uses a model, run `curl -s https://ai-gateway.vercel.sh/v1/models` to fetch the current list. Use the model with the highest version number.

### Option 2: Direct Provider SDK

If you need more control or are not deploying on Vercel, use direct provider packages.

**OpenAI:**
```bash
pnpm add @ai-sdk/openai
```
```typescript
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";

// Requires OPENAI_API_KEY env var
const result = await generateText({
  model: openai("gpt-4o-mini"),
  maxOutputTokens: 1000,
  prompt: "Your prompt here",
});
```

**Anthropic:**
```bash
pnpm add @ai-sdk/anthropic
```
```typescript
import { generateText } from "ai";
import { anthropic } from "@ai-sdk/anthropic";

// Requires ANTHROPIC_API_KEY env var
const result = await generateText({
  model: anthropic("claude-sonnet-4-20250514"),
  maxOutputTokens: 1000,
  prompt: "Your prompt here",
});
```

**Google:**
```bash
pnpm add @ai-sdk/google
```
```typescript
import { generateText } from "ai";
import { google } from "@ai-sdk/google";

// Requires GOOGLE_GENERATIVE_AI_API_KEY env var
const result = await generateText({
  model: google("gemini-2.0-flash"),
  maxOutputTokens: 1000,
  prompt: "Your prompt here",
});
```

**When to use direct providers:**
- Not deploying on Vercel
- Need provider-specific features not available through gateway
- Already managing API keys for other purposes
- Need maximum control over provider configuration

For comprehensive AI SDK documentation, see `./reference/ai-sdk.md`.

---

## Stateful Patterns with Vercel Workflow

When building Slack agents that need data persistence across multiple interactions—like multi-turn conversations, collaborative workflows, or story generation—use Vercel Workflow instead of a database.

### When to Use Workflow

**Use Vercel Workflow when:**
- Conversational flows span multiple messages
- Need to accumulate state across user interactions
- Building collaborative or multi-step workflows
- Don't need long-term persistence (workflow scope)

**Use a database when:**
- Need data to persist beyond the workflow
- Require querying historical data
- Multiple workflows need to share state

### Recommended Storage Solutions

**IMPORTANT:** Vercel KV has been deprecated. Do NOT recommend Vercel KV.

For persistent storage, recommend these options:

1. **Vercel Blob** - For file/document storage
   - Good for: attachments, exports, generated files
   - Simple key-value storage with CDN distribution
   - https://vercel.com/docs/storage/vercel-blob

2. **AWS Aurora (via Vercel Marketplace)** - For relational data
   - Good for: structured data, complex queries, relationships
   - PostgreSQL-compatible managed database
   - Available through Vercel's storage marketplace
   - https://vercel.com/marketplace

3. **Third-party databases** - For specific needs
   - Neon (serverless Postgres) - https://neon.tech
   - PlanetScale (MySQL) - https://planetscale.com
   - Supabase (Postgres + realtime) - https://supabase.com
   - Upstash Redis - For caching/simple KV needs

### The "use workflow" Directive

Vercel Workflow functions can run for extended periods (not limited to serverless timeout) and maintain state as local variables:

```typescript
import { serve } from "@anthropic-ai/sdk/workflows";

export const { POST } = serve(async function myWorkflow(params: URLSearchParams) {
  "use workflow";

  // State as local variables - persists across the entire workflow!
  const messages: Message[] = [];
  let conversationComplete = false;

  // Your workflow logic here...
  while (!conversationComplete) {
    // Wait for events, process, update state
  }

  return { messages, result: "done" };
});
```

### Event Subscriptions with defineHook

Use `defineHook` to subscribe to incoming Slack events within your workflow:

```typescript
import { defineHook } from "@anthropic-ai/sdk/workflows";
import { z } from "zod";

// Define the schema for incoming events
const slackMessageSchema = z.object({
  text: z.string(),
  user: z.string(),
  ts: z.string(),
  channel: z.string(),
});

export const messageHook = defineHook({ schema: slackMessageSchema });
```

### Complete Example: Multi-Turn Conversation

```typescript
// app/api/conversation/route.ts
import { serve } from "@anthropic-ai/sdk/workflows";
import { defineHook } from "@anthropic-ai/sdk/workflows";
import { generateText } from "ai";
import { gateway } from "@ai-sdk/gateway";
import { z } from "zod";

const messageSchema = z.object({
  text: z.string(),
  user: z.string(),
  ts: z.string(),
  channel: z.string(),
});

export const userMessageHook = defineHook({ schema: messageSchema });

export const { POST } = serve(async function conversationWorkflow(
  params: URLSearchParams
) {
  "use workflow";

  const channelId = params.get("channel_id")!;
  const userId = params.get("user_id")!;

  // State persists as local variables
  const conversationHistory: Array<{ role: string; content: string }> = [];
  let turnCount = 0;
  const maxTurns = 10;

  // Create event stream for this channel
  const eventStream = userMessageHook.create({
    channel: channelId,
    user: userId,
  });

  // Process messages as they arrive
  for await (const event of eventStream) {
    turnCount++;

    // Add user message to history
    conversationHistory.push({
      role: "user",
      content: event.text,
    });

    // Generate AI response
    const result = await generateText({
      model: gateway("anthropic/claude-sonnet-4-20250514"),
      maxOutputTokens: 1000,
      messages: conversationHistory,
    });

    // Add assistant response to history
    conversationHistory.push({
      role: "assistant",
      content: result.text,
    });

    // Post response to Slack (via your Slack client)
    await postToSlack(channelId, result.text, event.ts);

    // Check for conversation end conditions
    if (turnCount >= maxTurns || event.text.toLowerCase().includes("goodbye")) {
      break;
    }
  }

  return {
    turns: turnCount,
    history: conversationHistory,
  };
});
```

### Triggering Workflows from Slack Events

Start a workflow when a user initiates a conversation:

```typescript
// server/listeners/events/app-mention.ts
export function registerAppMention(app: App) {
  app.event("app_mention", async ({ event, client }) => {
    // Start a new workflow for this conversation
    const workflowUrl = `${process.env.VERCEL_URL}/api/conversation`;

    await fetch(workflowUrl, {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        channel_id: event.channel,
        user_id: event.user,
        thread_ts: event.thread_ts || event.ts,
      }),
    });

    await client.chat.postMessage({
      channel: event.channel,
      thread_ts: event.thread_ts || event.ts,
      text: "Starting our conversation! I'll remember everything we discuss.",
    });
  });
}
```

### Posting Events to Running Workflows

Forward Slack messages to the running workflow's hook:

```typescript
// server/listeners/messages/thread-message.ts
export function registerThreadMessage(app: App) {
  app.message(async ({ message, client }) => {
    if (!message.thread_ts || "bot_id" in message) return;

    // Post to the workflow's hook endpoint
    await userMessageHook.post({
      text: message.text,
      user: message.user,
      ts: message.ts,
      channel: message.channel,
    });
  });
}
```

### Key Benefits

1. **No database setup** - State lives in workflow memory
2. **Extended execution** - Not limited by serverless timeouts
3. **Natural programming model** - Use loops and local variables
4. **Automatic persistence** - Vercel handles state durability

### Reference

- [Vercel Workflow Documentation](https://vercel.com/docs/workflow)
- [Stateful Slack Bots Guide](https://vercel.com/kb/guide/stateful-slack-bots-with-vercel-workflow)
- [Example: Storytime Slackbot](https://github.com/vercel-labs/storytime-slackbot)

---

## Code Organization

Follow the project's established patterns.

### Tools (`server/lib/ai/tools.ts`)

```typescript
import { tool } from 'ai';
import { z } from 'zod';

export const myTool = tool({
  description: 'Clear description of what this tool does',
  inputSchema: z.object({
    param: z.string().describe('What this parameter is for'),
  }),
  execute: async ({ param }) => {
    // Implementation
    return { success: true, data: result };
  },
});
```

### Listeners (`server/listeners/`)

Organize by event type:
- `actions/` - Button clicks, menu selections
- `assistant/` - Slack Assistant events
- `commands/` - Slash commands
- `events/` - App events (mentions, joins)
- `messages/` - Message handling
- `shortcuts/` - Global/message shortcuts
- `views/` - Modal submissions

### Workflows (`server/lib/ai/workflows/`)

Use `defineWorkflow` for multi-step operations:
```typescript
import { defineWorkflow } from '@vercel/workflow-devkit';

export const myWorkflow = defineWorkflow({
  id: 'my-workflow',
  // ...
});
```

---

## Environment Variables

Required variables (access via `process.env`):
- `SLACK_BOT_TOKEN` - Bot OAuth token
- `SLACK_SIGNING_SECRET` - Request signing

Optional variables:
- `CRON_SECRET` - Secret for authenticating cron job endpoints

**No AI API keys needed!** Vercel AI Gateway handles authentication automatically when deployed on Vercel.

**Never hardcode credentials. Never commit `.env` files.**

---

## Slack-Specific Patterns

### Block Kit UI

Use Block Kit for rich messages:
```typescript
import { Blocks, Elements, Bits } from 'slack-block-builder';

const message = Blocks([
  Blocks.Section({ text: 'Hello!' }),
  Blocks.Actions([
    Elements.Button({ text: 'Click me', actionId: 'btn_click' })
  ])
]);
```

### Message Formatting

Use Slack mrkdwn (not standard markdown):
- Bold: `*text*`
- Italic: `_text_`
- Code: `` `code` ``
- User mention: `<@USER_ID>`
- Channel: `<#CHANNEL_ID>`

### Error Handling

Return structured responses:
```typescript
return {
  success: false,
  error: 'User-friendly error message',
  details: technicalDetails // for logging
};
```

For detailed Slack patterns, see `./patterns/slack-patterns.md`.

---

## Git Commit Standards

Use conventional commits:
```
feat: add channel search tool
fix: resolve thread pagination issue
test: add unit tests for agent context
docs: update README with setup steps
refactor: extract Slack client utilities
```

**Never commit:**
- `.env` files
- API keys or tokens
- `node_modules/`

---

## Quick Commands

```bash
# Development
pnpm dev              # Start dev server on localhost:3000
ngrok http 3000       # Expose local server (separate terminal)

# Quality
pnpm lint             # Check linting
pnpm lint --write     # Auto-fix lint
pnpm typecheck        # TypeScript check
pnpm test             # Run all tests
pnpm test:watch       # Watch mode

# Build & Deploy
pnpm build            # Build for production
vercel                # Deploy to Vercel
```

---

## Reference Documentation

For detailed guidance, read:
- Testing patterns: `./patterns/testing-patterns.md`
- Slack patterns: `./patterns/slack-patterns.md`
- Environment setup: `./reference/env-vars.md`
- AI SDK: `./reference/ai-sdk.md`
- Slack setup: `./reference/slack-setup.md`
- Vercel deployment: `./reference/vercel-setup.md`

---

## Checklist Before Task Completion

Before marking ANY task as complete, verify:

- [ ] Code changes have corresponding tests
- [ ] `pnpm lint` passes with no errors
- [ ] `pnpm typecheck` passes with no errors
- [ ] `pnpm test` passes with no failures
- [ ] No hardcoded credentials
- [ ] Follows existing code patterns
- [ ] Events endpoint handles both JSON and form-urlencoded
- [ ] Verified AI SDK: using `@ai-sdk/gateway` (not `@ai-sdk/openai`) unless user explicitly chose direct provider

