diff --git a/.agents/skills/chat-sdk/SKILL.md b/.agents/skills/chat-sdk/SKILL.md new file mode 100644 index 00000000..f46233c9 --- /dev/null +++ b/.agents/skills/chat-sdk/SKILL.md @@ -0,0 +1,204 @@ +--- +name: chat-sdk +description: Build multi-platform chat bots with Chat SDK (`chat` npm package). Use when developers want to build a Slack, Teams, Google Chat, Discord, Telegram, GitHub, Linear, or WhatsApp bot, handle mentions, direct messages, subscribed threads, reactions, slash commands, cards, modals, files, or AI streaming, set up webhook routes or multi-adapter bots, send rich cards or streamed AI responses to chat platforms, or build a custom adapter or state adapter. +--- + +# Chat SDK + +Unified TypeScript SDK for building chat bots across Slack, Teams, Google Chat, Discord, Telegram, GitHub, Linear, and WhatsApp. Write bot logic once, deploy everywhere. + +## Start with published sources + +When Chat SDK is installed in a user project, inspect the published files that ship in `node_modules`: + +``` +node_modules/chat/docs/ # bundled docs +node_modules/chat/dist/index.d.ts # core API types +node_modules/chat/dist/adapters/index.d.ts # static adapter catalog types +node_modules/chat/dist/jsx-runtime.d.ts # JSX runtime types +node_modules/chat/docs/contributing/ # adapter-authoring docs +node_modules/chat/resources/guides/ # framework/platform guides (markdown) +node_modules/chat/resources/templates.json # starter templates (title, description, href) +``` + +If one of the paths below does not exist, that package is not installed in the project yet. + +Read these before writing code: +- `node_modules/chat/docs/getting-started.mdx` — install and setup +- `node_modules/chat/docs/usage.mdx` — `Chat` config and lifecycle +- `node_modules/chat/docs/handling-events.mdx` — event routing and handlers +- `node_modules/chat/docs/threads-messages-channels.mdx` — thread/channel/message model +- `node_modules/chat/docs/posting-messages.mdx` — post, edit, delete, schedule +- `node_modules/chat/docs/streaming.mdx` — AI SDK integration and streaming semantics +- `node_modules/chat/docs/cards.mdx` — JSX cards +- `node_modules/chat/docs/actions.mdx` — button/select interactions +- `node_modules/chat/docs/modals.mdx` — modal submit/close flows +- `node_modules/chat/docs/slash-commands.mdx` — slash command routing +- `node_modules/chat/docs/direct-messages.mdx` — DM behavior and `openDM()` +- `node_modules/chat/docs/files.mdx` — attachments/uploads +- `node_modules/chat/docs/state-adapters.mdx` — persistence, locking, dedupe +- `node_modules/chat/docs/adapters.mdx` — cross-platform feature matrix +- `node_modules/chat/docs/api/chat.mdx` — exact `Chat` API +- `node_modules/chat/docs/api/thread.mdx` — exact `Thread` API +- `node_modules/chat/docs/api/message.mdx` — exact `Message` API +- `node_modules/chat/docs/api/modals.mdx` — modal element and event details + +For the specific adapter or state package you are using, inspect that installed package's `dist/index.d.ts` export surface in `node_modules`. + +## Adapter catalog subpath + +Chat SDK exposes a zero-dependency static catalog at `chat/adapters`. Agents can import `ADAPTERS`, `ADAPTER_NAMES`, `getAdapter`, `isAdapterSlug`, `listEnvVars`, `getSecretEnvVars`, and metadata types like `CatalogAdapter` and `AdapterSlug` from this subpath without importing any adapter implementation package. + +Use it for: +- Listing official and vendor-official adapter slugs, names, npm packages, groups, and platform vs state types. +- Building setup or onboarding flows that need package names, peer dependencies, and install guidance before any adapter is installed. +- Discovering required, optional, and credential-mode environment variables for an adapter, including which variables are secrets. +- Keeping vendor-official adapter docs and metadata aligned with the catalog when adding or updating a listed adapter. + +## Available resources + + + +### Guides + +- `node_modules/chat/resources/guides/how-to-build-an-ai-agent-for-slack-with-chat-sdk-and-ai-sdk.md` — Build a Slack AI agent using Chat SDK, AI SDK's ToolLoopAgent, and Vercel AI Gateway. Covers project setup, tool definitions, streaming responses, deployment to Vercel, and scaling tool selection with toolpick. +- `node_modules/chat/resources/guides/human-in-the-loop-with-chat-sdk-and-workflow-sdk.md` — Pause durable workflows on Slack approval cards using Chat SDK and Workflow SDK. Uses createWebhook to suspend workflows until a button click, with patterns for multi-stage approvals, timeouts via durable sleep, and approver validation. +- `node_modules/chat/resources/guides/liveblocks-chat-sdk-ai-sdk.md` — Build an AI agent that replies to @-mentions in Liveblocks comment threads with streamed responses and tool calling. Uses Chat SDK, the Liveblocks adapter, AI SDK's ToolLoopAgent, and Redis for thread subscriptions and distributed locking. +- `node_modules/chat/resources/guides/slack-bot-vercel-blob.md` — Build a Slack bot that lists, reads, uploads, and deletes files in Vercel Blob through tool calls. Uses Chat SDK, AI SDK's ToolLoopAgent, and Files SDK's createFileTools factory with approval-gated write tools and a read-only mode. +- `node_modules/chat/resources/guides/run-and-track-deploys-from-slack.md` — Build a Slack deploy bot with Chat SDK and Vercel Workflow. Dispatch GitHub Actions from a slash command, gate production behind approval, poll for completion, and notify Linear and GitHub when the run finishes. +- `node_modules/chat/resources/guides/triage-form-submissions-with-chat-sdk.md` — Build a Slack bot that triages form submissions with interactive cards. Forward, edit, or mark as spam without leaving Slack. Built with Chat SDK, Hono, and Resend. +- `node_modules/chat/resources/guides/how-to-build-a-slack-bot-with-next-js-and-redis.md` — This guide walks through building a Slack bot with Next.js, covering project setup, Slack app configuration, event handling, interactive features, and deployment. +- `node_modules/chat/resources/guides/create-a-discord-support-bot-with-nuxt-and-redis.md` — This guide walks through building a Discord support bot with Nuxt, covering project setup, Discord app configuration, Gateway forwarding, AI-powered responses, and deployment. +- `node_modules/chat/resources/guides/ship-a-github-code-review-bot-with-hono-and-redis.md` — This guide walks through building a GitHub bot that reviews pull requests on demand. When a user @mentions the bot on a PR, Chat SDK picks up the mention, spins up a Vercel Sandbox with the repo cloned, and uses AI SDK to analyze the diff. + +### Templates + +Listed in `node_modules/chat/resources/templates.json`: + +- **Chat SDK Liveblocks Bot** — Build a bot that you can engage with inside Liveblocks. (https://vercel.com/templates/next.js/chat-sdk-liveblocks-bot) +- **Durable iMessage Agent** — Durable iMessage agent powered by the Sendblue adapter. (https://vercel.com/templates/nitro/durable-imessage-ai-agent) +- **Knowledge Agent** — Open source file-system and knowledge based agent template. Build AI agents that stay up to date with your knowledge base. (https://vercel.com/templates/nuxt/chat-sdk-knowledge-agent) +- **Community Agent** — Open source AI-powered Slack community management bot with a built-in Next.js admin panel. Uses Chat SDK, AI SDK, and Vercel Workflow. (https://vercel.com/templates/next.js/chat-sdk-community-agent) + + + +## Quick start + +```typescript +import { Chat } from "chat"; +import { createSlackAdapter } from "@chat-adapter/slack"; +import { createRedisState } from "@chat-adapter/state-redis"; + +const bot = new Chat({ + userName: "mybot", + adapters: { + slack: createSlackAdapter(), + }, + state: createRedisState(), + dedupeTtlMs: 600_000, +}); + +bot.onNewMention(async (thread) => { + await thread.subscribe(); + await thread.post("Hello! I'm listening to this thread."); +}); + +bot.onSubscribedMessage(async (thread, message) => { + await thread.post(`You said: ${message.text}`); +}); +``` + +## Core concepts + +- **Chat** — main entry point; coordinates adapters, routing, locks, and state +- **Adapters** — platform-specific integrations for Slack, Teams, Google Chat, Discord, Telegram, GitHub, Linear, and WhatsApp +- **State adapters** — persistence for subscriptions, locks, dedupe, and thread state +- **Thread** — conversation context with `post()`, `stream()`, `subscribe()`, `setState()`, `startTyping()` +- **Message** — normalized content with `text`, `formatted`, attachments, author info, and platform `raw` +- **Channel** — container for threads and top-level posts + +## Event handlers + +| Handler | Trigger | +|---------|---------| +| `onNewMention` | Bot @-mentioned in an unsubscribed thread | +| `onDirectMessage` | New DM in an unsubscribed DM thread | +| `onSubscribedMessage` | Any message in a subscribed thread | +| `onNewMessage(regex)` | Regex match in an unsubscribed thread | +| `onReaction(emojis?)` | Emoji added or removed | +| `onAction(actionIds?)` | Button clicks and select/radio interactions | +| `onModalSubmit(callbackId?)` | Modal form submitted | +| `onModalClose(callbackId?)` | Modal dismissed/cancelled | +| `onSlashCommand(commands?)` | Slash command invocation | +| `onAssistantThreadStarted` | Slack assistant thread opened | +| `onAssistantContextChanged` | Slack assistant context changed | +| `onAppHomeOpened` | Slack App Home opened | +| `onMemberJoinedChannel` | Slack member joined channel event | + +Read `node_modules/chat/docs/handling-events.mdx`, `node_modules/chat/docs/actions.mdx`, `node_modules/chat/docs/modals.mdx`, and `node_modules/chat/docs/slash-commands.mdx` before wiring handlers. `onDirectMessage` behavior is documented in `node_modules/chat/docs/direct-messages.mdx`. + +## Streaming + +Pass any `AsyncIterable` to `thread.post()`. For AI SDK, prefer `result.fullStream` over `result.textStream` when available so step boundaries are preserved. + +```typescript +import { ToolLoopAgent } from "ai"; + +const agent = new ToolLoopAgent({ model: "anthropic/claude-4.5-sonnet" }); + +bot.onNewMention(async (thread, message) => { + const result = await agent.stream({ prompt: message.text }); + await thread.post(result.fullStream); +}); +``` + +Key details: +- `streamingUpdateIntervalMs` controls post+edit fallback cadence +- `fallbackStreamingPlaceholderText` defaults to `"..."`; set `null` to disable +- Structured `StreamChunk` support is Slack-only; other adapters ignore non-text chunks + +## Cards and modals (JSX) + +Set `jsxImportSource: "chat"` in `tsconfig.json`. + +Card components: +- `Card`, `CardText`, `Section`, `Fields`, `Field`, `Button`, `CardLink`, `LinkButton`, `Actions`, `Select`, `SelectOption`, `RadioSelect`, `Table`, `Image`, `Divider` + +Modal components: +- `Modal`, `TextInput`, `Select`, `SelectOption`, `RadioSelect` + +`Button` and `Modal` accept a `callbackUrl` prop — when triggered, the SDK POSTs the action payload to that URL in addition to firing any `onAction` / `onModalSubmit` handler. Use this for webhook-based workflow flows. See `node_modules/chat/docs/actions.mdx` and `node_modules/chat/docs/modals.mdx`. + +```tsx +await thread.post( + + Your order has been received. + + + + + +); +``` + +## Adapter inventory + +See [chat-sdk.dev/adapters](https://chat-sdk.dev/adapters) for the current list of official, vendor-official, and community adapters, including package names and authors. For the exact factory function and config types of an installed adapter, inspect its `dist/index.d.ts` in `node_modules`. + +## Building a custom adapter + +Read these published docs first: +- `node_modules/chat/docs/contributing/building.mdx` +- `node_modules/chat/docs/contributing/testing.mdx` +- `node_modules/chat/docs/contributing/publishing.mdx` + +Also inspect: +- `node_modules/chat/dist/index.d.ts` — `Adapter` and related interfaces +- `node_modules/@chat-adapter/shared/dist/index.d.ts` — shared errors and utilities +- Installed official adapter `dist/index.d.ts` files — reference implementations for config and APIs + +A custom adapter needs request verification, webhook parsing, message/thread/channel operations, ID encoding/decoding, and a format converter. Use `BaseFormatConverter` from `chat` and shared utilities from `@chat-adapter/shared`. + +## Webhook setup + +Each registered adapter exposes `bot.webhooks.`. Wire those directly to your HTTP framework routes. See `node_modules/chat/resources/guides/how-to-build-a-slack-bot-with-next-js-and-redis.md` and `node_modules/chat/resources/guides/create-a-discord-support-bot-with-nuxt-and-redis.md` for framework-specific route patterns. diff --git a/.agents/skills/coding-best-practices/SKILL.md b/.agents/skills/coding-best-practices/SKILL.md new file mode 100644 index 00000000..d0c905b3 --- /dev/null +++ b/.agents/skills/coding-best-practices/SKILL.md @@ -0,0 +1,106 @@ +--- +name: coding-best-practices +description: > + Code quality rules for the Gorkie Slack bot. Use when reviewing files for + violations, auditing PRs, or deciding how to structure new code. Covers + TypeScript patterns, Slack modal conventions, and architecture rules specific + to this repo. +--- + +## How to use this skill + +Read the rules below, then read the target files and report every violation with: +- File path and line numbers +- Which rule is violated +- Concrete suggested fix + +Only report real issues. If a file is clean, say so in one line. + +--- + +## Rules + +### 1. Minimal interfaces +Accept only what the callee uses. When an SDK type carries more than needed, export a smaller internal type and convert at the boundary. + +```ts +// bad — callee must fabricate fake SDK objects +toolsModal({ tools: ListToolsResult['tools'] }) + +// good +export type ToolEntry = { name: string; group: GroupSlug }; +toolsModal({ tools: ToolEntry[] }) +``` + +### 2. No fabricated data +Never construct fake objects (empty descriptions, synthetic annotations, placeholder schemas) to satisfy a type. The type is wrong — narrow it. + +### 3. Export shared type discriminants +When multiple files check the same string literals (`'ro' | 'dt' | 'gn'`, `'allow' | 'ask' | 'block'`), export a named union from one canonical location. Never re-validate the same literals with inline comparisons across files. + +### 4. No large inline closures +Async closures longer than ~20 lines inside object literals belong in named functions at module scope with explicit parameter types. + +```ts +// bad +tools[name] = { execute: async (input, opts) => { /* 100 lines */ } } + +// good +tools[name] = { execute: wrapMCPToolExecute({ ctxId, server, stream, ... }) } +``` + +### 5. Nested metadata over flat parallel fields +Fields that always move together should be nested. + +```ts +// bad +{ serverId: server.id, serverName: server.name, toolName } + +// good +{ server: { id: server.id, name: server.name }, tool: { name: toolName } } +``` + +### 6. Slack private_metadata: minimal and Zod-parsed +Only persist what cannot be re-derived from view state or a DB lookup. Always parse with Zod — never cast with `as`. + +```ts +// bad +const meta = JSON.parse(view.private_metadata || '{}') as SomeMeta; + +// good +const meta = someMetaSchema.parse(JSON.parse(view.private_metadata || '{}')); +``` + +### 7. Don't scrape view state to reconstruct stored data +If a handler reads `body.view.state.values` for every element just to rebuild a structure for re-rendering, that structure should have been in `private_metadata`. Only scrape view state for values the user actively changed in this action. + +### 8. Always pass `hash` on mid-session `views.update` +`block_actions` handlers must pass `hash: view.hash` to `client.views.update`. This prevents overwriting a view that a concurrent action already updated. + +### 9. Dict params +Functions with more than one parameter take a single options object. + +```ts +// bad +logReply(ctxId, author, result, reason); +// good +logReply({ ctxId, author, result, reason }); +``` + +### 10. Inline over extract (single-use) +Only extract to a named function when called in multiple places or genuinely complex. A helper called once is worse than the inline code. + +### 11. No type casts +Prefer schema parsing or narrower signatures over `as`. A cast is acceptable only at a real validated external boundary. + +### 12. Config for tuneable values +Magic numbers and strings that could change per deployment belong in `apps/bot/src/config.ts`. + +--- + +## Also check + +- Missing `await` on promises (fire-and-forget is only acceptable for intentional background work) +- Empty or swallowed `catch` blocks +- Ownership checks before DB mutations (user should only affect their own resources) +- `views.update` without `hash` in action handlers (race condition) diff --git a/.agents/skills/grill-with-docs/ADR-FORMAT.md b/.agents/skills/grill-with-docs/ADR-FORMAT.md new file mode 100644 index 00000000..da7e78ec --- /dev/null +++ b/.agents/skills/grill-with-docs/ADR-FORMAT.md @@ -0,0 +1,47 @@ +# ADR Format + +ADRs live in `docs/adr/` and use sequential numbering: `0001-slug.md`, `0002-slug.md`, etc. + +Create the `docs/adr/` directory lazily — only when the first ADR is needed. + +## Template + +```md +# {Short title of the decision} + +{1-3 sentences: what's the context, what did we decide, and why.} +``` + +That's it. An ADR can be a single paragraph. The value is in recording *that* a decision was made and *why* — not in filling out sections. + +## Optional sections + +Only include these when they add genuine value. Most ADRs won't need them. + +- **Status** frontmatter (`proposed | accepted | deprecated | superseded by ADR-NNNN`) — useful when decisions are revisited +- **Considered Options** — only when the rejected alternatives are worth remembering +- **Consequences** — only when non-obvious downstream effects need to be called out + +## Numbering + +Scan `docs/adr/` for the highest existing number and increment by one. + +## When to offer an ADR + +All three of these must be true: + +1. **Hard to reverse** — the cost of changing your mind later is meaningful +2. **Surprising without context** — a future reader will look at the code and wonder "why on earth did they do it this way?" +3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons + +If a decision is easy to reverse, skip it — you'll just reverse it. If it's not surprising, nobody will wonder why. If there was no real alternative, there's nothing to record beyond "we did the obvious thing." + +### What qualifies + +- **Architectural shape.** "We're using a monorepo." "The write model is event-sourced, the read model is projected into Postgres." +- **Integration patterns between contexts.** "Ordering and Billing communicate via domain events, not synchronous HTTP." +- **Technology choices that carry lock-in.** Database, message bus, auth provider, deployment target. Not every library — just the ones that would take a quarter to swap out. +- **Boundary and scope decisions.** "Customer data is owned by the Customer context; other contexts reference it by ID only." The explicit no-s are as valuable as the yes-s. +- **Deliberate deviations from the obvious path.** "We're using manual SQL instead of an ORM because X." Anything where a reasonable reader would assume the opposite. These stop the next engineer from "fixing" something that was deliberate. +- **Constraints not visible in the code.** "We can't use AWS because of compliance requirements." "Response times must be under 200ms because of the partner API contract." +- **Rejected alternatives when the rejection is non-obvious.** If you considered GraphQL and picked REST for subtle reasons, record it — otherwise someone will suggest GraphQL again in six months. diff --git a/.agents/skills/grill-with-docs/CONTEXT-FORMAT.md b/.agents/skills/grill-with-docs/CONTEXT-FORMAT.md new file mode 100644 index 00000000..eaf2a185 --- /dev/null +++ b/.agents/skills/grill-with-docs/CONTEXT-FORMAT.md @@ -0,0 +1,60 @@ +# CONTEXT.md Format + +## Structure + +```md +# {Context Name} + +{One or two sentence description of what this context is and why it exists.} + +## Language + +**Order**: +{A one or two sentence description of the term} +_Avoid_: Purchase, transaction + +**Invoice**: +A request for payment sent to a customer after delivery. +_Avoid_: Bill, payment request + +**Customer**: +A person or organization that places orders. +_Avoid_: Client, buyer, account +``` + +## Rules + +- **Be opinionated.** When multiple words exist for the same concept, pick the best one and list the others under `_Avoid_`. +- **Keep definitions tight.** One or two sentences max. Define what it IS, not what it does. +- **Only include terms specific to this project's context.** General programming concepts (timeouts, error types, utility patterns) don't belong even if the project uses them extensively. Before adding a term, ask: is this a concept unique to this context, or a general programming concept? Only the former belongs. +- **Group terms under subheadings** when natural clusters emerge. If all terms belong to a single cohesive area, a flat list is fine. + +## Single vs multi-context repos + +**Single context (most repos):** One `CONTEXT.md` at the repo root. + +**Multiple contexts:** A `CONTEXT-MAP.md` at the repo root lists the contexts, where they live, and how they relate to each other: + +```md +# Context Map + +## Contexts + +- [Ordering](./src/ordering/CONTEXT.md) — receives and tracks customer orders +- [Billing](./src/billing/CONTEXT.md) — generates invoices and processes payments +- [Fulfillment](./src/fulfillment/CONTEXT.md) — manages warehouse picking and shipping + +## Relationships + +- **Ordering → Fulfillment**: Ordering emits `OrderPlaced` events; Fulfillment consumes them to start picking +- **Fulfillment → Billing**: Fulfillment emits `ShipmentDispatched` events; Billing consumes them to generate invoices +- **Ordering ↔ Billing**: Shared types for `CustomerId` and `Money` +``` + +The skill infers which structure applies: + +- If `CONTEXT-MAP.md` exists, read it to find contexts +- If only a root `CONTEXT.md` exists, single context +- If neither exists, create a root `CONTEXT.md` lazily when the first term is resolved + +When multiple contexts exist, infer which one the current topic relates to. If unclear, ask. diff --git a/.agents/skills/grill-with-docs/SKILL.md b/.agents/skills/grill-with-docs/SKILL.md new file mode 100644 index 00000000..5ea0aa91 --- /dev/null +++ b/.agents/skills/grill-with-docs/SKILL.md @@ -0,0 +1,88 @@ +--- +name: grill-with-docs +description: Grilling session that challenges your plan against the existing domain model, sharpens terminology, and updates documentation (CONTEXT.md, ADRs) inline as decisions crystallise. Use when user wants to stress-test a plan against their project's language and documented decisions. +--- + + + +Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer. + +Ask the questions one at a time, waiting for feedback on each question before continuing. + +If a question can be answered by exploring the codebase, explore the codebase instead. + + + + + +## Domain awareness + +During codebase exploration, also look for existing documentation: + +### File structure + +Most repos have a single context: + +``` +/ +├── CONTEXT.md +├── docs/ +│ └── adr/ +│ ├── 0001-event-sourced-orders.md +│ └── 0002-postgres-for-write-model.md +└── src/ +``` + +If a `CONTEXT-MAP.md` exists at the root, the repo has multiple contexts. The map points to where each one lives: + +``` +/ +├── CONTEXT-MAP.md +├── docs/ +│ └── adr/ ← system-wide decisions +├── src/ +│ ├── ordering/ +│ │ ├── CONTEXT.md +│ │ └── docs/adr/ ← context-specific decisions +│ └── billing/ +│ ├── CONTEXT.md +│ └── docs/adr/ +``` + +Create files lazily — only when you have something to write. If no `CONTEXT.md` exists, create one when the first term is resolved. If no `docs/adr/` exists, create it when the first ADR is needed. + +## During the session + +### Challenge against the glossary + +When the user uses a term that conflicts with the existing language in `CONTEXT.md`, call it out immediately. "Your glossary defines 'cancellation' as X, but you seem to mean Y — which is it?" + +### Sharpen fuzzy language + +When the user uses vague or overloaded terms, propose a precise canonical term. "You're saying 'account' — do you mean the Customer or the User? Those are different things." + +### Discuss concrete scenarios + +When domain relationships are being discussed, stress-test them with specific scenarios. Invent scenarios that probe edge cases and force the user to be precise about the boundaries between concepts. + +### Cross-reference with code + +When the user states how something works, check whether the code agrees. If you find a contradiction, surface it: "Your code cancels entire Orders, but you just said partial cancellation is possible — which is right?" + +### Update CONTEXT.md inline + +When a term is resolved, update `CONTEXT.md` right there. Don't batch these up — capture them as they happen. Use the format in [CONTEXT-FORMAT.md](./CONTEXT-FORMAT.md). + +`CONTEXT.md` should be totally devoid of implementation details. Do not treat `CONTEXT.md` as a spec, a scratch pad, or a repository for implementation decisions. It is a glossary and nothing else. + +### Offer ADRs sparingly + +Only offer to create an ADR when all three are true: + +1. **Hard to reverse** — the cost of changing your mind later is meaningful +2. **Surprising without context** — a future reader will wonder "why did they do it this way?" +3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons + +If any of the three is missing, skip the ADR. Use the format in [ADR-FORMAT.md](./ADR-FORMAT.md). + + diff --git a/.agents/skills/improve/SKILL.md b/.agents/skills/improve/SKILL.md new file mode 100644 index 00000000..150568b9 --- /dev/null +++ b/.agents/skills/improve/SKILL.md @@ -0,0 +1,118 @@ +--- +name: improve +description: Survey any codebase as a senior advisor and produce prioritized, self-contained implementation plans for OTHER models/agents to execute. Strictly read-only on source code — never implements, fixes, or refactors anything itself. Use when asked to audit a codebase, find improvement opportunities (bugs, security, performance, test coverage, tech debt, migrations, DX), suggest features or where to take the project next (roadmap, product direction), or generate handoff plans for another agent to implement. +license: MIT +metadata: + author: shadcn + version: "1.0.0" +--- + +# Improve + +You are a **senior advisor, not an implementer**. Your job is to deeply understand a codebase, find the highest-value improvement opportunities, and write implementation plans good enough that a *different, less capable model with zero context from this session* can execute, test, and maintain them. + +The economics of this skill: an expensive, high-ceiling model does the part where intelligence compounds (understanding, judging, specifying). Cheaper models do the execution. The plan is the product — its quality determines whether the executor succeeds. + +## Hard Rules + +1. **Never modify source code yourself.** No edits, no fixes, no "quick wins while you're in there." The ONLY files you may create or modify live under `plans/` in the repo root (create it if absent). The `execute` variant dispatches a *separate executor subagent* that edits code in an isolated git worktree — you review its diff and render a verdict; you still never edit code directly, and you never merge, push, or commit to the user's branch. +2. **Never run commands that mutate the user's working tree** — no installs, no builds that write artifacts outside standard ignored dirs, no git commits, no formatters. Read, search, and run read-only analysis only (e.g. `tsc --noEmit`, lint in check mode, `npm audit` / `pnpm audit`, test suite if cheap and side-effect free). Two scoped exceptions: verification commands inside an executor's disposable worktree during `execute` review, and `gh issue create` under an explicit `--issues` flag. +3. **Every plan must be fully self-contained.** The executor has not seen this conversation, this codebase survey, or any other plan. If a plan references "the pattern discussed above," it is broken. +4. **Never reproduce secret values.** If the audit finds credentials, tokens, or `.env` contents, findings and plans reference the `file:line` and credential type only, and recommend rotation. The value itself must never appear in anything you write. +5. **If the user asks you to implement directly, decline and point at the plan** — offer `execute ` (dispatched executor + your review) or plan refinement instead. + +## Workflow + +### Phase 1 — Recon (always) + +Map the territory before judging it: + +- Read `README`, `CLAUDE.md`/`AGENTS.md`, `CONTRIBUTING`, root config files (`package.json`, `pyproject.toml`, `go.mod`, etc.), CI config, and the directory structure. +- Identify: language(s), framework(s), package manager, **how to build / test / lint / typecheck** (exact commands — these go into every plan as verification gates), test coverage shape, deployment target. +- Note repo conventions: code style, naming, folder layout, error-handling and state-management patterns. Plans must tell the executor to *match* these, with examples. +- Check git signal where useful (`git log --oneline -30`, churn hotspots) for what's actively evolving vs. frozen. + +If the repo has no working verification command (no tests, broken build), record that — "establish a verification baseline" is often finding #1, and it must precede risky plans in the dependency order. + +### Phase 2 — Audit (parallel) + +Audit the codebase across the categories in [references/audit-playbook.md](references/audit-playbook.md) — read it now. Categories: **correctness/bugs, security, performance, test coverage, tech debt & architecture, dependencies & migrations, DX & tooling, docs, direction (features & what to build next)**. + +For repos of any real size, fan out with parallel read-only subagents (in Claude Code: **Explore** agents) — one per category (or cluster of related categories). If the host agent can't spawn subagents, audit directly yourself in category-priority order. **Subagents do not inherit this skill's context**, so each subagent prompt must include: + +- the **absolute path** to this skill's `references/audit-playbook.md` plus the exact section headings to read — **always including "## Finding format"** (subagents can read files — this is far cheaper than pasting; paste the sections only if the path may not resolve in the subagent's environment), +- the recon facts that scope the search (languages, frameworks, key directories, what to skip), +- domain-specific risk hints from recon (e.g. for a CLI that writes user files: "pay attention to path traversal and command injection"), +- an explicit instruction to return findings only — no fixes, no file dumps — and to confirm it could read the playbook file. + +Audit depth follows the **effort level** (default `standard`; the user sets it with a `quick` / `deep` keyword anywhere in the invocation): + +| | `quick` | `standard` (default) | `deep` | +|---|---|---|---| +| Coverage | Recon hotspots only — highest-churn, highest-criticality code | Hotspot-weighted, key packages | Whole repo, every package | +| Subagents | 0–1 (sweep directly when feasible) | ≤4 concurrent | ≤8 concurrent, one per category | +| Breadth | "medium" | "very thorough" for correctness + security, "medium" rest | "very thorough" everywhere | +| Categories | correctness, security, tests | all nine | all nine | +| Findings | top ~6, HIGH-confidence only | full table | full table incl. LOW-confidence "investigate" items | + +Whatever the level, say in the final report what was *not* audited. On a large monorepo even `deep` scopes subagents to packages, not the root. + +Every finding needs: evidence (`file:line` references), impact, effort estimate (S/M/L), risk of the fix itself, and confidence. No vibes-only findings. + +### Phase 3 — Vet, prioritize, confirm + +**Vet before presenting — subagents over-report.** For every finding that will make the table, open the cited code yourself and confirm it. Expect three failure classes: **by-design behavior** reported as a bug or vulnerability (e.g. honoring `https_proxy` flagged as SSRF — it's the standard proxy convention); **mis-attributed evidence** (real finding, wrong file or line); and duplicates across subagents. Downgrade, correct, or reject accordingly, and record rejections in the index's "considered and rejected" section so they aren't re-audited next run. + +Present the vetted findings table to the user, ordered by leverage (impact ÷ effort, weighted by confidence): + +| # | Finding | Category | Impact | Effort | Risk | Evidence | + +Present **direction findings separately**, after the table — they're options for the maintainer to weigh, not problems ranked against bugs, and burying "build a plugin system" under "fix the N+1" serves neither. 2–4 grounded suggestions max, each with its evidence and trade-offs in two or three sentences. + +Then ask which findings to turn into plans (default suggestion: the top 3–5 plus anything they flag). Also surface **dependency ordering** — e.g. "characterization tests for module X (plan 02) must land before the refactor of X (plan 05)." + +Wait for the selection. Do not write 30 plans nobody asked for. If running non-interactively (no user available to choose), write plans for the top 3–5 by leverage and record that default in `plans/README.md`. + +### Phase 4 — Write the plans + +For each selected finding, write one plan file using the template in [references/plan-template.md](references/plan-template.md) — read it before writing the first plan. Plans go in: + +``` +plans/ + README.md ← index: priority order, dependency graph, status table + 001-.md + 002-.md +``` + +**Excerpts come from your own reads, never from a subagent's report.** Before writing each plan, open every cited file yourself — subagent line numbers and attributions are leads, not facts, and a wrong excerpt becomes a wrong plan that fails its own drift check. + +Before writing anything: record `git rev-parse --short HEAD` — every plan stamps the commit it was written against (the executor uses it for drift detection). If `plans/` already exists from a previous run, **reconcile, don't duplicate**: read `plans/README.md`, keep numbering monotonic, skip findings already planned or listed as rejected, and mark superseded plans stale in the index. If `plans/` exists for some unrelated purpose, use `advisor-plans/` instead and say so. + +Write each plan **for the weakest plausible executor**. That means: + +- All context inlined: why this matters, exact file paths, current-state code excerpts, the repo's conventions to follow (with a snippet of an existing exemplar file). +- Steps that are explicit and ordered, each with its own verification command and expected output. +- Hard boundaries: files in scope, files explicitly out of scope, things that look related but must not be touched. +- Machine-checkable done criteria — commands and expected results, not prose like "works correctly." +- A test plan (what new tests to write, where, following which existing test as a pattern). +- A maintenance note (what future changes will interact with this, what to watch in review). +- Escape hatches: "if X turns out to be true, STOP and report back instead of improvising." + +Finish by writing `plans/README.md` with the recommended execution order, dependencies between plans, and a status column the executor models can update. + +## Invocation variants + +- Bare invocation → full workflow above. +- `quick` / `deep` (anywhere in the invocation) → effort level for the audit; see the table in Phase 2. Composes with everything: `quick security`, `deep --issues`. Default is `standard`. +- With a focus argument (e.g. `security`, `perf`, `tests`) → run Recon, then audit only that category, then plan. +- `branch` → audit only the current working branch's changes: scope = files changed since the merge-base with the default branch (`git diff --name-only $(git merge-base origin/ HEAD)..HEAD`) plus their direct importers/callers. Light recon, all categories, usually no subagents. **Tag every finding `introduced` (by this branch) or `pre-existing` (in touched files)** — the table separates them; don't blame the branch for legacy debt, but do surface what it's building on top of. If on the default branch or zero commits ahead, say so and offer a full audit instead. +- `next` (or `features`, `roadmap`) → run Recon, then audit only the direction category, in more depth: 4–6 grounded suggestions, each with evidence, trade-offs, and a coarse effort estimate. Selected ones become design/spike plans, not build-everything plans. +- `plan ` → skip the audit; the user already knows what they want. Run Recon, investigate just enough to specify it properly, and write a single plan. If the description is too ambiguous to specify honestly, first try to resolve each ambiguity from the codebase itself; only what's left becomes questions to the user — asked one at a time, each with a recommended answer. +- `review-plan ` → critique an existing plan in `plans/` against the template's standards and tighten it. If you authored the plan in this same session, also have a fresh-context subagent read it cold and report ambiguities — self-critique misses gaps you mentally fill from context the executor won't have. +- `execute ` → dispatch a cheaper executor subagent on one plan (isolated worktree), then review its diff like a tech lead — re-run done criteria, check scope, read the code — and render a verdict. Requires a host agent that can spawn subagents in an isolated worktree; if yours can't, say so and hand the plan over for manual execution instead. **Read [references/closing-the-loop.md](references/closing-the-loop.md) before the first dispatch.** +- `reconcile` → process what happened since last session: verify DONE plans, investigate BLOCKED ones, refresh drifted TODOs, retire dead findings. See [references/closing-the-loop.md](references/closing-the-loop.md). +- `--issues` (modifier on any planning invocation) → also publish each written plan as a GitHub issue via `gh`, URL recorded in the plan and index. Only with the explicit flag. See [references/closing-the-loop.md](references/closing-the-loop.md). + +## Tone of the output + +You are advising, not selling. State findings plainly with evidence, flag uncertainty honestly, and prefer "not worth doing" verdicts over padding the list. A short list of high-confidence, high-leverage plans beats a long one. diff --git a/.agents/skills/improve/references/audit-playbook.md b/.agents/skills/improve/references/audit-playbook.md new file mode 100644 index 00000000..e20005e0 --- /dev/null +++ b/.agents/skills/improve/references/audit-playbook.md @@ -0,0 +1,130 @@ +# Audit Playbook + +What to look for, per category. Each subagent (or direct audit pass) gets the relevant section plus the **Finding format** at the bottom. Adapt depth to repo size — a 2K-line CLI gets a lighter pass than a 500K-line monorepo. + +A finding is only a finding with evidence. "Probably has N+1 queries somewhere" is not a finding; `orders/api.ts:142 issues one query per order item inside a loop` is. + +--- + +## 1. Correctness / Bugs + +The highest-trust category — real bugs found by reading, not speculation. + +- Error handling: swallowed exceptions, empty catch blocks, `catch (e) { console.log(e) }` on critical paths, missing error states in UI code. +- Async hazards: unawaited promises, race conditions on shared state, missing cancellation/cleanup (stale closures in React effects, listeners never removed). +- Null/undefined flows: non-null assertions (`!`) on values that can be null, optional chaining hiding a value that must exist, unchecked array indexing. +- Boundary conditions: off-by-one, empty-collection handling, timezone/locale assumptions, integer overflow in counters/IDs. +- State machines: impossible-state combinations representable in types, status enums with unhandled branches (look for `default:` that silently no-ops). +- Concurrency: check-then-act on shared resources, missing transactions around multi-write operations, idempotency of retried operations (webhooks, queues). +- Type escape hatches: `any` / `as` casts / `@ts-ignore` clusters — each one is a place the compiler was overruled. +- Resource leaks: unclosed handles, connections, subscriptions; missing `finally`. + +## 2. Security + +Report only what's evidenced in the code. Do not generate exploit code in plans — describe the fix. + +**Handling rule:** never copy a secret value into a finding or plan — those files get committed. Reference the `file:line` and credential type only ("Stripe live key at `config.ts:12`"), and the fix sketch always includes rotation, not just removal (a committed secret is burned even after deletion). + +**By-design is not a finding:** standard platform conventions are intentional behavior — honoring `https_proxy`/`NO_PROXY`, reading `~/.netrc`, an explicitly local dev tool shelling out to configured package managers. Flag these only when the *implementation* adds risk beyond the convention itself. + +- Secrets: hardcoded keys/tokens/passwords, secrets in committed `.env` files, secrets logged or persisted in event/history stores. +- Injection: string-built SQL/shell commands, `dangerouslySetInnerHTML` / `innerHTML` with user data, `eval`/`Function` on dynamic input, path traversal on user-supplied filenames. +- AuthN/Z: endpoints/server actions missing auth checks, authorization checked client-side only, IDOR (object access by ID without ownership check), missing CSRF protection on state-changing routes. +- Input validation: API boundaries trusting request bodies (no schema validation), file-upload handling (type/size/path), mass assignment from request objects. +- Dependencies: run the ecosystem's audit command (`npm audit`, `pip-audit`, `cargo audit`) in read-only mode; flag critical/high with known exploits, not the noise floor. +- Headers/config: CORS wildcard with credentials, missing CSP where it matters, cookies without `HttpOnly`/`Secure`/`SameSite`, debug/verbose modes reachable in production config. +- Data exposure: PII in logs, stack traces returned to clients, internal error details in API responses. + +## 3. Performance + +Look for the algorithmic and architectural wins, not micro-optimizations. + +- N+1 patterns: query/fetch per item inside loops or per list-row rendering; missing batching or dataloader. +- Wrong complexity: nested scans over the same collection, repeated `find`/`filter` inside hot loops where a Map keyed lookup belongs. +- Caching gaps: identical expensive computations or fetches repeated per request/render; missing memoization at clear function boundaries; no HTTP/data-layer caching on stable data. +- Payload size: over-fetching (select *, full objects where IDs suffice), missing pagination on unbounded lists, large JSON shipped to clients. +- Frontend (if applicable): bundle composition (heavyweight deps for trivial use), missing code-splitting on rarely-hit routes, unoptimized images/fonts, client-side fetching for data available at render time, render waterfalls. For React/Next.js, defer to the repo's framework conventions and any installed best-practices guidelines. +- Backend: synchronous work that belongs in a queue, missing indexes implied by query patterns (flag for verification — don't claim without schema evidence), connection-per-request patterns where pooling exists. +- Build/CI: slow CI from missing caching, redundant pipeline steps, test suites that could parallelize. + +## 4. Test Coverage + +The goal is not a percentage — it's *which untested code is dangerous*. + +- Map the critical paths (money, auth, data mutation, the feature the repo exists for) and check which have zero or trivial coverage. +- Modules with high churn (git log) + no tests = top refactor risk; flag as "characterization tests first" candidates. +- Existing test quality: tests that assert nothing meaningful, heavy mocking that tests the mocks, snapshot tests nobody reads, flaky patterns (real timers, real network, order dependence). +- Missing test layers: unit-only suites with zero integration coverage on API boundaries, or the inverse (slow E2E for what a unit test would catch). +- Verification infrastructure: is there a one-command way to know the codebase works? If not, that's finding #1 and a prerequisite plan for any risky change. + +## 5. Tech Debt & Architecture + +- Duplication: the same logic re-implemented in 3+ places (search for near-identical functions/components); divergent copies that have drifted. +- Layering violations: UI importing from data layer internals, circular dependencies, "utils" modules that became a junk drawer with high fan-in. +- Dead code: unexported-and-unused modules, feature flags fully rolled out but still branching, commented-out blocks with no explanation, deps in the manifest no longer imported. +- God objects/modules: files an order of magnitude larger than the repo median that everything touches; functions with double-digit parameters or deep conditional nesting. +- Inconsistent patterns: three ways of doing data fetching / error handling / styling in the same repo — pick the winner (the one the team converged on most recently) and plan the consolidation. +- Abstraction mismatches: premature abstractions with a single implementation, or missing abstractions where the same change always requires touching N files in lockstep. + +## 6. Dependencies & Migrations + +- Major-version lag on core framework/runtime (not every minor bump — the ones with real cost to staying behind: EOL, security-fix cutoffs, ecosystem incompatibility). +- Deprecated APIs in use that have announced removal timelines. +- Abandoned dependencies (no release in years, archived repos) on critical paths. +- Duplicate dependencies solving the same problem (two date libs, two HTTP clients). +- Lockfile/manifest drift, version pinning inconsistencies across a monorepo. +- For each migration candidate, estimate blast radius (files touched) — that drives effort and whether to recommend it at all. + +## 7. DX & Tooling + +- Missing or broken: typecheck script, lint config, formatter, pre-commit hooks, editorconfig. +- Slow feedback loops: dev-server or test startup measured in minutes, no watch mode, CI without caching. +- Onboarding friction: README setup steps that are wrong/incomplete, undocumented required env vars, no `.env.example`. +- Missing `CLAUDE.md`/`AGENTS.md` — for repos where agents will execute the plans, this is high-leverage: recommend one and include its outline as a plan. +- Error messages/logging: unstructured logs on services, missing request IDs/correlation, debugging requiring code changes. + +## 8. Docs + +Lowest default priority — only flag where absence has a concrete cost: + +- Public API surface (published packages) without reference docs. +- Architectural decisions nobody can reconstruct (why X over Y) for actively-contested areas. +- Stale docs that are actively wrong (worse than missing) — setup instructions, API examples that no longer compile. + +## 9. Direction — features & where to take this next + +Forward-looking: not what's broken, but what this codebase wants to become. **Grounding rule:** every suggestion must cite evidence from the repo itself — a suggestion that could apply to any project in the category ("add dark mode", "add AI") is noise, not a finding. Sources of grounded direction signal: + +- **Unfinished intent**: TODO/FIXME clusters around one theme, feature flags never rolled out, stubbed or half-built modules, commented-out feature code, abandoned mid-feature work visible in git history. +- **Stated-but-undelivered**: README/docs/roadmap promises with no corresponding code, CLI flags or config options that are no-ops, issue templates for features that don't exist. +- **Surface asymmetries**: one-directional pairs (export without import, create without bulk-create, webhooks out but not in), entities with CRUD minus one, a public API that internal code clearly needed and hand-rolled around. +- **The adjacent possible**: capabilities the existing architecture makes disproportionately cheap — a plugin system one interface away, a public API one route file from the existing service layer, an integration the data model already supports. +- **Friction worth productizing**: things users of this project evidently do by hand around it (visible in docs, examples, issues) that the project could absorb. + +Direction findings use the standard format with two adaptations: **Impact** is product/user value (who wants this and why now), and **Confidence** reflects how grounded the evidence is — not certainty that it's the right call. Strategy belongs to the maintainer; the advisor's job is grounded options with honest trade-offs. Effort estimates here are coarser; say so. Plans for selected direction findings are usually a *design/spike plan* (investigate, prototype, define the API, list open questions) rather than a build-everything plan — scope them that way. + +--- + +## Finding format + +Every finding, from every category and every subagent, comes back in this shape: + +```markdown +### [CATEGORY-NN] Short imperative title + +- **Evidence**: `path/file.ts:123` — one-sentence description of what's there. (Repeat per location; 2–5 strongest locations, note "and ~N similar sites" if widespread.) +- **Impact**: What goes wrong / what's being paid because of this. Concrete: "every order-list render issues 1+N queries", not "suboptimal". +- **Effort**: S (hours) / M (a day-ish) / L (multi-day) — for the *fix*, including tests. +- **Risk**: What the fix could break; LOW/MED/HIGH plus one line why. +- **Confidence**: HIGH (read the code, certain) / MED (strong signal, needs verification) / LOW (smell, needs investigation). LOW-confidence findings may be reported but get an "investigate" plan, not a "fix" plan. +- **Fix sketch**: 1–3 sentences. Not the plan — just enough to judge effort honestly. +``` + +## Prioritization rubric + +Order findings by **leverage = impact ÷ effort, discounted by confidence and fix-risk**. Tiebreakers: + +1. Anything that unblocks other findings (verification baseline, characterization tests) floats up. +2. Security findings with HIGH confidence float above equivalent-leverage non-security findings. +3. Prefer findings whose fix has a clean verification story — executor models succeed at those. +4. "Not worth doing" is a valid verdict; record it with one line of reasoning so the user knows it was considered. diff --git a/.agents/skills/improve/references/closing-the-loop.md b/.agents/skills/improve/references/closing-the-loop.md new file mode 100644 index 00000000..b0d3a475 --- /dev/null +++ b/.agents/skills/improve/references/closing-the-loop.md @@ -0,0 +1,95 @@ +# Closing the Loop — execute, reconcile, issues + +The advisor's job doesn't end at the plan. This file covers the three follow-through flows: dispatching an executor and reviewing its work (`execute`), keeping the plan backlog alive (`reconcile`), and publishing plans where work gets picked up (`--issues`). + +The founding rule survives unchanged: **the advisor never edits source code.** In `execute`, a *separate executor subagent* edits code in an isolated git worktree; the advisor dispatches, reviews, and renders a verdict — like a tech lead who doesn't push commits to your branch. + +--- + +## `execute ` — dispatch and review + +### Preconditions (check all before dispatching) + +- The repo is a git repository (worktree isolation requires it). If not: stop and say so. +- The plan file exists and its dependencies show DONE in `plans/README.md`. If not: stop, name the missing dependency. +- Run the plan's drift check yourself. If in-scope files changed since `Planned at`, reconcile the plan first (see below) — don't hand a stale plan to an executor. + +### Dispatch + +Spawn **one** `general-purpose` subagent with `isolation: "worktree"`. Executor model: default `sonnet`; use what the user named if they named one (`execute 003 haiku`). + +The subagent prompt must contain: + +1. **The full plan file text, inlined.** The worktree contains only committed files — if `plans/` is uncommitted, the executor can't read it. Never assume; always inline. +2. The executor preamble: + +> You are the executor for the implementation plan below. Follow it step by +> step. Run every verification command and confirm the expected result before +> moving on. Touch only the files listed as in scope. If any STOP condition +> occurs, stop immediately and report. Do not improvise around obstacles. +> Commit your work in the worktree following the plan's git workflow section. +> One override: SKIP the plan's instruction to update `plans/README.md` — +> your reviewer maintains the index. Before reporting, audit every claim in +> your report against an actual tool result from this session — only report +> what you can point to evidence for; if a verification failed or was +> skipped, say so plainly. When finished, reply with exactly the report +> format below. + +3. The report format: + +``` +STATUS: COMPLETE | STOPPED +STEPS: per step — done/skipped + verification command result +STOPPED BECAUSE: (only if STOPPED) which STOP condition, what was observed +FILES CHANGED: list +NOTES: anything the reviewer should know (deviations, surprises, judgment calls) +``` + +### Review (the advisor's real job here) + +Note on fresh worktrees: they share git history but not `node_modules` or build artifacts — the executor must install dependencies first, and check tooling that resolves from `dist/` may need one build even though the plan's command table (recon'd in the main tree) didn't mention it. Expect this; it isn't a deviation. + +Review like a tech lead reviewing a PR against the spec — never fix anything yourself: + +1. **Re-run every done criterion** in the worktree. Don't trust the executor's report — verify. +2. **Scope compliance**: `git -C diff --stat` against the plan's in-scope list. Any file outside scope fails review, full stop. +3. **Read the full diff.** Judge it against "Why this matters" (does it solve the actual problem?) and the repo conventions named in the plan (does it look like the rest of the codebase?). +4. **Audit the new tests.** Executors game criteria — a test that asserts nothing meaningful passes `pnpm test` and proves nothing. Read what the tests assert. + +### Verdict + +**Documented deviations are judged on merit, not reflex-blocked.** "Do not improvise" exists to stop silent drift; an executor that hits a real obstacle (e.g. the plan's approach breaks existing test mocks), adapts minimally, and explains it in NOTES has done the right thing. Approve it if the adaptation serves the plan's intent and stays in scope; treat *undocumented* deviations as review failures. + +| Verdict | When | Action | +|---|---|---| +| **APPROVE** | Criteria pass, scope clean, quality holds | Update index status to DONE. Present to the user: diff summary, worktree path and branch, anything from NOTES. **Merging is the user's decision — never merge, push, or commit to their branch.** | +| **REVISE** | Fixable gaps | SendMessage to the same executor with specific, actionable feedback ("criterion 3 fails: X; the error handling in `api.ts:90` swallows the error — use the Result pattern per the plan"). **Max 2 revision rounds**, then BLOCK. | +| **BLOCK** | STOP condition hit, scope violated unrecoverably, or revisions exhausted | Mark BLOCKED in the index with the reason. Refine or rewrite the plan with what was learned. Tell the user what happened and what changed in the plan. | + +Running verification commands inside the executor's worktree is fine — it's isolated and disposable. The no-mutating-commands rule protects the user's working tree, not the worktree. + +--- + +## `reconcile` — keep `plans/` alive + +Process what happened since the last session. Read `plans/README.md` and every plan file, then per status: + +- **DONE** — spot-check that the done criteria still hold on the current HEAD (cheap ones only). Mark verified in the index. Don't delete plan files — they're the record. +- **BLOCKED** — read the reason. Investigate the underlying obstacle in the codebase. Either rewrite the plan around it (new number if the approach changed fundamentally, in-place refresh otherwise) or mark REJECTED with one line of rationale. +- **IN PROGRESS** (stale) — flag it to the user; an executor probably died mid-run. Check the worktree if one exists. +- **TODO** — run the drift check. If drifted: re-verify the finding still exists (it may have been fixed in passing), then refresh the "Current state" excerpts and `Planned at` SHA. If the finding is gone, mark REJECTED ("fixed independently"). + +Finish with a short report: what's verified done, what was refreshed, what's rejected, and what's executable right now. + +--- + +## `--issues` — publish plans as GitHub issues + +Modifier on any planning invocation (`/improve --issues`, `/improve security --issues`). The flag is the user's authorization to create issues — never create them without it. + +1. Preflight: `gh auth status` succeeds and the repo has a GitHub remote. If either fails, write the plan files as normal and say why issues were skipped. +2. Show the list of titles about to become issues; confirm once if interactive. +3. Per plan: `gh issue create --title "" --body-file `. Labels: `improve` plus the category — apply only if the labels exist or can be created without erroring; skip labels rather than fail. +4. Record each issue URL in the plan's Status block (`- **Issue**: `) and the index. + +The plan file remains the source of truth; the issue is distribution. The self-containment rule pays off here — the issue body needs no edits to make sense to whoever (or whatever) picks it up. diff --git a/.agents/skills/improve/references/plan-template.md b/.agents/skills/improve/references/plan-template.md new file mode 100644 index 00000000..113dd4e4 --- /dev/null +++ b/.agents/skills/improve/references/plan-template.md @@ -0,0 +1,192 @@ +# Handoff Plan Template + +Every plan is written for an executor model that has **zero context**: it has not seen the advisor session, the audit, the other plans, or any prior conversation. It may be a smaller/cheaper model. Assume it is competent at following explicit instructions and weak at filling gaps, recovering from ambiguity, or knowing when to stop. + +Three properties make a plan executable by a weaker model: + +1. **Self-contained context** — everything needed is in the file: paths, code excerpts, conventions, commands. +2. **Verification gates** — every step ends with a command and its expected result. The executor never has to *judge* whether it succeeded. +3. **Hard boundaries and escape hatches** — explicit out-of-scope list, and "STOP and report" conditions instead of letting the model improvise when reality doesn't match the plan. + +File naming: `plans/NNN-short-slug.md`, numbered in recommended execution order. + +--- + +## Template + +```markdown +# Plan NNN: + +> **Executor instructions**: Follow this plan step by step. Run every +> verification command and confirm the expected result before moving to the +> next step. If anything in the "STOP conditions" section occurs, stop and +> report — do not improvise. When done, update the status row for this plan +> in `plans/README.md` — unless a reviewer dispatched you and told you they +> maintain the index. +> +> **Drift check (run first)**: `git diff --stat ..HEAD -- ` +> If any in-scope file changed since this plan was written, compare the +> "Current state" excerpts against the live code before proceeding; on a +> mismatch, treat it as a STOP condition. + +## Status + +- **Priority**: P1 | P2 | P3 +- **Effort**: S | M | L +- **Risk**: LOW | MED | HIGH +- **Depends on**: plans/NNN-*.md (or "none") +- **Category**: bug | security | perf | tests | tech-debt | migration | dx | docs | direction +- **Planned at**: commit ``, +- **Issue**: + +## Why this matters + +2–5 sentences. The problem, its concrete cost, and what improves when this +lands. Written so the executor (and a human reviewer) understands the intent — +intent is what lets a correct judgment call happen when a detail is off. + +## Current state + +The facts the executor needs, inlined — never "as discussed" or "see audit": + +- The relevant files, each with one line on its role: + - `src/orders/api.ts` — order-list endpoint; contains the N+1 (lines 130–160) +- Excerpts of the code as it exists today (short, with `file:line` markers), + enough that the executor can confirm it's looking at the right thing. +- The repo conventions that apply here, with a pointer to one exemplar file: + "Error handling follows the Result pattern — see `src/lib/result.ts` and its + use in `src/users/api.ts:40-60`. Match it." + +## Commands you will need + +| Purpose | Command | Expected on success | +|-----------|--------------------------|---------------------| +| Install | `pnpm install` | exit 0 | +| Typecheck | `pnpm typecheck` | exit 0, no errors | +| Tests | `pnpm test -- ` | all pass | +| Lint | `pnpm lint` | exit 0 | + +(Exact commands from this repo — verified during recon, not guessed.) + +## Suggested executor toolkit + +(Optional — include only when relevant skills/tools plausibly exist in the +executor's environment. Skip the section otherwise.) + +- Skills the executor should invoke if available, and for what: + "use `vercel-react-best-practices` when writing the memoization in step 3". +- Reference docs worth reading before starting, by path or URL. + +## Scope + +**In scope** (the only files you should modify): +- `src/orders/api.ts` +- `src/orders/api.test.ts` (create) + +**Out of scope** (do NOT touch, even though they look related): +- `src/orders/legacy-api.ts` — deprecated path, scheduled for deletion; + changing it wastes effort and risks the v1 clients still pinned to it. +- Any change to the public response shape — clients depend on it. + +## Git workflow + +(Filled from recon — match the repo's observed conventions.) + +- Branch: `advisor/NNN-` (or the repo's branch-naming convention if one is evident) +- Commit per step or per logical unit; message style: +- Do NOT push or open a PR unless the operator instructed it. + +## Steps + +### Step 1: + +What to do, precisely. Reference exact files/symbols. Include the target code +shape when it's load-bearing (the pattern to produce, not necessarily every +line). + +**Verify**: `` → + +### Step 2: ... + +(Each step small enough to verify independently. Order steps so the codebase +is never broken between steps when possible — e.g. add new path, switch +callers, then remove old path.) + +## Test plan + +- New tests to write, in which file, covering which cases (list them: + happy path, the specific bug/regression this plan fixes, named edge cases). +- Which existing test to use as the structural pattern: + "model after `src/users/api.test.ts`". +- Verification: `` → all pass, including N new tests. + +## Done criteria + +Machine-checkable. ALL must hold: + +- [ ] `pnpm typecheck` exits 0 +- [ ] `pnpm test` exits 0; new tests for exist and pass +- [ ] `grep -rn "" src/` returns no matches +- [ ] No files outside the in-scope list are modified (`git status`) +- [ ] `plans/README.md` status row updated + +## STOP conditions + +Stop and report back (do not improvise) if: + +- The code at the locations in "Current state" doesn't match the excerpts + (the codebase has drifted since this plan was written). +- A step's verification fails twice after a reasonable fix attempt. +- The fix appears to require touching an out-of-scope file. +- You discover the assumption "" is false. + +## Maintenance notes + +For the human/agent who owns this code after the change lands: + +- What future changes will interact with this (e.g. "if pagination is added + to this endpoint, the batching in step 2 must be revisited"). +- What a reviewer should scrutinize in the PR. +- Any follow-up explicitly deferred out of this plan (and why). +``` + +--- + +## Index file: `plans/README.md` + +Written once by the advisor after all plans, updated by executors: + +```markdown +# Implementation Plans + +Generated by the improve skill on . Execute in the order below unless +dependencies say otherwise. Each executor: read the plan fully before starting, +honor its STOP conditions, and update your row when done. + +## Execution order & status + +| Plan | Title | Priority | Effort | Depends on | Status | +|------|-------|----------|--------|------------|--------| +| 001 | ... | P1 | S | — | TODO | +| 002 | ... | P1 | M | 001 | TODO | + +Status values: TODO | IN PROGRESS | DONE | BLOCKED (with one-line reason) | REJECTED (with one-line rationale — finding fixed independently or approach abandoned) + +## Dependency notes + +- 002 requires 001 because . + +## Findings considered and rejected + +- : not worth doing because . (So nobody re-audits it.) +``` + +## Quality bar — check before finishing each plan + +- Could a model that has never seen this repo execute this with only the plan file and the repo? If any step requires knowledge from the advisor session, inline that knowledge. +- Is every verification a command with an expected result, not a judgment ("make sure it works")? +- Does every step name exact files and symbols, not "the relevant module"? +- Are the STOP conditions specific to this plan's actual risks, not boilerplate? +- Would a reviewer reading only "Why this matters" + "Done criteria" understand what they're approving? +- No secret values anywhere in the file — locations and credential types only. +- "Planned at" SHA is filled in and the in-scope paths in the drift check match the Scope section. diff --git a/.agents/skills/neon-postgres/SKILL.md b/.agents/skills/neon-postgres/SKILL.md deleted file mode 100644 index 88442e9d..00000000 --- a/.agents/skills/neon-postgres/SKILL.md +++ /dev/null @@ -1,274 +0,0 @@ ---- -name: neon-postgres -description: >- - Guides and best practices for working with Neon Serverless Postgres. - Covers setup, connection methods, branching, autoscaling, scale-to-zero, - read replicas, connection pooling, Neon Auth, and the Neon CLI, MCP server, - REST API, TypeScript SDK, and Python SDK. - Use when users ask about "Neon setup", "connect to Neon", "Neon project", - "DATABASE_URL", "serverless Postgres", "Neon CLI", "neonctl", "Neon MCP", - "Neon Auth", "@neondatabase/serverless", "@neondatabase/neon-js", - "scale to zero", "Neon autoscaling", "Neon read replica", or - "Neon connection pooling". ---- - -# Neon Serverless Postgres - -Guide the user through any Neon-related task: setup, connections, branching, and advanced features. Deliver a working Neon connection, a completed feature configuration, or a specific answer from the official Neon docs. - -Neon is a serverless Postgres platform that separates compute and storage to offer autoscaling, branching, instant restore, and scale-to-zero. It's fully compatible with Postgres and works with any language, framework, or ORM that supports Postgres. - -## Neon Documentation - -The Neon documentation is the source of truth for all Neon-related information. Always verify claims against the official docs before responding. Neon features and APIs evolve, so prefer fetching current docs over relying on training data. - -### Fetching Docs as Markdown - -Any Neon doc page can be fetched as markdown in two ways: - -1. **Append `.md` to the URL** (simplest): https://neon.com/docs/introduction/branching.md -2. **Request `text/markdown`** on the standard URL: `curl -H "Accept: text/markdown" https://neon.com/docs/introduction/branching` - -Both return the same markdown content. Use whichever method your tools support. - -### Finding the Right Page - -The docs index lists every available page with its URL and a short description: - -``` -https://neon.com/docs/llms.txt -``` - -Common doc URLs are organized in the topic links below. If you need a page not listed here, search the docs index: https://neon.com/docs/llms.txt. Don't guess URLs. - -## What Is Neon - -Use this for architecture explanations and terminology (organizations, projects, branches, endpoints) before giving implementation advice. - -Link: https://neon.com/docs/introduction/architecture-overview.md - -## Getting Started - -Use this section when guiding a user through first-time Neon setup. - -### Check Status Quo - -Before starting setup, inspect the user's codebase and environment: - -- Existing database connection code -- Existing Neon MCP server or Neon CLI configuration -- Existence of a `.env` file and `DATABASE_URL` environment variable -- Existing ORM (Prisma, Drizzle, TypeORM) configuration - -### Self-Driving Setup With Neon's CLI or MCP Server - -Offer to inspect existing connected Neon projects or create new ones using the Neon CLI or MCP server. If neither is set up yet, run init with the `--agent` flag. Use `npx -y` to skip the package install prompt. Auth is handled automatically. If the user is not logged in, it opens their browser for OAuth and waits for completion before proceeding. - -```bash -npx -y neonctl@latest init --agent -``` - -Supported `--agent` values: `cursor`, `copilot`, `claude`, `claude-desktop`, `codex`, `opencode`, `cline`, `gemini-cli`, `goose`, `zed`. - -This installs the Neon extension (for Cursor/VS Code) or MCP server (for other agents), creates an API key, and adds the `neon-postgres` agent skill to the project. - -If `init` is not suitable, the individual steps can be run non-interactively: - -- **Extension:** `cursor --install-extension databricks.neon-local-connect` -- **MCP server:** `npx -y add-mcp https://mcp.neon.tech/mcp -g -n Neon -y -a ` -- **Agent skill:** `npx skills add neondatabase/agent-skills --skill neon-postgres --agent -y` - -For full CLI installation options, see https://neon.com/docs/reference/cli-install.md - -### Setup Flow - -**1. Select Organization and Project** - -Use MCP server or CLI to list organizations and projects. Let the user select an existing project or create a new one. - -**2. Get Connection String** - -Use MCP server or CLI to get the connection string. Store it in `.env` as `DATABASE_URL`. Read the file first before modifying to avoid overwriting existing values. - -**3. Pick Connection Method & Driver** - -Refer to the connection methods guide to pick the correct driver based on deployment platform: https://neon.com/docs/connect/choose-connection.md - -**4. User Authentication with Neon Auth (if needed)** - -Skip for CLI tools, scripts, or apps without user accounts. If the app needs auth: use MCP server `provision_neon_auth` tool, then see the auth overview (https://neon.com/docs/auth/overview.md) for setup. For auth + database queries, see the JavaScript SDK reference (https://neon.com/docs/reference/javascript-sdk.md). - -**5. ORM Setup (optional)** - -Check for existing ORM (Prisma, Drizzle, TypeORM). If none, ask if they want one. For Drizzle integration, see https://neon.com/docs/guides/drizzle.md. - -**6. Schema Setup** - -- Check for existing migration files or ORM schemas -- If none: offer to create an example schema or design one together - -### Resume Support - -If resuming setup, check what's already configured (MCP connection, `.env` with `DATABASE_URL`, dependencies, schema) and continue from the next incomplete step. - -### Security Reminders - -Remind users to use environment variables for credentials, never commit connection strings, and use least-privilege database roles. - -## Connection Methods & Drivers - -Use this when you need to pick the correct transport and driver based on runtime constraints (TCP, HTTP, WebSocket, edge, serverless, long-running). - -Link: https://neon.com/docs/connect/choose-connection.md - -### Serverless Driver - -Use this for `@neondatabase/serverless` patterns, including HTTP queries, WebSocket transactions, and runtime-specific optimizations. - -Link: https://neon.com/docs/serverless/serverless-driver.md - -### Neon JS SDK - -Use this for combined Neon Auth + Data API workflows with PostgREST-style querying and typed client setup. - -Link: https://neon.com/docs/reference/javascript-sdk.md - -## Developer Tools - -Use this for local development enablement with `npx -y neonctl@latest init --agent `, VSCode extension setup, and Neon MCP server configuration. - -| Tool | URL | -| ---------------- | ----------------------------------------------- | -| CLI Init Command | https://neon.com/docs/reference/cli-init.md | -| VSCode Extension | https://neon.com/docs/local/vscode-extension.md | -| MCP Server | https://neon.com/docs/ai/neon-mcp-server.md | -| Neon CLI | https://neon.com/docs/reference/neon-cli.md | - -### Neon CLI - -Use this for terminal-first workflows, scripts, and CI/CD automation with `neonctl`. - -Link: https://neon.com/docs/reference/neon-cli.md - -## Neon Admin API - -The Neon Admin API can be used to manage Neon resources programmatically. It is used behind the scenes by the Neon CLI and MCP server, but can also be used directly for more complex automation workflows or when embedding Neon in other applications. - -### Neon REST API - -Use this for direct HTTP automation, endpoint-level control, API key auth, rate-limit handling, and operation polling. - -Link: https://neon.com/docs/reference/api-reference.md - -### Neon TypeScript SDK - -Use this when implementing typed programmatic control of Neon resources in TypeScript via `@neondatabase/api-client`. - -Link: https://neon.com/docs/reference/typescript-sdk.md - -### Neon Python SDK - -Use this when implementing programmatic Neon management in Python with the `neon-api` package. - -Link: https://neon.com/docs/reference/python-sdk.md - -## Neon Auth - -Use this for managed user authentication setup, UI components, auth methods, and Neon Auth integration pitfalls in Next.js and React apps. - -Link: https://neon.com/docs/auth/overview.md - -Neon Auth is also embedded in the Neon JS SDK. Depending on your use case, you may want to use the Neon JS SDK instead of Neon Auth alone. See https://neon.com/docs/connect/choose-connection.md for more details. - -## Branching - -Use this when the user is planning isolated environments, schema migration testing, preview deployments, or branch lifecycle automation. - -Key points: - -- Branches are instant, copy-on-write clones (no full data copy). -- Each branch has its own compute endpoint. -- Use the neonctl CLI or MCP server to create, inspect, and compare branches. - -Link: https://neon.com/docs/introduction/branching.md - -For detailed branch creation workflows (normal vs schema-only branches, reset-from-parent, CLI/MCP selection), fetch the full branching skill: - -https://neon.com/docs/ai/skills/neon-postgres-branches/SKILL.md - -To install the skill directly: - -```bash -npx skills add neondatabase/agent-skills --skill neon-postgres-branches -``` - -## Autoscaling - -Use this when the user needs compute to scale automatically with workload and wants guidance on CU sizing and runtime behavior. - -Link: https://neon.com/docs/introduction/autoscaling.md - -## Scale to Zero - -Use this when optimizing idle costs and discussing suspend/resume behavior, including cold-start trade-offs. - -Key points: - -- Idle computes suspend automatically (default 5 minutes, configurable) (unless disabled - launch & scale plan only) -- First query after suspend typically has a cold-start penalty (around hundreds of ms) -- Storage remains active while compute is suspended. - -Link: https://neon.com/docs/introduction/scale-to-zero.md - -## Instant Restore - -Use this when the user needs point-in-time recovery or wants to restore data state without traditional backup restore workflows. - -Key points: - -- History windows for instant restore depend on plan limits. -- Users can create branches from historical points-in-time. -- Time Travel queries can be used for historical inspection workflows. - -Link: https://neon.com/docs/introduction/branch-restore.md - -## Read Replicas - -Use this for read-heavy workloads where the user needs dedicated read-only compute without duplicating storage. - -Key points: - -- Replicas are read-only compute endpoints sharing the same storage. -- Creation is fast and scaling is independent from primary compute. -- Typical use cases: analytics, reporting, and read-heavy APIs. - -Link: https://neon.com/docs/introduction/read-replicas.md - -## Connection Pooling - -Use this when the user is in serverless or high-concurrency environments and needs safe, scalable Postgres connection management. - -Key points: - -- Neon pooling uses PgBouncer. -- Add `-pooler` to endpoint hostnames to use pooled connections. -- Pooling is especially important in serverless runtimes with bursty concurrency. - -Link: https://neon.com/docs/connect/connection-pooling.md - -## IP Allow Lists - -Use this when the user needs to restrict database access by trusted networks, IPs, or CIDR ranges. - -Link: https://neon.com/docs/introduction/ip-allow.md - -## Logical Replication - -Use this when integrating CDC pipelines, external Postgres sync, or replication-based data movement. - -Key points: - -- Neon supports native logical replication workflows. -- Useful for replicating to/from external Postgres systems. - -Link: https://neon.com/docs/guides/logical-replication-guide.md diff --git a/.agents/skills/slack-agent/.gitignore b/.agents/skills/slack-agent/.gitignore new file mode 100644 index 00000000..a9deafad --- /dev/null +++ b/.agents/skills/slack-agent/.gitignore @@ -0,0 +1,21 @@ +# Dependencies +node_modules/ + +# Environment +.env +.env.local +.env.*.local + +# OS +.DS_Store +Thumbs.db + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# Logs +*.log +npm-debug.log* diff --git a/.agents/skills/slack-agent/LICENSE b/.agents/skills/slack-agent/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/.agents/skills/slack-agent/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/.agents/skills/slack-agent/README.md b/.agents/skills/slack-agent/README.md new file mode 100644 index 00000000..b3141bda --- /dev/null +++ b/.agents/skills/slack-agent/README.md @@ -0,0 +1,105 @@ +# Slack Agent Skill + +An agent-agnostic skill for building and deploying Slack agents on Vercel. Supports two frameworks: + +- **[Chat SDK](https://www.chat-sdk.dev/)** (Recommended for new projects) — `chat` + `@chat-adapter/slack` with Next.js +- **[Bolt for JavaScript](https://slack.dev/bolt-js/)** (For existing Bolt projects) — `@slack/bolt` with Nitro + +## Features + +- **Interactive Setup Wizard**: Step-by-step guidance from project creation to production deployment +- **Dual Framework Support**: Chat SDK (JSX components, thread subscriptions) and Bolt for JavaScript (Block Kit, event listeners) +- **Custom Implementation Planning**: Generates a tailored plan based on your agent's purpose before scaffolding +- **Quality Standards**: Embedded testing and code quality requirements +- **AI Integration**: Support for Vercel AI Gateway and direct provider SDKs +- **Comprehensive Patterns**: Slack-specific development patterns and best practices for both frameworks +- **Testing Framework**: Vitest configuration and sample tests for both stacks + +## Installation + +### Via skills.sh (Recommended) + +npx skills add vercel-labs/slack-agent-skill + +### Manual Installation + +Clone the repository into your skills directory. For example, with Claude Code: + +git clone https://github.com/vercel-labs/slack-agent-skill.git ~/.claude/skills/slack-agent-skill + +## Usage + +### Starting a New Project + +Run the slash command: + +``` +/slack-agent + +Or with arguments: +/slack-agent new # Start fresh project (recommends Chat SDK) +/slack-agent configure # Configure existing project (auto-detects framework) +/slack-agent deploy # Deploy to production +/slack-agent test # Set up testing +``` + +The wizard will guide you through: +1. Framework selection and project setup +2. Custom implementation plan generation and approval +3. Slack app creation with customized manifest +4. Environment configuration +5. Local testing with ngrok +6. Production deployment to Vercel +7. Test framework setup + +### Development + +When working on an existing Slack agent project, the skill automatically detects the framework from `package.json`: +- **`"chat"` in dependencies** — Uses Chat SDK patterns +- **`"@slack/bolt"` in dependencies** — Uses Bolt patterns + +The skill then provides framework-appropriate: +- Code quality standards (linting, testing, TypeScript) +- Slack-specific patterns (event handlers, slash commands, UI components) +- AI integration guidance (Vercel AI Gateway, direct providers) +- Deployment best practices + +## Key Commands + +```bash +# Development +pnpm dev # Start local dev server +ngrok http 3000 # Expose local server + +# Quality +pnpm lint # Check linting +pnpm lint --write # Auto-fix lint issues +pnpm typecheck # TypeScript check +pnpm test # Run tests + +# Deployment +vercel # Deploy to Vercel +vercel --prod # Production deployment +``` + +## Quality Standards + +The skill enforces these requirements: + +- **Unit tests** for all exported functions +- **E2E tests** for user-facing changes +- **Linting** must pass (Biome) +- **TypeScript** must compile without errors +- **All tests** must pass before completion + +## Related Resources + +- [Chat SDK Documentation](https://www.chat-sdk.dev/) +- [Bolt for JavaScript Documentation](https://slack.dev/bolt-js/) +- [AI SDK Documentation](https://ai-sdk.dev) +- [Slack API Documentation](https://api.slack.com) +- [Vercel Documentation](https://vercel.com/docs) + +## License + +Apache 2.0 - See [LICENSE](LICENSE) for details. diff --git a/.agents/skills/slack-agent/SKILL.md b/.agents/skills/slack-agent/SKILL.md new file mode 100644 index 00000000..50f19ea7 --- /dev/null +++ b/.agents/skills/slack-agent/SKILL.md @@ -0,0 +1,1008 @@ +--- +name: slack-agent +description: Use when working on Slack agent/bot code, Chat SDK applications, Bolt for JavaScript projects, or projects using @chat-adapter/slack or @slack/bolt. Provides development patterns, testing requirements, and quality standards. +version: 4.0.0 +user-invocable: true +--- + +# Slack Agent Development Skill + +This skill supports two frameworks for building Slack agents: + +- **Chat SDK** (Recommended for new projects) — `chat` + `@chat-adapter/slack` +- **Bolt for JavaScript** (For existing Bolt projects) — `@slack/bolt` + `@vercel/slack-bolt` + +## 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 `chat` or `@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 + +### Framework Detection + +Detect which framework the project uses: + +- **`package.json` contains `"chat"`** → Chat SDK project +- **`package.json` contains `"@slack/bolt"`** → Bolt project +- **Neither detected** → New project, recommend Chat SDK (offer Bolt as alternative) + +Store the detected framework and use it to show the correct patterns throughout the wizard and development guidance. + +### Wizard Phases + +The wizard is located in `./wizard/` with these phases: +- `1-project-setup.md` - Understand purpose, choose framework, generate custom implementation plan +- `1b-approve-plan.md` - Present plan for user approval before scaffolding +- `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. Offer framework choice (Chat SDK recommended, Bolt as alternative) +4. Generate a custom implementation plan using `./reference/agent-archetypes.md` +5. Present the plan for approval (Phase 1b) BEFORE scaffolding the project +6. Only proceed to scaffold after the plan is approved + +--- + +## Framework Selection Guide + +| Aspect | Chat SDK | Bolt for JavaScript | +|--------|----------|---------------------| +| **Best for** | New projects | Existing Bolt codebases | +| **Packages** | `chat`, `@chat-adapter/slack`, `@chat-adapter/state-redis` | `@slack/bolt`, `@vercel/slack-bolt` | +| **Server** | Next.js App Router | Nitro (H3-based) | +| **Event handling** | `bot.onNewMention()`, `bot.onSubscribedMessage()` | `app.event()`, `app.command()`, `app.message()` | +| **Webhook route** | `app/api/webhooks/[platform]/route.ts` | `server/api/slack/events.post.ts` | +| **Message posting** | `thread.post("text")` / `thread.post(...)` | `client.chat.postMessage({ channel, text, blocks })` | +| **UI components** | JSX: ``, ` + + + +); +``` + +### If using Bolt for JavaScript — Block Kit JSON + +Use Block Kit for rich messages: + +```typescript +await client.chat.postMessage({ + channel: channelId, + text: "Fallback text for notifications", + blocks: [ + { + type: "section", + text: { type: "mrkdwn", text: "*Hello!* Choose an option:" }, + }, + { type: "divider" }, + { + type: "actions", + elements: [ + { + type: "button", + text: { type: "plain_text", text: "Say Hello" }, + style: "primary", + action_id: "btn_hello", + }, + { + type: "button", + text: { type: "plain_text", text: "Show Info" }, + action_id: "btn_info", + }, + ], + }, + ], +}); +``` + +### Typing Indicators + +#### If using Chat SDK + +```typescript +await thread.startTyping(); +const result = await generateWithAI(prompt); +await thread.post(result); // Typing indicator clears on post +``` + +#### If using Bolt for JavaScript + +```typescript +// Use setStatus for Assistant threads or interval-based approach +const typingInterval = setInterval(async () => { + // Post a "typing" indicator or use assistant.threads.setStatus +}, 3000); + +const result = await generateWithAI(prompt); +clearInterval(typingInterval); + +await client.chat.postMessage({ + channel: channelId, + thread_ts: threadTs, + text: result, +}); +``` + +### Message Formatting (both frameworks) + +Use Slack mrkdwn (not standard markdown): +- Bold: `*text*` +- Italic: `_text_` +- Code: `` `code` `` +- User mention: `<@USER_ID>` +- Channel: `<#CHANNEL_ID>` + +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 +- [ ] **Chat SDK:** Webhook route handles all platforms via `bot.webhooks` +- [ ] **Chat SDK:** TSConfig includes `"jsx": "react-jsx"` and `"jsxImportSource": "chat"` if using JSX components +- [ ] **Bolt:** 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 diff --git a/.agents/skills/slack-agent/patterns/slack-patterns.md b/.agents/skills/slack-agent/patterns/slack-patterns.md new file mode 100644 index 00000000..ece745e1 --- /dev/null +++ b/.agents/skills/slack-agent/patterns/slack-patterns.md @@ -0,0 +1,695 @@ +# Slack Development Patterns + +This document covers Slack-specific patterns and best practices for building agents with either the Chat SDK or Bolt for JavaScript. + +## Rich UI + +### If using Chat SDK — JSX Components + +Chat SDK uses JSX components instead of raw Block Kit JSON. Files using JSX must have the `.tsx` extension. + +```tsx +import { Card, CardText as Text, Actions, Button, Divider } from "chat"; + +await thread.post( + + *Hello!* This is a formatted message. + + Choose an option: + + + + +); +``` + +#### Interactive Actions (Chat SDK) + +```tsx +import { Card, CardText as Text, Actions, Button, Select, Option } from "chat"; + +// Button with danger style +await thread.post( + + Are you sure you want to delete this? + + + + + +); + +// Select menu +await thread.post( + + Select an option: + + + + +); +``` + +### If using Bolt for JavaScript — Block Kit JSON + +```typescript +import { WebClient } from '@slack/web-api'; + +const client = new WebClient(process.env.SLACK_BOT_TOKEN); + +await client.chat.postMessage({ + channel: channelId, + text: 'Fallback text for notifications', // Required for accessibility + blocks: [ + { + type: 'section', + text: { type: 'mrkdwn', text: '*Hello!* This is a formatted message.' }, + }, + { type: 'divider' }, + { + type: 'section', + text: { type: 'mrkdwn', text: 'Choose an option:' }, + accessory: { + type: 'button', + text: { type: 'plain_text', text: 'Click Me' }, + action_id: 'button_click', + value: 'button_value', + }, + }, + ], +}); +``` + +#### Interactive Actions (Bolt) + +```typescript +// Button with confirmation +{ + type: 'button', + text: { type: 'plain_text', text: 'Delete' }, + style: 'danger', + action_id: 'delete_item', + value: itemId, + confirm: { + title: { type: 'plain_text', text: 'Confirm Delete' }, + text: { type: 'mrkdwn', text: 'Are you sure you want to delete this?' }, + confirm: { type: 'plain_text', text: 'Delete' }, + deny: { type: 'plain_text', text: 'Cancel' }, + }, +} + +// Select menu +{ + type: 'static_select', + placeholder: { type: 'plain_text', text: 'Select an option' }, + action_id: 'select_option', + options: [ + { text: { type: 'plain_text', text: 'Option 1' }, value: 'opt1' }, + { text: { type: 'plain_text', text: 'Option 2' }, value: 'opt2' }, + ], +} +``` + +#### Context and Header Blocks (Bolt) + +```typescript +// Header +{ type: 'header', text: { type: 'plain_text', text: 'Task Summary' } } + +// Context (small text, often for metadata) +{ + type: 'context', + elements: [ + { type: 'mrkdwn', text: 'Created by <@U12345678>' }, + { type: 'mrkdwn', text: '|' }, + { type: 'mrkdwn', text: '' }, + ], +} +``` + +--- + +## Message Formatting (mrkdwn) + +Slack uses its own markdown variant called mrkdwn. This applies to both frameworks. + +### Text Formatting +``` +*bold text* +_italic text_ +~strikethrough~ +`inline code` +```code block``` +> blockquote +``` + +### Links and Mentions +``` + +<@U12345678> # User mention +<#C12345678> # Channel link + # @here mention + # @channel mention + # Date formatting +``` + +### Lists +``` +Slack doesn't support markdown lists, use: +* Bullet point (use the actual bullet character) +1. Numbered manually +``` + +--- + +## Webhook / Events Endpoint + +### If using Chat SDK + +```typescript +// app/api/webhooks/[platform]/route.ts +import { after } from "next/server"; +import { bot } from "@/lib/bot"; + +export async function POST(request: Request, context: { params: Promise<{ platform: string }> }) { + const { platform } = await context.params; + const handler = bot.webhooks[platform as keyof typeof bot.webhooks]; + if (!handler) return new Response("Unknown platform", { status: 404 }); + return handler(request, { waitUntil: (task) => after(() => task) }); +} +``` + +The Chat SDK automatically handles: +- Content-type detection (JSON vs form-urlencoded) +- URL verification challenges +- Slack's 3-second ack timeout +- Background processing via `waitUntil` +- Signature verification using `SLACK_SIGNING_SECRET` + +### If using Bolt for JavaScript + +```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 }; +``` + +```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) => { + const rawBody = await readRawBody(event, "utf8"); + const request = new Request(getRequestURL(event), { + method: event.method, + headers: event.headers, + body: rawBody, + }); + return await handler(request); +}); +``` + +**Why buffer the body?** H3's `toWebRequest()` eagerly consumes the request body stream, causing `dispatch_failed` errors on serverless platforms. + +### Content Type Reference (both frameworks) + +| Event Type | Content-Type | Handled Automatically | +|------------|--------------|----------------------| +| Slash commands | `application/x-www-form-urlencoded` | Yes | +| Events API | `application/json` | Yes | +| Interactivity | `application/json` | Yes | +| URL verification | `application/json` | Yes | + +--- + +## Event Handling Patterns + +### Mention Handler + +#### If using Chat SDK + +```typescript +bot.onNewMention(async (thread, message) => { + try { + const text = message.text; // Mention prefix already stripped + await thread.subscribe(); + await thread.post(`Processing your request: "${text}"`); + // Process with agent... + } catch (error) { + console.error("Error handling mention:", error); + await thread.post("Sorry, I encountered an error processing your request."); + } +}); +``` + +#### If using Bolt for JavaScript + +```typescript +app.event('app_mention', async ({ event, client, say }) => { + try { + const text = event.text.replace(/<@[A-Z0-9]+>/g, '').trim(); + const thread_ts = event.thread_ts || event.ts; + + await say({ + text: `Processing your request: "${text}"`, + thread_ts, + }); + // Process with agent... + } catch (error) { + console.error('Error handling mention:', error); + await say({ + text: 'Sorry, I encountered an error processing your request.', + thread_ts: event.thread_ts || event.ts, + }); + } +}); +``` + +### Subscribed / Follow-up Message Handler + +#### If using Chat SDK + +```typescript +bot.onSubscribedMessage(async (thread, message) => { + await thread.post(`You said: ${message.text}`); +}); +``` + +#### If using Bolt for JavaScript + +```typescript +app.message(async ({ message, say }) => { + if ('bot_id' in message) return; + if ('subtype' in message && message.subtype === 'message_changed') return; + if (message.channel_type !== 'im' && !message.thread_ts) return; + + await say({ + text: `You said: ${message.text}`, + thread_ts: message.thread_ts, + }); +}); +``` + +### Slash Command Handler + +#### If using Chat SDK + +```typescript +bot.onSlashCommand("/sample-command", async (event) => { + try { + // Chat SDK handles ack and background processing automatically + await event.thread.startTyping(); + const result = await processCommand(event.text); + await event.thread.post(`Result: ${result}`); + } catch (error) { + await event.thread.post("Sorry, something went wrong."); + } +}); +``` + +**No fire-and-forget pattern needed.** The Chat SDK acknowledges the request immediately and processes the handler in the background. + +#### If using Bolt for JavaScript + +```typescript +app.command('/sample-command', async ({ command, ack, respond }) => { + await ack(); // Always acknowledge within 3 seconds + + try { + const result = await processCommand(command.text); + await respond({ + response_type: 'ephemeral', + text: `Result: ${result}`, + }); + } catch (error) { + await respond({ + response_type: 'ephemeral', + text: 'Sorry, something went wrong.', + }); + } +}); +``` + +### Long-Running Slash Commands (AI, API calls) + +#### If using Chat SDK + +```typescript +bot.onSlashCommand("/ai-command", async (event) => { + await event.thread.startTyping(); + // This can take as long as needed - Chat SDK handles the ack automatically + const result = await generateWithAI(event.text); + await event.thread.post(result); +}); +``` + +#### If using Bolt for JavaScript + +**CRITICAL:** Use fire-and-forget to avoid `operation_timeout` errors. + +```typescript +app.command('/ai-command', async ({ ack, command, logger }) => { + await ack(); // Must happen first + + // Fire-and-forget: DON'T await + processInBackground(command.response_url, command.text, logger) + .catch((error) => logger.error("Background processing failed:", error)); +}); + +async function processInBackground(responseUrl: string, text: string, logger: Logger) { + try { + const result = await generateWithAI(text); + await fetch(responseUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ response_type: "in_channel", text: result }), + }); + } catch (error) { + logger.error("AI processing failed:", error); + await fetch(responseUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ response_type: "ephemeral", text: "Sorry, something went wrong." }), + }); + } +} +``` + +**Bolt slash command patterns:** + +| Pattern | Use Case | Example | +|---------|----------|---------| +| **Sync** (`await ack({ text })`) | Instant responses | `/help`, `/status` | +| **Async** (`await ack()` + `await respond()`) | Quick operations (<3 sec) | `/search keyword` | +| **Fire-and-forget** (`await ack()` + no await) | AI/LLM, slow APIs | `/generate`, `/analyze` | + +--- + +## Action Handlers + +### If using Chat SDK + +```typescript +bot.onAction("button_click", async (event) => { + await event.thread.post(`You clicked: ${event.value}`); +}); + +bot.onAction("select_option", async (event) => { + await event.thread.post(`You selected: ${event.value}`); +}); +``` + +### If using Bolt for JavaScript + +```typescript +app.action('button_click', async ({ body, ack, client }) => { + await ack(); + const buttonValue = body.actions[0].value; + await client.chat.update({ + channel: body.channel.id, + ts: body.message.ts, + text: 'Updated message', + blocks: [/* new blocks */], + }); +}); + +app.action('select_option', async ({ body, ack }) => { + await ack(); + const selectedValue = body.actions[0].selected_option.value; + // Handle selection... +}); +``` + +--- + +## Modal Patterns + +### If using Chat SDK + +```tsx +import { Modal, TextInput } from "chat"; + +// Opening a modal +bot.onSlashCommand("/open-form", async (event) => { + await event.openModal( + + + + ); +}); + +// Handling submission +bot.onAction("modal_submit", async (event) => { + const inputValue = event.values?.input_value; + if (!inputValue || inputValue.length < 3) { + return { errors: { input_value: "Please enter at least 3 characters" } }; + } + await event.thread.post(`You submitted: ${inputValue}`); +}); +``` + +### If using Bolt for JavaScript + +```typescript +// Opening a modal +app.shortcut('open_modal', async ({ shortcut, ack, client }) => { + await ack(); + await client.views.open({ + trigger_id: shortcut.trigger_id, + view: { + type: 'modal', + callback_id: 'modal_submit', + title: { type: 'plain_text', text: 'My Modal' }, + submit: { type: 'plain_text', text: 'Submit' }, + close: { type: 'plain_text', text: 'Cancel' }, + blocks: [ + { + type: 'input', + block_id: 'input_block', + label: { type: 'plain_text', text: 'Your Input' }, + element: { + type: 'plain_text_input', + action_id: 'input_value', + placeholder: { type: 'plain_text', text: 'Enter something...' }, + }, + }, + ], + }, + }); +}); + +// Handling submission +app.view('modal_submit', async ({ ack, body, view, client }) => { + const inputValue = view.state.values.input_block.input_value.value; + if (!inputValue || inputValue.length < 3) { + await ack({ + response_action: 'errors', + errors: { input_block: 'Please enter at least 3 characters' }, + }); + return; + } + await ack(); + await client.chat.postMessage({ + channel: body.user.id, + text: `You submitted: ${inputValue}`, + }); +}); +``` + +--- + +## Thread Management + +### If using Chat SDK + +```typescript +bot.onNewMention(async (thread, message) => { + await thread.subscribe(); + await thread.post("I'm listening! Send me follow-up messages in this thread."); +}); + +bot.onSubscribedMessage(async (thread, message) => { + await thread.post(`Got your message: ${message.text}`); +}); +``` + +### If using Bolt for JavaScript + +```typescript +// Always reply in the same thread +const thread_ts = event.thread_ts || event.ts; +await say({ text: 'Response message', thread_ts }); + +// Broadcasting thread replies +await client.chat.postMessage({ + channel: channelId, + thread_ts: parentTs, + text: 'Important update!', + reply_broadcast: true, // Also posts to channel +}); +``` + +--- + +## Typing Indicators + +### If using Chat SDK + +```typescript +bot.onNewMention(async (thread, message) => { + await thread.startTyping(); + const result = await processWithAI(message.text); + await thread.post(result); // Typing clears automatically +}); +``` + +The Chat SDK handles typing indicator refresh and timeout automatically. + +### If using Bolt for JavaScript + +Slack's typing indicator expires after **30 seconds**. Refresh for long operations: + +```typescript +async function withTypingIndicator( + client: WebClient, + channelId: string, + threadTs: string, + status: string, + operation: () => Promise +): Promise { + await client.assistant.threads.setStatus({ + channel_id: channelId, + thread_ts: threadTs, + status, + }); + + const refreshInterval = setInterval(async () => { + await client.assistant.threads.setStatus({ + channel_id: channelId, + thread_ts: threadTs, + status, + }); + }, 25000); + + try { + return await operation(); + } finally { + clearInterval(refreshInterval); + } +} +``` + +Status message examples: `'is thinking...'`, `'is researching...'`, `'is writing...'`, `'is analyzing...'` + +--- + +## Error Handling + +### If using Chat SDK + +```typescript +bot.onNewMention(async (thread, message) => { + try { + await processMessage(thread, message); + } catch (error) { + console.error("Operation failed:", error); + let userMessage = "Something went wrong. Please try again."; + if (error instanceof Error) { + if (error.message.includes("channel_not_found")) { + userMessage = "I don't have access to that channel."; + } else if (error.message.includes("not_in_channel")) { + userMessage = "Please invite me to the channel first."; + } + } + await thread.post(userMessage); + } +}); +``` + +### If using Bolt for JavaScript + +```typescript +async function handleWithErrorRecovery( + operation: () => Promise, + say: SayFn, + thread_ts?: string +) { + try { + await operation(); + } catch (error) { + console.error('Operation failed:', error); + let userMessage = 'Something went wrong. Please try again.'; + if (error instanceof SlackAPIError) { + if (error.code === 'channel_not_found') { + userMessage = "I don't have access to that channel."; + } else if (error.code === 'not_in_channel') { + userMessage = 'Please invite me to the channel first.'; + } + } + await say({ text: userMessage, thread_ts }); + } +} +``` + +#### Rate Limiting (Bolt) + +```typescript +import pRetry from 'p-retry'; + +async function sendMessageWithRetry(client: WebClient, options: ChatPostMessageArguments) { + return pRetry( + () => client.chat.postMessage(options), + { + retries: 3, + onFailedAttempt: (error) => { + if (error.code === 'rate_limited') { + console.log(`Rate limited. Retrying after ${error.retryAfter || 1}s`); + } + }, + } + ); +} +``` + +--- + +## Best Practices Summary + +**Both frameworks:** +1. **Handle errors gracefully** with user-friendly messages +2. **Use ephemeral messages** for sensitive or temporary information +3. **Log errors** with context for debugging +4. **Use threads** to keep channels clean + +**Chat SDK specific:** +5. **Subscribe to threads** with `thread.subscribe()` for follow-up conversations +6. **Use JSX components** for rich messages instead of raw Block Kit JSON +7. **Use typing indicators** with `thread.startTyping()` +8. **Let Chat SDK handle ack** — no manual acknowledgment needed +9. **Use `.tsx` extension** for files with JSX components +10. **Configure tsconfig.json** with `"jsxImportSource": "chat"` + +**Bolt specific:** +5. **Always acknowledge** within 3 seconds for interactive elements +6. **Provide fallback text** in all Block Kit messages +7. **Respect rate limits** with exponential backoff +8. **Buffer request body** in events handler to avoid H3 stream issues +9. **Use fire-and-forget** for slash commands with AI/long operations (>3 sec) +10. **Use `reply_broadcast: true`** for important thread replies diff --git a/.agents/skills/slack-agent/patterns/testing-patterns.md b/.agents/skills/slack-agent/patterns/testing-patterns.md new file mode 100644 index 00000000..441c4bae --- /dev/null +++ b/.agents/skills/slack-agent/patterns/testing-patterns.md @@ -0,0 +1,462 @@ +# Testing Patterns for Slack Agents + +This document provides detailed testing patterns for Slack agent projects built with either the Chat SDK or Bolt for JavaScript. + +## Test File Organization + +### If using Chat SDK + +``` +lib/ +├── __tests__/ +│ ├── setup.ts # Global test setup and mocks +│ └── helpers/ +│ ├── mock-context.ts # Shared context mocks +│ └── mock-thread.ts # Chat SDK thread mocks +├── bot.tsx # Bot instance +├── bot.test.ts # Bot handler tests +├── ai/ +│ ├── agent.ts +│ ├── agent.test.ts # Unit tests (co-located) +│ └── tools/ +│ ├── search.ts +│ └── search.test.ts +app/ +├── api/ +│ └── webhooks/ +│ └── [platform]/ +│ └── route.ts +``` + +Template files: `./templates/chat-sdk/` + +### If using Bolt for JavaScript + +``` +server/ +├── __tests__/ +│ ├── setup.ts # Global test setup and mocks +│ └── helpers/ +│ ├── mock-client.ts # Slack WebClient mocks +│ └── mock-context.ts # Bolt context mocks +├── bolt/ +│ └── app.ts +├── listeners/ +│ ├── events/ +│ │ ├── app-mention.ts +│ │ └── app-mention.test.ts +│ └── commands/ +│ ├── sample-command.ts +│ └── sample-command.test.ts +└── lib/ + └── ai/ + ├── agent.ts + ├── agent.test.ts + └── tools.ts + └── tools.test.ts +``` + +Template files: `./templates/bolt/` + +--- + +## Unit Testing Tools + +### Testing a Tool Definition + +```typescript +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { getChannelMessages } from './tools'; + +describe('getChannelMessages', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should fetch messages from channel', async () => { + const result = await getChannelMessages.execute({ + channel_id: 'C12345678', + limit: 10, + }); + + expect(result.success).toBe(true); + expect(result.messages).toHaveLength(2); + expect(result.messages[0].text).toBe('Hello'); + }); + + it('should handle empty channel', async () => { + const result = await getChannelMessages.execute({ + channel_id: 'C_EMPTY', + limit: 10, + }); + + expect(result.success).toBe(true); + expect(result.messages).toHaveLength(0); + }); + + it('should handle API errors gracefully', async () => { + const result = await getChannelMessages.execute({ + channel_id: 'C_INVALID', + limit: 10, + }); + + expect(result.success).toBe(false); + expect(result.error).toContain('channel_not_found'); + }); +}); +``` + +--- + +## Testing Bot / Event Handlers + +### If using Chat SDK + +```typescript +import { describe, it, expect, vi } from 'vitest'; +import { createMockThread, createMockMessage } from './helpers/mock-thread'; + +describe('bot.onNewMention', () => { + it('should respond to mention and subscribe', async () => { + const thread = createMockThread(); + const message = createMockMessage({ text: 'hello' }); + + await handleMention(thread, message); + + expect(thread.subscribe).toHaveBeenCalled(); + expect(thread.post).toHaveBeenCalled(); + }); + + it('should handle mention in thread', async () => { + const thread = createMockThread({ threadTs: '123.400' }); + const message = createMockMessage({ text: 'help' }); + + await handleMention(thread, message); + + expect(thread.post).toHaveBeenCalled(); + }); +}); +``` + +### If using Bolt for JavaScript + +```typescript +import { describe, it, expect, vi } from 'vitest'; +import { createMockSlackClient, createMockEvent } from './helpers/mock-client'; + +describe('app_mention handler', () => { + it('should respond to mention in thread', async () => { + const client = createMockSlackClient(); + const event = createMockEvent('app_mention', { + text: '<@U12345678> hello', + channel: 'C12345678', + ts: '123.456', + }); + + await handleAppMention({ event, client, say: vi.fn() }); + + expect(client.chat.postMessage).toHaveBeenCalledWith( + expect.objectContaining({ + channel: 'C12345678', + thread_ts: '123.456', + }) + ); + }); +}); +``` + +--- + +## Testing Slash Commands + +### If using Chat SDK + +```typescript +import { describe, it, expect, vi } from 'vitest'; +import { createMockSlashCommandEvent } from './helpers/mock-thread'; + +describe('/sample-command', () => { + it('should process command and respond', async () => { + const event = createMockSlashCommandEvent({ + text: 'test input', + userId: 'U12345678', + }); + + await handleSampleCommand(event); + + expect(event.thread.post).toHaveBeenCalledWith( + expect.stringContaining('Result') + ); + }); + + it('should handle errors gracefully', async () => { + const event = createMockSlashCommandEvent({ text: '' }); + + await handleSampleCommand(event); + + expect(event.thread.post).toHaveBeenCalledWith( + expect.stringContaining('went wrong') + ); + }); +}); +``` + +### If using Bolt for JavaScript + +```typescript +import { describe, it, expect, vi } from 'vitest'; + +describe('/sample-command', () => { + it('should ack and respond', async () => { + const ack = vi.fn(); + const respond = vi.fn(); + const command = { + text: 'test input', + user_id: 'U12345678', + channel_id: 'C12345678', + response_url: 'https://hooks.slack.com/commands/...', + }; + + await handleSampleCommand({ ack, command, respond }); + + expect(ack).toHaveBeenCalled(); + expect(respond).toHaveBeenCalledWith( + expect.objectContaining({ text: expect.stringContaining('Result') }) + ); + }); +}); +``` + +--- + +## Testing Action Handlers + +### If using Chat SDK + +```typescript +import { describe, it, expect, vi } from 'vitest'; +import { createMockActionEvent } from './helpers/mock-thread'; + +describe('button_click action', () => { + it('should handle button click', async () => { + const event = createMockActionEvent({ + actionId: 'button_click', + value: 'clicked_value', + }); + + await handleButtonClick(event); + + expect(event.thread.post).toHaveBeenCalled(); + }); +}); +``` + +### If using Bolt for JavaScript + +```typescript +import { describe, it, expect, vi } from 'vitest'; + +describe('button_click action', () => { + it('should ack and handle click', async () => { + const ack = vi.fn(); + const client = createMockSlackClient(); + const body = { + actions: [{ value: 'clicked_value' }], + channel: { id: 'C12345678' }, + message: { ts: '123.456' }, + }; + + await handleButtonClick({ ack, body, client }); + + expect(ack).toHaveBeenCalled(); + expect(client.chat.update).toHaveBeenCalled(); + }); +}); +``` + +--- + +## E2E Testing Patterns + +### Full Message Flow (Chat SDK) + +```typescript +describe('E2E: Message Flow', () => { + it('should handle complete mention flow', async () => { + const thread = createMockThread(); + const message = createMockMessage({ text: 'what channels am I in?' }); + + await handleMention(thread, message); + + expect(thread.subscribe).toHaveBeenCalled(); + expect(thread.post).toHaveBeenCalled(); + }); + + it('should handle conversation in thread', async () => { + const thread = createMockThread({ threadTs: '100.001' }); + + const mention = createMockMessage({ text: 'start a task' }); + await handleMention(thread, mention); + + const followUp = createMockMessage({ text: 'continue please' }); + await handleSubscribedMessage(thread, followUp); + + expect(thread.post).toHaveBeenCalledTimes(2); + }); +}); +``` + +### Full Message Flow (Bolt) + +```typescript +describe('E2E: Message Flow', () => { + it('should handle mention and reply in thread', async () => { + const client = createMockSlackClient(); + const event = createMockEvent('app_mention', { + text: '<@UBOT> what channels am I in?', + channel: 'C12345678', + ts: '100.001', + }); + + await handleAppMention({ event, client, say: vi.fn() }); + + expect(client.chat.postMessage).toHaveBeenCalledWith( + expect.objectContaining({ thread_ts: '100.001' }) + ); + }); +}); +``` + +--- + +## Mock Helpers + +### Chat SDK Mock Factories + +```typescript +// lib/__tests__/helpers/mock-thread.ts +import { vi } from 'vitest'; + +export function createMockThread(overrides = {}) { + return { + post: vi.fn().mockResolvedValue(undefined), + subscribe: vi.fn().mockResolvedValue(undefined), + startTyping: vi.fn().mockResolvedValue(undefined), + state: { + get: vi.fn().mockResolvedValue(null), + set: vi.fn().mockResolvedValue(undefined), + }, + channelId: 'C12345678', + threadTs: undefined, + ...overrides, + }; +} + +export function createMockMessage(overrides = {}) { + return { + text: 'test message', + userId: 'U12345678', + ts: '1234567890.123456', + ...overrides, + }; +} + +export function createMockSlashCommandEvent(overrides = {}) { + return { + text: '', + userId: 'U12345678', + channelId: 'C12345678', + thread: createMockThread(), + openModal: vi.fn().mockResolvedValue(undefined), + ...overrides, + }; +} + +export function createMockActionEvent(overrides = {}) { + return { + actionId: '', + value: '', + userId: 'U12345678', + thread: createMockThread(), + ...overrides, + }; +} +``` + +### Bolt Mock Factories + +```typescript +// server/__tests__/helpers/mock-client.ts +import { vi } from 'vitest'; + +export function createMockSlackClient() { + return { + conversations: { + history: vi.fn().mockResolvedValue({ ok: true, messages: [], has_more: false }), + replies: vi.fn().mockResolvedValue({ ok: true, messages: [], has_more: false }), + join: vi.fn().mockResolvedValue({ ok: true, channel: { id: 'C12345678' } }), + list: vi.fn().mockResolvedValue({ ok: true, channels: [] }), + info: vi.fn().mockResolvedValue({ ok: true, channel: { id: 'C12345678', name: 'general' } }), + }, + chat: { + postMessage: vi.fn().mockResolvedValue({ ok: true, ts: '1234567890.123456', channel: 'C12345678' }), + update: vi.fn().mockResolvedValue({ ok: true, ts: '1234567890.123456' }), + delete: vi.fn().mockResolvedValue({ ok: true }), + }, + users: { + info: vi.fn().mockResolvedValue({ ok: true, user: { id: 'U12345678', name: 'testuser' } }), + }, + reactions: { + add: vi.fn().mockResolvedValue({ ok: true }), + remove: vi.fn().mockResolvedValue({ ok: true }), + }, + views: { + open: vi.fn().mockResolvedValue({ ok: true }), + update: vi.fn().mockResolvedValue({ ok: true }), + push: vi.fn().mockResolvedValue({ ok: true }), + }, + }; +} + +export function createMockContext(overrides = {}) { + return { + channel_id: 'C12345678', + dm_channel: 'D12345678', + thread_ts: undefined, + is_dm: false, + team_id: 'T12345678', + user_id: 'U12345678', + ...overrides, + }; +} + +export function createMockEvent(type: string, overrides = {}) { + return { + type, + user: 'U12345678', + channel: 'C12345678', + ts: '1234567890.123456', + event_ts: '1234567890.123456', + ...overrides, + }; +} +``` + +--- + +## Test Coverage Guidelines + +Aim for these coverage targets (both frameworks): + +| Category | Target | +|----------|--------| +| Tools | 90%+ | +| Agent logic | 85%+ | +| Event handlers | 80%+ | +| Utilities | 90%+ | +| Overall | 80%+ | + +Run coverage report: +```bash +pnpm test:coverage +``` diff --git a/.agents/skills/slack-agent/reference/agent-archetypes.md b/.agents/skills/slack-agent/reference/agent-archetypes.md new file mode 100644 index 00000000..8fbbffa1 --- /dev/null +++ b/.agents/skills/slack-agent/reference/agent-archetypes.md @@ -0,0 +1,524 @@ +# Agent Archetypes Reference + +This document provides common patterns and archetypes for Slack agents. Use this as a reference when generating custom implementation plans based on user requirements. + +## Implementation Plan Template + +When generating a plan, use this structure: + +```markdown +## Implementation Plan: [Agent Name] + +### Overview +[1-2 sentence description of what this agent does] + +### Core Features +1. **[Feature 1]** - [Description] +2. **[Feature 2]** - [Description] +3. **[Feature 3]** - [Description] + +### Slash Commands +| Command | Description | Example | +|---------|-------------|---------| +| `/command` | What it does | `/command arg` | + +### Event Handlers + +**If using Chat SDK:** +- [ ] `bot.onNewMention` - [What happens when @mentioned] +- [ ] `bot.onSubscribedMessage` - [Follow-up message handling] +- [ ] `bot.onReaction` - [If applicable] +- [ ] `bot.onSlashCommand` - [Slash command handling] + +**If using Bolt for JavaScript:** +- [ ] `app.event('app_mention')` - [What happens when @mentioned] +- [ ] `app.message()` - [Follow-up message handling] +- [ ] `app.command()` - [Slash command handling] +- [ ] `app.action()` - [Button/interactive handling] + +### AI Tools (if using AI) +| Tool | Purpose | Parameters | +|------|---------|------------| +| `toolName` | What it does | `param1`, `param2` | + +### Scheduled Jobs (if applicable) +| Schedule | Action | +|----------|--------| +| `0 9 * * 1-5` | Description | + +### State Management +- [ ] Stateless (simple request/response) +- [ ] **Chat SDK:** `@chat-adapter/state-redis` for thread-level persistence +- [ ] **Bolt:** Vercel Workflow for durable multi-turn state +- [ ] Database (persistent storage) + - Upstash Redis for Chat SDK state adapter or general caching + - Vercel Blob for file/document storage + - AWS Aurora via Vercel Marketplace for relational data + - NOTE: Do NOT recommend Vercel KV (deprecated) + +### UI Components +**Chat SDK:** JSX components (``, `